jfs-components 0.0.74 → 0.0.78

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (146) hide show
  1. package/CHANGELOG.md +109 -0
  2. package/lib/commonjs/components/Accordion/Accordion.js +55 -55
  3. package/lib/commonjs/components/ActionFooter/ActionFooter.js +193 -82
  4. package/lib/commonjs/components/Avatar/Avatar.js +20 -0
  5. package/lib/commonjs/components/Badge/Badge.js +23 -0
  6. package/lib/commonjs/components/Button/Button.js +37 -0
  7. package/lib/commonjs/components/Checkbox/Checkbox.js +21 -9
  8. package/lib/commonjs/components/DropdownInput/DropdownInput.js +30 -16
  9. package/lib/commonjs/components/ExpandableCheckbox/ExpandableCheckbox.js +167 -0
  10. package/lib/commonjs/components/FormField/FormField.js +14 -1
  11. package/lib/commonjs/components/FullscreenModal/FullscreenModal.js +355 -0
  12. package/lib/commonjs/components/IconButton/IconButton.js +20 -0
  13. package/lib/commonjs/components/Image/Image.js +26 -1
  14. package/lib/commonjs/components/ListItem/ListItem.js +25 -10
  15. package/lib/commonjs/components/LottiePlayer/LottiePlayer.js +116 -0
  16. package/lib/commonjs/components/LottiePlayer/LottiePlayer.web.js +82 -0
  17. package/lib/commonjs/components/LottiePlayer/loadNativeLottieView.js +74 -0
  18. package/lib/commonjs/components/LottiePlayer/loadWebLottieView.js +50 -0
  19. package/lib/commonjs/components/MessageField/MessageField.js +318 -0
  20. package/lib/commonjs/components/NavArrow/NavArrow.js +58 -17
  21. package/lib/commonjs/components/PageHero/PageHero.js +41 -5
  22. package/lib/commonjs/components/RechargeCard/RechargeCard.js +32 -17
  23. package/lib/commonjs/components/Stepper/Step.js +47 -60
  24. package/lib/commonjs/components/Stepper/StepLabel.js +40 -10
  25. package/lib/commonjs/components/Stepper/Stepper.js +15 -17
  26. package/lib/commonjs/components/SuggestiveSearch/SuggestiveSearch.js +487 -0
  27. package/lib/commonjs/components/Text/Text.js +31 -1
  28. package/lib/commonjs/components/TextInput/TextInput.js +16 -1
  29. package/lib/commonjs/components/Title/Title.js +10 -2
  30. package/lib/commonjs/components/index.js +35 -0
  31. package/lib/commonjs/design-tokens/Coin Variables-variables-full.json +1 -1
  32. package/lib/commonjs/icons/Icon.js +16 -0
  33. package/lib/commonjs/icons/registry.js +1 -1
  34. package/lib/commonjs/index.js +12 -0
  35. package/lib/commonjs/skeleton/Skeleton.js +234 -0
  36. package/lib/commonjs/skeleton/SkeletonGroup.js +140 -0
  37. package/lib/commonjs/skeleton/index.js +58 -0
  38. package/lib/commonjs/skeleton/shimmer-tokens.js +189 -0
  39. package/lib/commonjs/skeleton/useReducedMotion.js +64 -0
  40. package/lib/module/components/Accordion/Accordion.js +56 -56
  41. package/lib/module/components/ActionFooter/ActionFooter.js +193 -83
  42. package/lib/module/components/Avatar/Avatar.js +19 -0
  43. package/lib/module/components/Badge/Badge.js +23 -0
  44. package/lib/module/components/Button/Button.js +37 -0
  45. package/lib/module/components/Checkbox/Checkbox.js +22 -10
  46. package/lib/module/components/DropdownInput/DropdownInput.js +30 -16
  47. package/lib/module/components/ExpandableCheckbox/ExpandableCheckbox.js +161 -0
  48. package/lib/module/components/FormField/FormField.js +16 -3
  49. package/lib/module/components/FullscreenModal/FullscreenModal.js +350 -0
  50. package/lib/module/components/IconButton/IconButton.js +20 -0
  51. package/lib/module/components/Image/Image.js +25 -1
  52. package/lib/module/components/ListItem/ListItem.js +25 -10
  53. package/lib/module/components/LottiePlayer/LottiePlayer.js +111 -0
  54. package/lib/module/components/LottiePlayer/LottiePlayer.web.js +77 -0
  55. package/lib/module/components/LottiePlayer/loadNativeLottieView.js +69 -0
  56. package/lib/module/components/LottiePlayer/loadWebLottieView.js +45 -0
  57. package/lib/module/components/MessageField/MessageField.js +313 -0
  58. package/lib/module/components/NavArrow/NavArrow.js +59 -18
  59. package/lib/module/components/PageHero/PageHero.js +41 -5
  60. package/lib/module/components/RechargeCard/RechargeCard.js +33 -17
  61. package/lib/module/components/Stepper/Step.js +48 -61
  62. package/lib/module/components/Stepper/StepLabel.js +40 -10
  63. package/lib/module/components/Stepper/Stepper.js +15 -17
  64. package/lib/module/components/SuggestiveSearch/SuggestiveSearch.js +481 -0
  65. package/lib/module/components/Text/Text.js +31 -1
  66. package/lib/module/components/TextInput/TextInput.js +17 -2
  67. package/lib/module/components/Title/Title.js +10 -2
  68. package/lib/module/components/index.js +5 -0
  69. package/lib/module/design-tokens/Coin Variables-variables-full.json +1 -1
  70. package/lib/module/icons/Icon.js +16 -0
  71. package/lib/module/icons/registry.js +1 -1
  72. package/lib/module/index.js +2 -1
  73. package/lib/module/skeleton/Skeleton.js +229 -0
  74. package/lib/module/skeleton/SkeletonGroup.js +133 -0
  75. package/lib/module/skeleton/index.js +6 -0
  76. package/lib/module/skeleton/shimmer-tokens.js +181 -0
  77. package/lib/module/skeleton/useReducedMotion.js +61 -0
  78. package/lib/typescript/src/components/Accordion/Accordion.d.ts +14 -20
  79. package/lib/typescript/src/components/ActionFooter/ActionFooter.d.ts +26 -21
  80. package/lib/typescript/src/components/Avatar/Avatar.d.ts +7 -1
  81. package/lib/typescript/src/components/Badge/Badge.d.ts +7 -1
  82. package/lib/typescript/src/components/Button/Button.d.ts +8 -1
  83. package/lib/typescript/src/components/ExpandableCheckbox/ExpandableCheckbox.d.ts +63 -0
  84. package/lib/typescript/src/components/FullscreenModal/FullscreenModal.d.ts +99 -0
  85. package/lib/typescript/src/components/IconButton/IconButton.d.ts +7 -1
  86. package/lib/typescript/src/components/Image/Image.d.ts +8 -1
  87. package/lib/typescript/src/components/LottiePlayer/LottiePlayer.d.ts +85 -0
  88. package/lib/typescript/src/components/LottiePlayer/LottiePlayer.web.d.ts +28 -0
  89. package/lib/typescript/src/components/LottiePlayer/loadNativeLottieView.d.ts +11 -0
  90. package/lib/typescript/src/components/LottiePlayer/loadWebLottieView.d.ts +11 -0
  91. package/lib/typescript/src/components/MessageField/MessageField.d.ts +81 -0
  92. package/lib/typescript/src/components/NavArrow/NavArrow.d.ts +10 -5
  93. package/lib/typescript/src/components/PageHero/PageHero.d.ts +31 -5
  94. package/lib/typescript/src/components/Stepper/Step.d.ts +4 -1
  95. package/lib/typescript/src/components/Stepper/StepLabel.d.ts +4 -1
  96. package/lib/typescript/src/components/Stepper/Stepper.d.ts +3 -1
  97. package/lib/typescript/src/components/SuggestiveSearch/SuggestiveSearch.d.ts +123 -0
  98. package/lib/typescript/src/components/Text/Text.d.ts +20 -1
  99. package/lib/typescript/src/components/index.d.ts +8 -3
  100. package/lib/typescript/src/icons/Icon.d.ts +7 -1
  101. package/lib/typescript/src/icons/registry.d.ts +1 -1
  102. package/lib/typescript/src/index.d.ts +1 -0
  103. package/lib/typescript/src/skeleton/Skeleton.d.ts +60 -0
  104. package/lib/typescript/src/skeleton/SkeletonGroup.d.ts +78 -0
  105. package/lib/typescript/src/skeleton/index.d.ts +5 -0
  106. package/lib/typescript/src/skeleton/shimmer-tokens.d.ts +160 -0
  107. package/lib/typescript/src/skeleton/useReducedMotion.d.ts +15 -0
  108. package/package.json +11 -1
  109. package/src/components/Accordion/Accordion.tsx +113 -73
  110. package/src/components/ActionFooter/ActionFooter.tsx +210 -92
  111. package/src/components/Avatar/Avatar.tsx +26 -0
  112. package/src/components/Badge/Badge.tsx +27 -0
  113. package/src/components/Button/Button.tsx +40 -0
  114. package/src/components/Checkbox/Checkbox.tsx +22 -9
  115. package/src/components/DropdownInput/DropdownInput.tsx +67 -39
  116. package/src/components/ExpandableCheckbox/ExpandableCheckbox.tsx +237 -0
  117. package/src/components/FormField/FormField.tsx +19 -3
  118. package/src/components/FullscreenModal/FullscreenModal.tsx +414 -0
  119. package/src/components/IconButton/IconButton.tsx +27 -0
  120. package/src/components/Image/Image.tsx +25 -0
  121. package/src/components/ListItem/ListItem.tsx +21 -10
  122. package/src/components/LottiePlayer/LottiePlayer.tsx +145 -0
  123. package/src/components/LottiePlayer/LottiePlayer.web.tsx +94 -0
  124. package/src/components/LottiePlayer/loadNativeLottieView.tsx +87 -0
  125. package/src/components/LottiePlayer/loadWebLottieView.tsx +64 -0
  126. package/src/components/MessageField/MessageField.tsx +543 -0
  127. package/src/components/NavArrow/NavArrow.tsx +81 -17
  128. package/src/components/PageHero/PageHero.tsx +61 -4
  129. package/src/components/RechargeCard/RechargeCard.tsx +32 -24
  130. package/src/components/Stepper/Step.tsx +52 -51
  131. package/src/components/Stepper/StepLabel.tsx +46 -9
  132. package/src/components/Stepper/Stepper.tsx +20 -15
  133. package/src/components/SuggestiveSearch/SuggestiveSearch.tsx +756 -0
  134. package/src/components/Text/Text.tsx +54 -0
  135. package/src/components/TextInput/TextInput.tsx +14 -1
  136. package/src/components/Title/Title.tsx +13 -2
  137. package/src/components/index.ts +8 -3
  138. package/src/design-tokens/Coin Variables-variables-full.json +1 -1
  139. package/src/icons/Icon.tsx +17 -0
  140. package/src/icons/registry.ts +1 -1
  141. package/src/index.ts +1 -0
  142. package/src/skeleton/Skeleton.tsx +298 -0
  143. package/src/skeleton/SkeletonGroup.tsx +193 -0
  144. package/src/skeleton/index.ts +10 -0
  145. package/src/skeleton/shimmer-tokens.ts +221 -0
  146. package/src/skeleton/useReducedMotion.ts +72 -0
