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.
Files changed (87) hide show
  1. package/CHANGELOG.md +28 -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/Attached/Attached.js +144 -0
  5. package/lib/commonjs/components/Card/Card.js +25 -2
  6. package/lib/commonjs/components/Checkbox/Checkbox.js +21 -9
  7. package/lib/commonjs/components/DropdownInput/DropdownInput.js +30 -16
  8. package/lib/commonjs/components/ExpandableCheckbox/ExpandableCheckbox.js +167 -0
  9. package/lib/commonjs/components/FormField/FormField.js +14 -1
  10. package/lib/commonjs/components/FullscreenModal/FullscreenModal.js +353 -0
  11. package/lib/commonjs/components/ListItem/ListItem.js +46 -24
  12. package/lib/commonjs/components/MessageField/MessageField.js +318 -0
  13. package/lib/commonjs/components/NavArrow/NavArrow.js +58 -17
  14. package/lib/commonjs/components/PlanComparisonCard/PlanComparisonCard.js +328 -0
  15. package/lib/commonjs/components/Slot/Slot.js +73 -0
  16. package/lib/commonjs/components/Stepper/Step.js +47 -60
  17. package/lib/commonjs/components/Stepper/StepLabel.js +40 -10
  18. package/lib/commonjs/components/Stepper/Stepper.js +15 -17
  19. package/lib/commonjs/components/SuggestiveSearch/SuggestiveSearch.js +487 -0
  20. package/lib/commonjs/components/TextInput/TextInput.js +16 -1
  21. package/lib/commonjs/components/Title/Title.js +10 -2
  22. package/lib/commonjs/components/index.js +49 -0
  23. package/lib/commonjs/design-tokens/Coin Variables-variables-full.json +1 -1
  24. package/lib/commonjs/icons/registry.js +1 -1
  25. package/lib/module/components/Accordion/Accordion.js +56 -56
  26. package/lib/module/components/ActionFooter/ActionFooter.js +50 -4
  27. package/lib/module/components/Attached/Attached.js +139 -0
  28. package/lib/module/components/Card/Card.js +25 -2
  29. package/lib/module/components/Checkbox/Checkbox.js +22 -10
  30. package/lib/module/components/DropdownInput/DropdownInput.js +30 -16
  31. package/lib/module/components/ExpandableCheckbox/ExpandableCheckbox.js +161 -0
  32. package/lib/module/components/FormField/FormField.js +16 -3
  33. package/lib/module/components/FullscreenModal/FullscreenModal.js +348 -0
  34. package/lib/module/components/ListItem/ListItem.js +46 -24
  35. package/lib/module/components/MessageField/MessageField.js +313 -0
  36. package/lib/module/components/NavArrow/NavArrow.js +59 -18
  37. package/lib/module/components/PlanComparisonCard/PlanComparisonCard.js +322 -0
  38. package/lib/module/components/Slot/Slot.js +68 -0
  39. package/lib/module/components/Stepper/Step.js +48 -61
  40. package/lib/module/components/Stepper/StepLabel.js +40 -10
  41. package/lib/module/components/Stepper/Stepper.js +15 -17
  42. package/lib/module/components/SuggestiveSearch/SuggestiveSearch.js +481 -0
  43. package/lib/module/components/TextInput/TextInput.js +17 -2
  44. package/lib/module/components/Title/Title.js +10 -2
  45. package/lib/module/components/index.js +7 -0
  46. package/lib/module/design-tokens/Coin Variables-variables-full.json +1 -1
  47. package/lib/module/icons/registry.js +1 -1
  48. package/lib/typescript/src/components/Accordion/Accordion.d.ts +14 -20
  49. package/lib/typescript/src/components/Attached/Attached.d.ts +61 -0
  50. package/lib/typescript/src/components/Card/Card.d.ts +9 -2
  51. package/lib/typescript/src/components/ExpandableCheckbox/ExpandableCheckbox.d.ts +63 -0
  52. package/lib/typescript/src/components/FullscreenModal/FullscreenModal.d.ts +99 -0
  53. package/lib/typescript/src/components/ListItem/ListItem.d.ts +15 -5
  54. package/lib/typescript/src/components/MessageField/MessageField.d.ts +81 -0
  55. package/lib/typescript/src/components/NavArrow/NavArrow.d.ts +10 -5
  56. package/lib/typescript/src/components/PlanComparisonCard/PlanComparisonCard.d.ts +64 -0
  57. package/lib/typescript/src/components/Slot/Slot.d.ts +52 -0
  58. package/lib/typescript/src/components/Stepper/Step.d.ts +4 -1
  59. package/lib/typescript/src/components/Stepper/StepLabel.d.ts +4 -1
  60. package/lib/typescript/src/components/Stepper/Stepper.d.ts +3 -1
  61. package/lib/typescript/src/components/SuggestiveSearch/SuggestiveSearch.d.ts +123 -0
  62. package/lib/typescript/src/components/index.d.ts +10 -3
  63. package/lib/typescript/src/icons/registry.d.ts +1 -1
  64. package/package.json +1 -1
  65. package/src/components/Accordion/Accordion.tsx +113 -73
  66. package/src/components/ActionFooter/ActionFooter.tsx +56 -4
  67. package/src/components/Attached/Attached.tsx +181 -0
  68. package/src/components/Card/Card.tsx +28 -1
  69. package/src/components/Checkbox/Checkbox.tsx +22 -9
  70. package/src/components/DropdownInput/DropdownInput.tsx +67 -39
  71. package/src/components/ExpandableCheckbox/ExpandableCheckbox.tsx +237 -0
  72. package/src/components/FormField/FormField.tsx +19 -3
  73. package/src/components/FullscreenModal/FullscreenModal.tsx +414 -0
  74. package/src/components/ListItem/ListItem.tsx +55 -25
  75. package/src/components/MessageField/MessageField.tsx +543 -0
  76. package/src/components/NavArrow/NavArrow.tsx +81 -17
  77. package/src/components/PlanComparisonCard/PlanComparisonCard.tsx +426 -0
  78. package/src/components/Slot/Slot.tsx +91 -0
  79. package/src/components/Stepper/Step.tsx +52 -51
  80. package/src/components/Stepper/StepLabel.tsx +46 -9
  81. package/src/components/Stepper/Stepper.tsx +20 -15
  82. package/src/components/SuggestiveSearch/SuggestiveSearch.tsx +756 -0
  83. package/src/components/TextInput/TextInput.tsx +14 -1
  84. package/src/components/Title/Title.tsx +13 -2
  85. package/src/components/index.ts +10 -3
  86. package/src/design-tokens/Coin Variables-variables-full.json +1 -1
  87. package/src/icons/registry.ts +1 -1