@@ -0,0 +1,69 @@
1
+ "use strict";
2
+
3
+ import React from 'react';
4
+ import { Text, View } from 'react-native';
5
+
6
+ /** Props we forward to the underlying native Lottie view. */
7
+ import { jsx as _jsx } from "react/jsx-runtime";
8
+ const INSTALL_HINT = 'LottiePlayer requires lottie-react-native in your app.\n' + ' npm install lottie-react-native\n' + ' cd ios && pod install';
9
+
10
+ /**
11
+ * Metro resolves `require('lottie-react-native')` at bundle time even inside
12
+ * try/catch, which breaks apps that import `jfs-components` without having
13
+ * the optional peer installed. Splitting the module id into a runtime string
14
+ * keeps Metro from statically linking it — the native module is loaded only
15
+ * when present in the consumer's node_modules.
16
+ */
17
+ function resolveNativeLottieModuleName() {
18
+ return ['lottie', '-react', '-native'].join('');
19
+ }
20
+ function LottieUnavailableView({
21
+ style
22
+ }) {
23
+ if (__DEV__) {
24
+ return /*#__PURE__*/_jsx(View, {
25
+ style: [style, {
26
+ alignItems: 'center',
27
+ justifyContent: 'center',
28
+ backgroundColor: 'rgba(255, 196, 0, 0.12)',
29
+ borderWidth: 1,
30
+ borderColor: 'rgba(255, 196, 0, 0.45)',
31
+ borderRadius: 8,
32
+ padding: 8
33
+ }],
34
+ children: /*#__PURE__*/_jsx(Text, {
35
+ style: {
36
+ color: '#8a6d00',
37
+ fontSize: 11,
38
+ textAlign: 'center',
39
+ lineHeight: 15
40
+ },
41
+ children: INSTALL_HINT
42
+ })
43
+ });
44
+ }
45
+ return /*#__PURE__*/_jsx(View, {
46
+ style: style
47
+ });
48
+ }
49
+ function LottieUnavailable(props) {
50
+ React.useEffect(() => {
51
+ if (__DEV__) {
52
+ console.warn(`[jfs-components/LottiePlayer] ${INSTALL_HINT}`);
53
+ }
54
+ }, []);
55
+ return /*#__PURE__*/_jsx(LottieUnavailableView, {
56
+ style: props.style
57
+ });
58
+ }
59
+ let cachedView;
60
+ export function getNativeLottieView() {
61
+ if (cachedView !== undefined) return cachedView;
62
+ try {
63
+ const mod = require(resolveNativeLottieModuleName());
64
+ cachedView = mod.default ?? LottieUnavailable;
65
+ } catch {
66
+ cachedView = LottieUnavailable;
67
+ }
68
+ return cachedView;
69
+ }
@@ -0,0 +1,45 @@
1
+ "use strict";
2
+
3
+ import React from 'react';
4
+
5
+ /** Props we forward to the underlying web Lottie view. */
6
+ import { jsx as _jsx } from "react/jsx-runtime";
7
+ const INSTALL_HINT = 'LottiePlayer requires lottie-react in your app.\n' + ' npm install lottie-react';
8
+ function resolveWebLottieModuleName() {
9
+ return ['lottie', '-react'].join('');
10
+ }
11
+ function LottieUnavailable(props) {
12
+ React.useEffect(() => {
13
+ if (typeof __DEV__ !== 'undefined' && __DEV__) {
14
+ console.warn(`[jfs-components/LottiePlayer] ${INSTALL_HINT}`);
15
+ }
16
+ }, []);
17
+ return /*#__PURE__*/_jsx("div", {
18
+ style: {
19
+ ...props.style,
20
+ display: 'flex',
21
+ alignItems: 'center',
22
+ justifyContent: 'center',
23
+ backgroundColor: 'rgba(255, 196, 0, 0.12)',
24
+ border: '1px solid rgba(255, 196, 0, 0.45)',
25
+ borderRadius: 8,
26
+ padding: 8,
27
+ color: '#8a6d00',
28
+ fontSize: 11,
29
+ textAlign: 'center',
30
+ lineHeight: '15px'
31
+ },
32
+ children: typeof __DEV__ !== 'undefined' && __DEV__ ? INSTALL_HINT : null
33
+ });
34
+ }
35
+ let cachedView;
36
+ export function getWebLottieView() {
37
+ if (cachedView !== undefined) return cachedView;
38
+ try {
39
+ const mod = require(resolveWebLottieModuleName());
40
+ cachedView = mod.default ?? LottieUnavailable;
41
+ } catch {
42
+ cachedView = LottieUnavailable;
43
+ }
44
+ return cachedView;
45
+ }
@@ -0,0 +1,313 @@
1
+ "use strict";
2
+
3
+ import React, { useCallback, useMemo, useRef, useState } from 'react';
4
+ import { View, Text, Pressable, TextInput as RNTextInput } from 'react-native';
5
+ import { getVariableByName } from '../../design-tokens/figma-variables-resolver';
6
+ import { useTokens } from '../../design-tokens/JFSThemeProvider';
7
+ import { EMPTY_MODES } from '../../utils/react-utils';
8
+ import { useFormContext } from '../Form/Form';
9
+
10
+ // ---------------------------------------------------------------------------
11
+ // Types
12
+ // ---------------------------------------------------------------------------
13
+
14
+ /**
15
+ * Visual state of the textarea. Mirrors the `FormField States` collection so
16
+ * MessageField slots into the same theming pipeline as FormField. The state
17
+ * is always derived from props (`isInvalid`, `isDisabled`, `isReadOnly` and
18
+ * focus) and is locked in `modes['FormField States']` — passing that key in
19
+ * `modes` is intentionally ignored to keep interactive behaviour and visual
20
+ * state in sync.
21
+ */
22
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
23
+ // ---------------------------------------------------------------------------
24
+ // Token helpers
25
+ // ---------------------------------------------------------------------------
26
+
27
+ function toNumber(value, fallback) {
28
+ if (typeof value === 'number' && Number.isFinite(value)) return value;
29
+ if (typeof value === 'string') {
30
+ const parsed = parseFloat(value);
31
+ if (Number.isFinite(parsed)) return parsed;
32
+ }
33
+ return fallback;
34
+ }
35
+ function toFontWeight(value, fallback) {
36
+ if (typeof value === 'number') return value.toString();
37
+ if (typeof value === 'string' && value.length > 0) return value;
38
+ return fallback;
39
+ }
40
+ function firstError(error) {
41
+ if (!error) return undefined;
42
+ if (Array.isArray(error)) return error[0];
43
+ return error;
44
+ }
45
+ function useMessageFieldTokens(modes) {
46
+ return useMemo(() => {
47
+ const wrapperGap = toNumber(getVariableByName('messageField/gap', modes), 8);
48
+ const labelColor = getVariableByName('messageField/label/foreground', modes) || '#000000';
49
+ const labelFontFamily = getVariableByName('messageField/label/fontFamily', modes) || 'JioType Var';
50
+ const labelFontSize = toNumber(getVariableByName('messageField/label/fontSize', modes), 14);
51
+ const labelLineHeight = toNumber(getVariableByName('messageField/label/lineHeight', modes), 17);
52
+ const labelFontWeight = toFontWeight(getVariableByName('messageField/label/fontWeight', modes), '500');
53
+ const textareaBackground = getVariableByName('messageField/textarea/background', modes) || '#ffffff';
54
+ const textareaBorderColor = getVariableByName('messageField/textarea/border/color', modes) || '#b5b6b7';
55
+ const textareaBorderSize = toNumber(getVariableByName('messageField/textarea/border/size', modes), 1.5);
56
+ const textareaRadius = toNumber(getVariableByName('messageField/textarea/radius', modes), 8);
57
+ const textareaPadding = toNumber(getVariableByName('messageField/textarea/padding', modes), 12);
58
+ const textareaHeight = toNumber(getVariableByName('messageField/textarea/height', modes), 108);
59
+ const textareaGap = toNumber(getVariableByName('messageField/textarea/gap', modes), 0);
60
+
61
+ // `messageField/text/foreground` is the input text color. It also
62
+ // serves as the placeholder color — in mode-aware token sets it
63
+ // resolves to a muted/idle color when empty and shifts darker via
64
+ // the `FormField States` cascade once typed-state tokens land. We
65
+ // never re-route this through another token (e.g. the counter
66
+ // color) because that conflates two semantically distinct tokens.
67
+ const inputTextColor = getVariableByName('messageField/text/foreground', modes) || '#707275';
68
+ const inputFontFamily = getVariableByName('messageField/text/fontFamily', modes) || 'JioType Var';
69
+ const inputFontSize = toNumber(getVariableByName('messageField/text/fontSize', modes), 16);
70
+ const inputLineHeight = toNumber(getVariableByName('messageField/text/lineHeight', modes), 21);
71
+ const inputFontWeight = toFontWeight(getVariableByName('messageField/text/fontWeight', modes), '400');
72
+ const counterColor = getVariableByName('messageField/maxLength/foreground', modes) || '#24262b';
73
+ const counterFontFamily = getVariableByName('messageField/maxLength/fontFamily', modes) || 'JioType Var';
74
+ const counterFontSize = toNumber(getVariableByName('messageField/maxLength/fontSize', modes), 14);
75
+ const counterLineHeight = toNumber(getVariableByName('messageField/maxLength/lineHeight', modes), 18);
76
+ const counterFontWeight = toFontWeight(getVariableByName('messageField/maxLength/fontWeight', modes), '400');
77
+ return {
78
+ wrapperGap,
79
+ labelColor,
80
+ labelFontFamily,
81
+ labelFontSize,
82
+ labelLineHeight,
83
+ labelFontWeight,
84
+ textareaBackground,
85
+ textareaBorderColor,
86
+ textareaBorderSize,
87
+ textareaRadius,
88
+ textareaPadding,
89
+ textareaHeight,
90
+ textareaGap,
91
+ inputTextColor,
92
+ inputFontFamily,
93
+ inputFontSize,
94
+ inputLineHeight,
95
+ inputFontWeight,
96
+ counterColor,
97
+ counterFontFamily,
98
+ counterFontSize,
99
+ counterLineHeight,
100
+ counterFontWeight
101
+ };
102
+ }, [modes]);
103
+ }
104
+
105
+ // ---------------------------------------------------------------------------
106
+ // Component
107
+ // ---------------------------------------------------------------------------
108
+
109
+ const REQUIRED_INDICATOR_COLOR = '#d93d3d';
110
+ function MessageField({
111
+ label,
112
+ placeholder,
113
+ value,
114
+ defaultValue,
115
+ onChangeText,
116
+ name,
117
+ maxLength,
118
+ showCounter,
119
+ rows,
120
+ isRequired = false,
121
+ isDisabled = false,
122
+ isInvalid = false,
123
+ isReadOnly = false,
124
+ autoFocus = false,
125
+ modes: propModes = EMPTY_MODES,
126
+ style,
127
+ textareaStyle,
128
+ inputStyle,
129
+ accessibilityLabel,
130
+ accessibilityHint,
131
+ testID,
132
+ onFocus,
133
+ onBlur
134
+ }) {
135
+ const formCtx = useFormContext();
136
+ const formError = name && formCtx ? firstError(formCtx.validationErrors[name]) : undefined;
137
+ const resolvedIsInvalid = isInvalid || Boolean(formError);
138
+ const isControlled = value !== undefined;
139
+ const [uncontrolledValue, setUncontrolledValue] = useState(defaultValue ?? '');
140
+ const currentValue = isControlled ? value : uncontrolledValue;
141
+ const [isFocused, setIsFocused] = useState(false);
142
+ const interactive = !isDisabled && !isReadOnly;
143
+
144
+ // Ref to the native textarea so tapping anywhere in the (padded) textarea
145
+ // container focuses it on the FIRST tap, fixing the Android "two taps to
146
+ // open the keyboard" issue.
147
+ const inputRef = useRef(null);
148
+ const focusInput = useCallback(() => {
149
+ if (!interactive) return;
150
+ inputRef.current?.focus();
151
+ }, [interactive]);
152
+ const {
153
+ modes: globalModes
154
+ } = useTokens();
155
+ const baseModes = useMemo(() => ({
156
+ ...globalModes,
157
+ ...propModes
158
+ }), [globalModes, propModes]);
159
+
160
+ // FormField States cascade — error > disabled > read only > active (focus)
161
+ // > idle. Always derived from props and locked into the modes object so
162
+ // consumers cannot pass `modes={{ 'FormField States': ... }}` and get out
163
+ // of sync with the component's actual interactive behaviour.
164
+ const stateMode = useMemo(() => {
165
+ if (resolvedIsInvalid) return 'Error';
166
+ if (isDisabled) return 'Disabled';
167
+ if (isReadOnly) return 'Read Only';
168
+ if (isFocused) return 'Active';
169
+ return 'Idle';
170
+ }, [resolvedIsInvalid, isDisabled, isReadOnly, isFocused]);
171
+ const modes = useMemo(() => ({
172
+ ...baseModes,
173
+ 'FormField States': stateMode
174
+ }), [baseModes, stateMode]);
175
+ const tokens = useMessageFieldTokens(modes);
176
+
177
+ // ---------- Event handlers ---------------------------------------------
178
+ const handleFocus = useCallback(e => {
179
+ setIsFocused(true);
180
+ onFocus?.(e);
181
+ }, [onFocus]);
182
+ const handleBlur = useCallback(e => {
183
+ setIsFocused(false);
184
+ onBlur?.(e);
185
+ }, [onBlur]);
186
+ const handleChangeText = useCallback(next => {
187
+ if (!isControlled) {
188
+ setUncontrolledValue(next);
189
+ }
190
+ onChangeText?.(next);
191
+ if (name && formCtx) formCtx.onFieldChange(name);
192
+ }, [isControlled, onChangeText, name, formCtx]);
193
+
194
+ // ---------- Derived layout values --------------------------------------
195
+ const computedHeight = useMemo(() => {
196
+ if (rows && rows > 0) {
197
+ return Math.round(rows * tokens.inputLineHeight + 2 * tokens.textareaPadding);
198
+ }
199
+ return tokens.textareaHeight;
200
+ }, [rows, tokens.inputLineHeight, tokens.textareaPadding, tokens.textareaHeight]);
201
+ const shouldShowCounter = useMemo(() => {
202
+ if (showCounter === false) return false;
203
+ if (showCounter === true) return true;
204
+ return typeof maxLength === 'number';
205
+ }, [showCounter, maxLength]);
206
+ const counterText = useMemo(() => {
207
+ const count = currentValue.length;
208
+ if (typeof maxLength === 'number') return `${count}/${maxLength}`;
209
+ return `${count}`;
210
+ }, [currentValue.length, maxLength]);
211
+
212
+ // ---------- Styles -----------------------------------------------------
213
+ const wrapperStyle = useMemo(() => ({
214
+ gap: tokens.wrapperGap,
215
+ width: '100%'
216
+ }), [tokens.wrapperGap]);
217
+ const labelRowStyle = useMemo(() => ({
218
+ flexDirection: 'row',
219
+ alignItems: 'baseline'
220
+ }), []);
221
+ const labelTextStyle = useMemo(() => ({
222
+ color: tokens.labelColor,
223
+ fontFamily: tokens.labelFontFamily,
224
+ fontSize: tokens.labelFontSize,
225
+ lineHeight: tokens.labelLineHeight,
226
+ fontWeight: tokens.labelFontWeight
227
+ }), [tokens.labelColor, tokens.labelFontFamily, tokens.labelFontSize, tokens.labelLineHeight, tokens.labelFontWeight]);
228
+ const requiredIndicatorStyle = useMemo(() => ({
229
+ ...labelTextStyle,
230
+ color: REQUIRED_INDICATOR_COLOR
231
+ }), [labelTextStyle]);
232
+ const textareaContainerStyle = useMemo(() => ({
233
+ backgroundColor: tokens.textareaBackground,
234
+ borderColor: tokens.textareaBorderColor,
235
+ borderWidth: tokens.textareaBorderSize,
236
+ borderStyle: 'solid',
237
+ borderRadius: tokens.textareaRadius,
238
+ padding: tokens.textareaPadding,
239
+ height: computedHeight,
240
+ width: '100%',
241
+ overflow: 'hidden',
242
+ // The gap token is for content within the textarea (icons, etc.);
243
+ // we keep it so downstream layouts that pass children align.
244
+ gap: tokens.textareaGap
245
+ }), [tokens.textareaBackground, tokens.textareaBorderColor, tokens.textareaBorderSize, tokens.textareaRadius, tokens.textareaPadding, computedHeight, tokens.textareaGap]);
246
+ const inputTextStyle = useMemo(() => ({
247
+ flex: 1,
248
+ color: tokens.inputTextColor,
249
+ fontFamily: tokens.inputFontFamily,
250
+ fontSize: tokens.inputFontSize,
251
+ lineHeight: tokens.inputLineHeight,
252
+ fontWeight: tokens.inputFontWeight,
253
+ padding: 0,
254
+ margin: 0,
255
+ textAlignVertical: 'top',
256
+ // Disable the default web focus ring; the textarea border
257
+ // already encodes focus state.
258
+ outlineStyle: 'none',
259
+ outlineWidth: 0,
260
+ outlineColor: 'transparent'
261
+ }), [tokens.inputTextColor, tokens.inputFontFamily, tokens.inputFontSize, tokens.inputLineHeight, tokens.inputFontWeight]);
262
+ const counterTextStyle = useMemo(() => ({
263
+ color: tokens.counterColor,
264
+ fontFamily: tokens.counterFontFamily,
265
+ fontSize: tokens.counterFontSize,
266
+ lineHeight: tokens.counterLineHeight,
267
+ fontWeight: tokens.counterFontWeight,
268
+ textAlign: 'right',
269
+ width: '100%'
270
+ }), [tokens.counterColor, tokens.counterFontFamily, tokens.counterFontSize, tokens.counterLineHeight, tokens.counterFontWeight]);
271
+ const resolvedA11yLabel = accessibilityLabel || label || placeholder || 'Message field';
272
+ return /*#__PURE__*/_jsxs(View, {
273
+ style: [wrapperStyle, style],
274
+ pointerEvents: isDisabled ? 'none' : 'auto',
275
+ testID: testID,
276
+ accessible: false,
277
+ children: [label != null && label !== '' && /*#__PURE__*/_jsxs(View, {
278
+ style: labelRowStyle,
279
+ children: [/*#__PURE__*/_jsx(Text, {
280
+ style: labelTextStyle,
281
+ children: label
282
+ }), isRequired && /*#__PURE__*/_jsx(Text, {
283
+ style: requiredIndicatorStyle,
284
+ children: " *"
285
+ })]
286
+ }), /*#__PURE__*/_jsx(Pressable, {
287
+ style: [textareaContainerStyle, textareaStyle],
288
+ onPress: focusInput,
289
+ accessible: false,
290
+ children: /*#__PURE__*/_jsx(RNTextInput, {
291
+ ref: inputRef,
292
+ multiline: true,
293
+ value: currentValue,
294
+ onChangeText: handleChangeText,
295
+ onFocus: handleFocus,
296
+ onBlur: handleBlur,
297
+ placeholder: placeholder ?? '',
298
+ placeholderTextColor: tokens.inputTextColor,
299
+ editable: interactive,
300
+ maxLength: maxLength,
301
+ autoFocus: autoFocus,
302
+ accessibilityLabel: resolvedA11yLabel,
303
+ accessibilityHint: accessibilityHint,
304
+ style: [inputTextStyle, inputStyle]
305
+ })
306
+ }), shouldShowCounter && /*#__PURE__*/_jsx(Text, {
307
+ style: counterTextStyle,
308
+ accessibilityElementsHidden: true,
309
+ children: counterText
310
+ })]
311
+ });
312
+ }
313
+ export default MessageField;
@@ -1,11 +1,22 @@
1
1
  "use strict";
2
2
 
3
3
  import React, { useMemo } from 'react';
4
- import { View } from 'react-native';
4
+ import { Platform, Pressable, View } from 'react-native';
5
5
  import Svg, { Polyline } from 'react-native-svg';
6
6
  import { getVariableByName } from '../../design-tokens/figma-variables-resolver';
7
+ import { usePressableWebSupport } from '../../utils/web-platform-utils';
7
8
  import { EMPTY_MODES } from '../../utils/react-utils';
8
9
  import { jsx as _jsx } from "react/jsx-runtime";
10
+ /** Minimum touch target per iOS HIG / Material accessibility guidance. */
11
+ const MIN_TOUCH_TARGET = 44;
12
+ const IS_IOS = Platform.OS === 'ios';
13
+ const PRESS_DELAY = IS_IOS ? 130 : 0;
14
+ const touchTargetStyle = {
15
+ minWidth: MIN_TOUCH_TARGET,
16
+ minHeight: MIN_TOUCH_TARGET,
17
+ alignItems: 'center',
18
+ justifyContent: 'center'
19
+ };
9
20
  function resolveNavArrowTokens(modes) {
10
21
  const iconColor = getVariableByName('navArrow/icon/color', modes) || '#24262b';
11
22
  const widthToken = Number(getVariableByName('navArrow/width', modes)) || 6;
@@ -46,6 +57,8 @@ function NavArrow({
46
57
  modes = EMPTY_MODES,
47
58
  style,
48
59
  accessibilityLabel,
60
+ onPress,
61
+ disabled = false,
49
62
  ...rest
50
63
  }) {
51
64
  const tokens = useMemo(() => resolveNavArrowTokens(modes), [modes]);
@@ -59,8 +72,7 @@ function NavArrow({
59
72
  borderRadius: tokens.borderRadius,
60
73
  backgroundColor: tokens.backgroundColor,
61
74
  alignItems: 'center',
62
- justifyContent: 'center',
63
- ...(style || {})
75
+ justifyContent: 'center'
64
76
  };
65
77
  const chevronW = isDown ? tokens.iconHeight : tokens.iconWidth;
66
78
  const chevronH = isDown ? tokens.iconWidth : tokens.iconHeight;
@@ -86,26 +98,55 @@ function NavArrow({
86
98
  svgHeight,
87
99
  points
88
100
  };
89
- }, [tokens, direction, style]);
101
+ }, [tokens, direction]);
90
102
  const defaultAccessibilityLabel = accessibilityLabel || (direction === 'Back' ? 'Go back' : direction === 'Forward' ? 'Go forward' : 'Go down');