@@ -15,9 +15,10 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
15
15
  const IS_IOS = Platform.OS === 'ios';
16
16
  const PRESS_DELAY = IS_IOS ? 130 : 0;
17
17
 
18
- // Forced modes for the endSlot — `Context: 'ListItem'` can never be
19
- // overridden by external modes. Frozen so identity is stable across renders.
20
- const END_SLOT_FORCED_MODES = Object.freeze({
18
+ // Forced modes for the leading/trailing slots — `Context: 'ListItem'` can
19
+ // never be overridden by external modes. Frozen so identity is stable across
20
+ // renders. Applied to both slots so they cascade modes identically.
21
+ const SLOT_FORCED_MODES = Object.freeze({
21
22
  Context: 'ListItem'
22
23
  });
23
24
 
@@ -32,27 +33,42 @@ const pressedOverlayStyle = {
32
33
  // ---------------------------------------------------------------------------
33
34
 
34
35
  function resolveListItemTokens(modes) {
36
+ // Modes used to cascade into slot children (leading / supportSlot / trailing).
37
+ // We do NOT inject an `AppearanceBrand` default here: slot content such as
38
+ // Buttons or Badges carry their own intended appearance, so forcing one onto
39
+ // them would be surprising.
35
40
  const resolvedModes = {
36
41
  ...modes,
37
42
  Context: 'ListItem'
38
43
  };
44
+
45
+ // Modes used to resolve the ListItem's OWN title + support text. Within this
46
+ // component, `AppearanceBrand` only affects `listItem/title/color` and
47
+ // `listItem/supportText/color`, so the text defaults to the "Neutral"
48
+ // appearance (in both Vertical and Horizontal layouts). A caller-supplied
49
+ // `AppearanceBrand` still wins; `Context` is always forced to 'ListItem'.
50
+ const textModes = {
51
+ AppearanceBrand: 'Neutral',
52
+ ...modes,
53
+ Context: 'ListItem'
54
+ };
39
55
  const gap = getVariableByName('listItem/gap', resolvedModes) ?? 8;
40
56
  const paddingTop = getVariableByName('listItem/padding/top', resolvedModes) ?? 0;
41
57
  const paddingBottom = getVariableByName('listItem/padding/bottom', resolvedModes) ?? 0;
42
58
  const paddingLeft = getVariableByName('listItem/padding/left', resolvedModes) ?? 0;
43
59
  const paddingRight = getVariableByName('listItem/padding/right', resolvedModes) ?? 0;
44
60
  const textWrapGap = getVariableByName('listItem/text wrap', resolvedModes) ?? 0;
45
- const titleColor = getVariableByName('listItem/title/color', resolvedModes) || '#0f0d0a';
46
- const titleFontSize = getVariableByName('listItem/title/fontSize', resolvedModes) || 14;
47
- const titleLineHeight = getVariableByName('listItem/title/lineHeight', resolvedModes) || 16;
48
- const titleFontFamily = getVariableByName('listItem/title/fontFamily', resolvedModes) || 'System';
49
- const titleFontWeightRaw = getVariableByName('listItem/title/fontWeight', resolvedModes) || 700;
61
+ const titleColor = getVariableByName('listItem/title/color', textModes) || '#0f0d0a';
62
+ const titleFontSize = getVariableByName('listItem/title/fontSize', textModes) || 14;
63
+ const titleLineHeight = getVariableByName('listItem/title/lineHeight', textModes) || 16;
64
+ const titleFontFamily = getVariableByName('listItem/title/fontFamily', textModes) || 'System';
65
+ const titleFontWeightRaw = getVariableByName('listItem/title/fontWeight', textModes) || 700;
50
66
  const titleFontWeight = typeof titleFontWeightRaw === 'number' ? titleFontWeightRaw.toString() : titleFontWeightRaw;
51
- const supportColor = getVariableByName('listItem/supportText/color', resolvedModes) || '#1f1a14';
52
- const supportFontSize = getVariableByName('listItem/supportText/fontSize', resolvedModes) || 12;
53
- const supportLineHeight = getVariableByName('listItem/supportText/lineHeight', resolvedModes) || 14;
54
- const supportFontFamily = getVariableByName('listItem/supportText/fontFamily', resolvedModes) || 'System';
55
- const supportFontWeightRaw = getVariableByName('listItem/supportText/fontWeight', resolvedModes) || 500;
67
+ const supportColor = getVariableByName('listItem/supportText/color', textModes) || '#1f1a14';
68
+ const supportFontSize = getVariableByName('listItem/supportText/fontSize', textModes) || 12;
69
+ const supportLineHeight = getVariableByName('listItem/supportText/lineHeight', textModes) || 14;
70
+ const supportFontFamily = getVariableByName('listItem/supportText/fontFamily', textModes) || 'System';
71
+ const supportFontWeightRaw = getVariableByName('listItem/supportText/fontWeight', textModes) || 500;
56
72
  const supportFontWeight = typeof supportFontWeightRaw === 'number' ? supportFontWeightRaw.toString() : supportFontWeightRaw;
57
73
  return {
58
74
  baseContainerStyle: {
@@ -116,9 +132,11 @@ const verticalSupportTextOverride = {
116
132
  * - **design-token driven styling** via `getVariableByName` and `modes`
117
133
  *
118
134
  * Wherever the Figma layer name contains "Slot", this component exposes a
119
- * dedicated React "slot" prop:
135
+ * dedicated React "slot" prop. The leading and trailing edges share a
136
+ * symmetric `leading` / `trailing` slot API:
137
+ * - Slot "leading" → `leading`
120
138
  * - Slot "support text" → `supportSlot`
121
- * - Slot "end" → `endSlot`
139
+ * - Slot "trailing" → `trailing`
122
140
  *
123
141
  * @component
124
142
  * @param {Object} props
@@ -126,9 +144,9 @@ const verticalSupportTextOverride = {
126
144
  * @param {string} [props.title='Title'] - Primary title used in the horizontal layout.
127
145
  * @param {string} [props.supportText='Support Text'] - Support text used in both layouts when `supportSlot` is not provided.
128
146
  * @param {boolean} [props.showSupportText=true] - Toggles rendering of the support text in Horizontal layout.
129
- * @param {React.ReactNode} [props.leading] - Optional leading element. Defaults to `IconCapsule`.
147
+ * @param {React.ReactNode} [props.leading] - Optional leading slot. Defaults to `IconCapsule`.
130
148
  * @param {React.ReactNode} [props.supportSlot] - Optional custom slot used instead of the default support text block.
131
- * @param {React.ReactNode} [props.endSlot] - Optional custom trailing slot (Figma Slot "end").
149
+ * @param {React.ReactNode} [props.trailing] - Optional trailing slot (Figma Slot "trailing"). Horizontal layout only.
132
150
  * @param {boolean} [props.navArrow=true] - Whether to show NavArrow on the far right (Horizontal layout only).
133
151
  * @param {Object} [props.modes={}] - Modes object passed to `getVariableByName` for all design tokens.
134
152
  * @param {Function} [props.onPress] - When provided, the entire item becomes pressable (navigation variant).
@@ -157,6 +175,7 @@ function ListItemImpl({
157
175
  showSupportText = true,
158
176
  leading,
159
177
  supportSlot,
178
+ trailing,
160
179
  endSlot,
161
180
  navArrow = true,
162
181
  modes = EMPTY_MODES,
@@ -194,7 +213,7 @@ function ListItemImpl({
194
213
  // Process leading slot to pass modes to children. Memoized on
195
214
  // (leading, resolvedModes) so a parent re-render doesn't re-walk the tree.
196
215
  const leadingElement = useMemo(() => {
197
- const processed = leading ? cloneChildrenWithModes(React.Children.toArray(leading), tokens.resolvedModes) : [];
216
+ const processed = leading ? cloneChildrenWithModes(React.Children.toArray(leading), tokens.resolvedModes, SLOT_FORCED_MODES) : [];
198
217
  if (processed.length === 0) {
199
218
  return /*#__PURE__*/_jsx(IconCapsule, {
200
219
  modes: tokens.resolvedModes,
@@ -208,11 +227,14 @@ function ListItemImpl({
208
227
  const processed = cloneChildrenWithModes(React.Children.toArray(supportSlot), tokens.resolvedModes);
209
228
  return processed.length === 1 ? processed[0] : processed;
210
229
  }, [supportSlot, tokens.resolvedModes]);
211
- const processedEndSlot = useMemo(() => {
212
- if (!endSlot) return null;
213
- const processed = cloneChildrenWithModes(React.Children.toArray(endSlot), tokens.resolvedModes, END_SLOT_FORCED_MODES);
230
+
231
+ // `trailing` wins; `endSlot` is the deprecated alias kept for back-compat.
232
+ const trailingContent = trailing ?? endSlot;
233
+ const processedTrailing = useMemo(() => {
234
+ if (!trailingContent) return null;
235
+ const processed = cloneChildrenWithModes(React.Children.toArray(trailingContent), tokens.resolvedModes, SLOT_FORCED_MODES);
214
236
  return processed.length === 1 ? processed[0] : processed;
215
- }, [endSlot, tokens.resolvedModes]);
237
+ }, [trailingContent, tokens.resolvedModes]);
216
238
  const renderSupportContent = () => {
217
239
  if (processedSupportSlot) return processedSupportSlot;
218
240
 
@@ -258,9 +280,9 @@ function ListItemImpl({
258
280
  numberOfLines: 1,
259
281
  children: title
260
282
  }), showSupportText && renderSupportContent()]
261
- }), processedEndSlot ? /*#__PURE__*/_jsx(View, {
283
+ }), processedTrailing ? /*#__PURE__*/_jsx(View, {
262
284
  style: tokens.trailingWrapperStyle,
263
- children: processedEndSlot
285
+ children: processedTrailing
264
286
  }) : null, navArrow && /*#__PURE__*/_jsx(NavArrow, {
265
287
  direction: "Forward",
266
288
  modes: tokens.resolvedModes
@@ -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);