103
+ const webProps = usePressableWebSupport({
104
+ restProps: rest,
105
+ onPress,
106
+ disabled,
107
+ accessibilityLabel: defaultAccessibilityLabel
108
+ });
109
+ const chevron = /*#__PURE__*/_jsx(Svg, {
110
+ width: computed.svgWidth,
111
+ height: computed.svgHeight,
112
+ viewBox: `0 0 ${computed.svgWidth} ${computed.svgHeight}`,
113
+ children: /*#__PURE__*/_jsx(Polyline, {
114
+ points: computed.points,
115
+ stroke: tokens.iconColor,
116
+ strokeWidth: tokens.strokeWeight,
117
+ strokeLinecap: "round",
118
+ strokeLinejoin: "round",
119
+ fill: "none"
120
+ })
121
+ });
122
+ if (onPress) {
123
+ return /*#__PURE__*/_jsx(Pressable, {
124
+ onPress: onPress,
125
+ disabled: disabled,
126
+ accessibilityRole: "button",
127
+ accessibilityLabel: defaultAccessibilityLabel,
128
+ accessibilityState: {
129
+ disabled
130
+ },
131
+ unstable_pressDelay: PRESS_DELAY,
132
+ style: ({
133
+ pressed
134
+ }) => [touchTargetStyle, style, pressed && !disabled ? {
135
+ opacity: 0.7
136
+ } : null],
137
+ ...webProps,
138
+ children: /*#__PURE__*/_jsx(View, {
139
+ style: computed.containerStyle,
140
+ children: chevron
141
+ })
142
+ });
143
+ }
91
144
  return /*#__PURE__*/_jsx(View, {
92
- style: computed.containerStyle,
145
+ style: [computed.containerStyle, style],
93
146
  accessibilityRole: "image",
94
147
  accessibilityLabel: defaultAccessibilityLabel,
95
148
  ...rest,
96
- children: /*#__PURE__*/_jsx(Svg, {
97
- width: computed.svgWidth,
98
- height: computed.svgHeight,
99
- viewBox: `0 0 ${computed.svgWidth} ${computed.svgHeight}`,
100
- children: /*#__PURE__*/_jsx(Polyline, {
101
- points: computed.points,
102
- stroke: tokens.iconColor,
103
- strokeWidth: tokens.strokeWeight,
104
- strokeLinecap: "round",
105
- strokeLinejoin: "round",
106
- fill: "none"
107
- })
108
- })
149
+ children: chevron
109
150
  });
110
151
  }
111
152
  export default /*#__PURE__*/React.memo(NavArrow);
@@ -9,12 +9,14 @@ import { EMPTY_MODES, cloneChildrenWithModes } from '../../utils/react-utils';
9
9
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
10
10
  /**
11
11
  * PageHero displays a centered hero block typically used at the top of a page
12
- * or feature screen. It contains an eyebrow line, a large headline, an optional
13
- * supporting line (e.g. price/timeline), and an optional action button.
12
+ * or feature screen. It contains an optional media slot (illustration / image
13
+ * / Lottie / SVG / video — consumer's choice), an eyebrow line, a large
14
+ * headline, an optional supporting line (e.g. price / timeline), and an
15
+ * optional action button.
14
16
  *
15
17
  * All visual values are resolved from Figma design tokens via
16
- * `getVariableByName`. The button slot cascades the active `modes` to its
17
- * children through `cloneChildrenWithModes`.
18
+ * `getVariableByName`. Slots cascade the active `modes` to their children
19
+ * through `cloneChildrenWithModes`.
18
20
  *
19
21
  * @component
20
22
  * @example
@@ -25,6 +27,13 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
25
27
  * supportingText="₹999/year · ₹0 until 2027"
26
28
  * buttonLabel="Renew for free"
27
29
  * onButtonPress={() => navigate('Upgrade')}
30
+ * media={
31
+ * <Image
32
+ * imageSource={require('./assets/upgrade.png')}
33
+ * width={117}
34
+ * height={117}
35
+ * />
36
+ * }
28
37
  * />
29
38
  * ```
30
39
  */
@@ -37,6 +46,7 @@ function PageHero({
37
46
  onButtonPress,
38
47
  showButton = true,
39
48
  buttonSlot,
49
+ media,
40
50
  modes: propModes = EMPTY_MODES,
41
51
  style,
42
52
  testID
@@ -51,6 +61,12 @@ function PageHero({
51
61
  const gap = Number(getVariableByName('PageHero/gap', modes)) || 16;
52
62
  const paddingHorizontal = Number(getVariableByName('PageHero/padding/horizontal', modes)) || 0;
53
63
  const textWrapGap = Number(getVariableByName('PageHero/textWrap/gap', modes)) || 8;
64
+
65
+ // Media slot box — matches the 117×117 frame in Figma (node 4540:7845).
66
+ // Tokens fall back to 117 when not defined in the variables collection,
67
+ // so the layout stays stable on consumers that haven't tokenized this yet.
68
+ const mediaWidth = Number(getVariableByName('media/width', modes)) || 117;
69
+ const mediaHeight = Number(getVariableByName('media/height', modes)) || 117;
54
70
  const eyebrowColor = getVariableByName('PageHero/eyebrow/color', modes) || '#ffffff';
55
71
  const eyebrowFontFamily = getVariableByName('PageHero/eyebrow/fontFamily', modes) || 'System';
56
72
  const eyebrowFontSize = Number(getVariableByName('PageHero/eyebrow/fontSize', modes)) || 18;
@@ -126,10 +142,30 @@ function PageHero({
126
142
  // shape never does.
127
143
  // eslint-disable-next-line react-hooks/exhaustive-deps
128
144
  }, [buttonSlot, showButton, buttonLabel, onButtonPress, modes]);
145
+
146
+ // Sized container for the media slot. Always rendered when `media` is
147
+ // provided, so the slot has a predictable box (matches Figma frame
148
+ // 4540:7845 — 117×117 by default) even if the inner element omits its
149
+ // own width/height. `overflow: 'hidden'` mirrors the Figma frame's
150
+ // `clipsContent` so a slightly oversized illustration doesn't break
151
+ // the centered layout.
152
+ const mediaContent = useMemo(() => {
153
+ if (media === undefined || media === null) return null;
154
+ return /*#__PURE__*/_jsx(View, {
155
+ style: {
156
+ width: mediaWidth,
157
+ height: mediaHeight,
158
+ alignItems: 'center',
159
+ justifyContent: 'center',
160
+ overflow: 'hidden'
161
+ },
162
+ children: cloneChildrenWithModes(media, modes)
163
+ });
164
+ }, [media, mediaWidth, mediaHeight, modes]);
129
165
  return /*#__PURE__*/_jsxs(View, {
130
166
  style: [containerStyle, style],
131
167
  testID: testID,
132
- children: [/*#__PURE__*/_jsxs(View, {
168
+ children: [mediaContent, /*#__PURE__*/_jsxs(View, {
133
169
  style: textWrapStyle,
134
170
  children: [eyebrow ? /*#__PURE__*/_jsx(Text, {
135
171
  style: eyebrowStyle,