jfs-components 0.0.73 → 0.0.77

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 (134) hide show
  1. package/CHANGELOG.md +115 -6
  2. package/lib/commonjs/components/AccountCard/AccountCard.js +247 -0
  3. package/lib/commonjs/components/ActionFooter/ActionFooter.js +147 -82
  4. package/lib/commonjs/components/AppBar/AppBar.js +17 -11
  5. package/lib/commonjs/components/Avatar/Avatar.js +20 -0
  6. package/lib/commonjs/components/Badge/Badge.js +23 -0
  7. package/lib/commonjs/components/Button/Button.js +37 -0
  8. package/lib/commonjs/components/CardBankAccount/CardBankAccount.js +18 -2
  9. package/lib/commonjs/components/CheckboxItem/CheckboxItem.js +40 -25
  10. package/lib/commonjs/components/Dropdown/Dropdown.js +214 -0
  11. package/lib/commonjs/components/DropdownInput/DropdownInput.js +542 -0
  12. package/lib/commonjs/components/FormField/FormField.js +328 -178
  13. package/lib/commonjs/components/IconButton/IconButton.js +20 -0
  14. package/lib/commonjs/components/Image/Image.js +26 -1
  15. package/lib/commonjs/components/LottieIntroBlock/LottieIntroBlock.js +150 -0
  16. package/lib/commonjs/components/LottiePlayer/LottiePlayer.js +116 -0
  17. package/lib/commonjs/components/LottiePlayer/LottiePlayer.web.js +82 -0
  18. package/lib/commonjs/components/LottiePlayer/loadNativeLottieView.js +74 -0
  19. package/lib/commonjs/components/LottiePlayer/loadWebLottieView.js +50 -0
  20. package/lib/commonjs/components/PageHero/PageHero.js +189 -0
  21. package/lib/commonjs/components/PoweredByLabel/PoweredByLabel.js +135 -0
  22. package/lib/commonjs/components/PoweredByLabel/finvu.png +0 -0
  23. package/lib/commonjs/components/RechargeCard/RechargeCard.js +32 -17
  24. package/lib/commonjs/components/Text/Text.js +40 -3
  25. package/lib/commonjs/components/Tooltip/Tooltip.js +34 -27
  26. package/lib/commonjs/components/index.js +67 -0
  27. package/lib/commonjs/design-tokens/Coin Variables-variables-full.json +1 -1
  28. package/lib/commonjs/icons/Icon.js +16 -0
  29. package/lib/commonjs/icons/registry.js +1 -1
  30. package/lib/commonjs/index.js +12 -0
  31. package/lib/commonjs/skeleton/Skeleton.js +234 -0
  32. package/lib/commonjs/skeleton/SkeletonGroup.js +140 -0
  33. package/lib/commonjs/skeleton/index.js +58 -0
  34. package/lib/commonjs/skeleton/shimmer-tokens.js +189 -0
  35. package/lib/commonjs/skeleton/useReducedMotion.js +64 -0
  36. package/lib/module/components/AccountCard/AccountCard.js +241 -0
  37. package/lib/module/components/ActionFooter/ActionFooter.js +146 -82
  38. package/lib/module/components/AppBar/AppBar.js +17 -11
  39. package/lib/module/components/Avatar/Avatar.js +19 -0
  40. package/lib/module/components/Badge/Badge.js +23 -0
  41. package/lib/module/components/Button/Button.js +37 -0
  42. package/lib/module/components/CardBankAccount/CardBankAccount.js +17 -2
  43. package/lib/module/components/CheckboxItem/CheckboxItem.js +41 -26
  44. package/lib/module/components/Dropdown/Dropdown.js +206 -0
  45. package/lib/module/components/DropdownInput/DropdownInput.js +536 -0
  46. package/lib/module/components/FormField/FormField.js +330 -180
  47. package/lib/module/components/IconButton/IconButton.js +20 -0
  48. package/lib/module/components/Image/Image.js +25 -1
  49. package/lib/module/components/LottieIntroBlock/LottieIntroBlock.js +144 -0
  50. package/lib/module/components/LottiePlayer/LottiePlayer.js +111 -0
  51. package/lib/module/components/LottiePlayer/LottiePlayer.web.js +77 -0
  52. package/lib/module/components/LottiePlayer/loadNativeLottieView.js +69 -0
  53. package/lib/module/components/LottiePlayer/loadWebLottieView.js +45 -0
  54. package/lib/module/components/PageHero/PageHero.js +183 -0
  55. package/lib/module/components/PoweredByLabel/PoweredByLabel.js +130 -0
  56. package/lib/module/components/PoweredByLabel/finvu.png +0 -0
  57. package/lib/module/components/RechargeCard/RechargeCard.js +33 -17
  58. package/lib/module/components/Text/Text.js +40 -3
  59. package/lib/module/components/Tooltip/Tooltip.js +34 -27
  60. package/lib/module/components/index.js +8 -1
  61. package/lib/module/design-tokens/Coin Variables-variables-full.json +1 -1
  62. package/lib/module/icons/Icon.js +16 -0
  63. package/lib/module/icons/registry.js +1 -1
  64. package/lib/module/index.js +2 -1
  65. package/lib/module/skeleton/Skeleton.js +229 -0
  66. package/lib/module/skeleton/SkeletonGroup.js +133 -0
  67. package/lib/module/skeleton/index.js +6 -0
  68. package/lib/module/skeleton/shimmer-tokens.js +181 -0
  69. package/lib/module/skeleton/useReducedMotion.js +61 -0
  70. package/lib/typescript/src/components/AccountCard/AccountCard.d.ts +81 -0
  71. package/lib/typescript/src/components/ActionFooter/ActionFooter.d.ts +26 -21
  72. package/lib/typescript/src/components/Avatar/Avatar.d.ts +7 -1
  73. package/lib/typescript/src/components/Badge/Badge.d.ts +7 -1
  74. package/lib/typescript/src/components/Button/Button.d.ts +8 -1
  75. package/lib/typescript/src/components/CardBankAccount/CardBankAccount.d.ts +9 -2
  76. package/lib/typescript/src/components/CheckboxItem/CheckboxItem.d.ts +18 -2
  77. package/lib/typescript/src/components/Dropdown/Dropdown.d.ts +62 -0
  78. package/lib/typescript/src/components/DropdownInput/DropdownInput.d.ts +107 -0
  79. package/lib/typescript/src/components/FormField/FormField.d.ts +76 -19
  80. package/lib/typescript/src/components/IconButton/IconButton.d.ts +7 -1
  81. package/lib/typescript/src/components/Image/Image.d.ts +8 -1
  82. package/lib/typescript/src/components/LottieIntroBlock/LottieIntroBlock.d.ts +58 -0
  83. package/lib/typescript/src/components/LottiePlayer/LottiePlayer.d.ts +85 -0
  84. package/lib/typescript/src/components/LottiePlayer/LottiePlayer.web.d.ts +28 -0
  85. package/lib/typescript/src/components/LottiePlayer/loadNativeLottieView.d.ts +11 -0
  86. package/lib/typescript/src/components/LottiePlayer/loadWebLottieView.d.ts +11 -0
  87. package/lib/typescript/src/components/PageHero/PageHero.d.ts +79 -0
  88. package/lib/typescript/src/components/PoweredByLabel/PoweredByLabel.d.ts +70 -0
  89. package/lib/typescript/src/components/Text/Text.d.ts +31 -2
  90. package/lib/typescript/src/components/Tooltip/Tooltip.d.ts +13 -2
  91. package/lib/typescript/src/components/index.d.ts +8 -1
  92. package/lib/typescript/src/icons/Icon.d.ts +7 -1
  93. package/lib/typescript/src/icons/registry.d.ts +1 -1
  94. package/lib/typescript/src/index.d.ts +1 -0
  95. package/lib/typescript/src/skeleton/Skeleton.d.ts +60 -0
  96. package/lib/typescript/src/skeleton/SkeletonGroup.d.ts +78 -0
  97. package/lib/typescript/src/skeleton/index.d.ts +5 -0
  98. package/lib/typescript/src/skeleton/shimmer-tokens.d.ts +160 -0
  99. package/lib/typescript/src/skeleton/useReducedMotion.d.ts +15 -0
  100. package/package.json +11 -3
  101. package/src/components/AccountCard/AccountCard.tsx +376 -0
  102. package/src/components/ActionFooter/ActionFooter.tsx +152 -86
  103. package/src/components/AppBar/AppBar.tsx +25 -14
  104. package/src/components/Avatar/Avatar.tsx +26 -0
  105. package/src/components/Badge/Badge.tsx +27 -0
  106. package/src/components/Button/Button.tsx +40 -0
  107. package/src/components/CardBankAccount/CardBankAccount.tsx +29 -3
  108. package/src/components/CheckboxItem/CheckboxItem.tsx +65 -30
  109. package/src/components/Dropdown/Dropdown.tsx +331 -0
  110. package/src/components/DropdownInput/DropdownInput.tsx +819 -0
  111. package/src/components/FormField/FormField.tsx +542 -215
  112. package/src/components/IconButton/IconButton.tsx +27 -0
  113. package/src/components/Image/Image.tsx +25 -0
  114. package/src/components/LottieIntroBlock/LottieIntroBlock.tsx +202 -0
  115. package/src/components/LottiePlayer/LottiePlayer.tsx +145 -0
  116. package/src/components/LottiePlayer/LottiePlayer.web.tsx +94 -0
  117. package/src/components/LottiePlayer/loadNativeLottieView.tsx +87 -0
  118. package/src/components/LottiePlayer/loadWebLottieView.tsx +64 -0
  119. package/src/components/PageHero/PageHero.tsx +257 -0
  120. package/src/components/PoweredByLabel/PoweredByLabel.tsx +221 -0
  121. package/src/components/PoweredByLabel/finvu.png +0 -0
  122. package/src/components/RechargeCard/RechargeCard.tsx +32 -24
  123. package/src/components/Text/Text.tsx +78 -3
  124. package/src/components/Tooltip/Tooltip.tsx +50 -25
  125. package/src/components/index.ts +16 -1
  126. package/src/design-tokens/Coin Variables-variables-full.json +1 -1
  127. package/src/icons/Icon.tsx +17 -0
  128. package/src/icons/registry.ts +1 -1
  129. package/src/index.ts +1 -0
  130. package/src/skeleton/Skeleton.tsx +298 -0
  131. package/src/skeleton/SkeletonGroup.tsx +193 -0
  132. package/src/skeleton/index.ts +10 -0
  133. package/src/skeleton/shimmer-tokens.ts +221 -0
  134. package/src/skeleton/useReducedMotion.ts +72 -0
@@ -1,223 +1,373 @@
1
1
  "use strict";
2
2
 
3
- import React, { useState, useMemo, useCallback } from 'react';
4
- import { View, Text } from 'react-native';
3
+ import React, { useCallback, useMemo, useState } from 'react';
4
+ import { View, Text, TextInput as RNTextInput } from 'react-native';
5
5
  import { getVariableByName } from '../../design-tokens/figma-variables-resolver';
6
- import { EMPTY_MODES } from '../../utils/react-utils';
7
6
  import { useTokens } from '../../design-tokens/JFSThemeProvider';
8
- import TextInput from '../TextInput/TextInput';
7
+ import { EMPTY_MODES, cloneChildrenWithModes } from '../../utils/react-utils';
9
8
  import SupportText from '../SupportText/SupportText';
9
+ import Icon from '../../icons/Icon';
10
+ import { useFormContext } from '../Form/Form';
10
11
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
11
- function useFormField(props) {
12
- const {
13
- type = 'text',
14
- isDisabled = false,
15
- isInvalid = false,
16
- supportText,
17
- errorMessage,
18
- modes: propModes = EMPTY_MODES,
19
- onFocus,
20
- onBlur
21
- } = props;
22
- const {
23
- modes: globalModes
24
- } = useTokens();
25
- const baseModes = useMemo(() => ({
26
- ...globalModes,
27
- ...propModes
28
- }), [globalModes, propModes]);
29
- const [isFocused, setIsFocused] = useState(false);
12
+ // ---------------------------------------------------------------------------
13
+ // Token resolution
14
+ // ---------------------------------------------------------------------------
30
15
 
31
- // Merge FormField States collection based on focus
32
- const modes = useMemo(() => ({
33
- ...baseModes,
34
- 'FormField States': isFocused ? 'Active' : 'Idle'
35
- }), [baseModes, isFocused]);
36
-
37
- // -- Label tokens (from "FormField / Output" collection) --
38
- const labelColor = getVariableByName('formField/label/color', modes) || '#0c0d10';
39
- const labelFontFamily = getVariableByName('formField/label/fontFamily', modes) || 'JioType Var';
40
- const labelFontSize = parseInt(getVariableByName('formField/label/fontSize', modes), 10) || 14;
41
- const labelLineHeight = parseInt(getVariableByName('formField/label/lineHeight', modes), 10) || 17;
42
- const labelFontWeight = getVariableByName('formField/label/fontWeight', modes) || '500';
43
- const gap = parseInt(getVariableByName('formField/gap', modes), 10) || 8;
44
-
45
- // -- Input tokens (from "FormField / Output" + "FormField States" collections) --
46
- const inputPaddingH = parseInt(getVariableByName('formField/input/padding/horizontal', modes), 10) || 12;
47
- const inputGap = parseInt(getVariableByName('formField/input/gap', modes), 10) || 8;
48
- const inputRadius = parseInt(getVariableByName('formField/input/radius', modes), 10) || 8;
49
- const inputBackground = getVariableByName('formField/input/background', modes) || '#ffffff';
50
- const inputFontSize = parseInt(getVariableByName('formField/input/label/fontSize', modes), 10) || 16;
51
- const inputLineHeight = parseInt(getVariableByName('formField/input/label/lineHeight', modes), 10) || 45;
52
- const inputFontFamily = getVariableByName('formField/input/label/fontFamily', modes) || 'JioType Var';
53
- const inputFontWeight = getVariableByName('formField/input/label/fontWeight', modes) || '400';
54
- const inputTextColor = getVariableByName('states/formField/input/label/color', modes) || getVariableByName('formField/input/label/color', modes) || '#24262b';
55
- const inputBorderColor = getVariableByName('states/formField/input/border/color', modes) || getVariableByName('formField/input/border/color', modes) || '#b5b6b7';
56
- const inputBorderSize = parseInt(getVariableByName('formField/input/border/size', modes), 10) || 1;
57
-
58
- // -- Styles --
59
- const labelStyle = useMemo(() => ({
60
- color: labelColor,
61
- fontFamily: labelFontFamily,
62
- fontSize: labelFontSize,
63
- lineHeight: labelLineHeight,
64
- fontWeight: labelFontWeight
65
- }), [labelColor, labelFontFamily, labelFontSize, labelLineHeight, labelFontWeight]);
66
- const wrapperStyle = useMemo(() => ({
67
- gap,
68
- opacity: isDisabled ? 0.5 : 1
69
- }), [gap, isDisabled]);
70
- const requiredIndicatorStyle = useMemo(() => ({
71
- color: '#d93d3d',
72
- fontFamily: labelFontFamily,
73
- fontSize: labelFontSize,
74
- lineHeight: labelLineHeight,
75
- fontWeight: labelFontWeight
76
- }), [labelFontFamily, labelFontSize, labelLineHeight, labelFontWeight]);
77
-
78
- // Style overrides for the input row, sourced from formField/input/* tokens
79
- const inputContainerStyle = useMemo(() => ({
80
- backgroundColor: inputBackground,
81
- borderColor: inputBorderColor,
82
- borderWidth: inputBorderSize,
83
- borderRadius: inputRadius,
84
- paddingHorizontal: inputPaddingH,
85
- paddingVertical: 0,
86
- gap: inputGap
87
- }), [inputBackground, inputBorderColor, inputBorderSize, inputRadius, inputPaddingH, inputGap]);
88
- const inputTextStyle = useMemo(() => ({
89
- color: inputTextColor,
90
- fontSize: inputFontSize,
91
- lineHeight: inputLineHeight,
92
- fontFamily: inputFontFamily,
93
- fontWeight: inputFontWeight
94
- }), [inputTextColor, inputFontSize, inputLineHeight, inputFontFamily, inputFontWeight]);
95
-
96
- // -- Support text logic --
97
- const supportStatus = isInvalid ? 'Error' : 'Neutral';
98
- const supportLabel = isInvalid && errorMessage ? errorMessage : supportText;
99
-
100
- // -- Input type derived props --
101
- const secureTextEntry = type === 'password';
102
- const keyboardType = type === 'email' ? 'email-address' : 'default';
103
- const autoCapitalize = type === 'email' || type === 'password' ? 'none' : 'sentences';
104
-
105
- // -- Event handlers --
106
- const handleFocus = useCallback(e => {
107
- setIsFocused(true);
108
- onFocus?.(e);
109
- }, [onFocus]);
110
- const handleBlur = useCallback(e => {
111
- setIsFocused(false);
112
- onBlur?.(e);
113
- }, [onBlur]);
114
- return {
115
- modes,
116
- labelStyle,
117
- wrapperStyle,
118
- requiredIndicatorStyle,
119
- inputContainerStyle,
120
- inputTextStyle,
121
- supportStatus,
122
- supportLabel,
123
- secureTextEntry,
124
- keyboardType,
125
- autoCapitalize,
126
- handleFocus,
127
- handleBlur
128
- };
16
+ function toNumber(value, fallback) {
17
+ if (typeof value === 'number' && Number.isFinite(value)) return value;
18
+ if (typeof value === 'string') {
19
+ const parsed = parseFloat(value);
20
+ if (Number.isFinite(parsed)) return parsed;
21
+ }
22
+ return fallback;
23
+ }
24
+ function toFontWeight(value, fallback) {
25
+ if (typeof value === 'number') return value.toString();
26
+ if (typeof value === 'string' && value.length > 0) return value;
27
+ return fallback;
129
28
  }
29
+ function useFormFieldTokens(modes) {
30
+ return useMemo(() => {
31
+ // Wrapper
32
+ const gap = toNumber(getVariableByName('formField/gap', modes), 8);
33
+
34
+ // Label (Figma: 14/17 medium, color #0c0d10)
35
+ const labelColor = getVariableByName('formField/label/color', modes) || '#0c0d10';
36
+ const labelFontFamily = getVariableByName('formField/label/fontFamily', modes) || 'JioType Var';
37
+ const labelFontSize = toNumber(getVariableByName('formField/label/fontSize', modes), 14);
38
+ const labelLineHeight = toNumber(getVariableByName('formField/label/lineHeight', modes), 17);
39
+ const labelFontWeight = toFontWeight(getVariableByName('formField/label/fontWeight', modes), '500');
40
+
41
+ // Input row (Figma: 12 px padding-h, 8 px gap, 8 px radius, 1 px border)
42
+ const inputPaddingH = toNumber(getVariableByName('formField/input/padding/horizontal', modes), 12);
43
+ const inputGap = toNumber(getVariableByName('formField/input/gap', modes), 8);
44
+ const inputRadius = toNumber(getVariableByName('formField/input/radius', modes), 8);
45
+ const inputBorderSize = toNumber(getVariableByName('formField/input/border/size', modes), 1);
46
+
47
+ // Input text (Figma: 16/45 regular)
48
+ const inputFontSize = toNumber(getVariableByName('formField/input/label/fontSize', modes), 16);
49
+ const inputLineHeight = toNumber(getVariableByName('formField/input/label/lineHeight', modes), 45);
50
+ const inputFontFamily = getVariableByName('formField/input/label/fontFamily', modes) || 'JioType Var';
51
+ const inputFontWeight = toFontWeight(getVariableByName('formField/input/label/fontWeight', modes), '400');
52
+ const inputBackground = getVariableByName('formField/input/background', modes) || '#ffffff';
53
+ const inputBorderColor = getVariableByName('formField/input/border/color', modes) || '#b5b6b7';
54
+ if (__DEV__) {
55
+ console.warn('[FormField] border color (modes changed)', {
56
+ 'FormField States': modes['FormField States'],
57
+ inputBorderColor,
58
+ 'formField/input/border/color': getVariableByName('formField/input/border/color', modes)
59
+ });
60
+ }
61
+ const inputTextColor = getVariableByName('formField/input/label/color', modes) || '#24262b';
62
+ return {
63
+ gap,
64
+ labelColor,
65
+ labelFontFamily,
66
+ labelFontSize,
67
+ labelLineHeight,
68
+ labelFontWeight,
69
+ inputPaddingH,
70
+ inputGap,
71
+ inputRadius,
72
+ inputBorderSize,
73
+ inputFontSize,
74
+ inputLineHeight,
75
+ inputFontFamily,
76
+ inputFontWeight,
77
+ inputBackground,
78
+ inputBorderColor,
79
+ inputTextColor
80
+ };
81
+ }, [modes]);
82
+ }
83
+
84
+ // ---------------------------------------------------------------------------
85
+ // Helpers
86
+ // ---------------------------------------------------------------------------
87
+
88
+ function deriveTypeProps(type) {
89
+ switch (type) {
90
+ case 'password':
91
+ return {
92
+ secureTextEntry: true,
93
+ keyboardType: 'default',
94
+ autoCapitalize: 'none',
95
+ autoComplete: 'password',
96
+ textContentType: 'password'
97
+ };
98
+ case 'email':
99
+ return {
100
+ secureTextEntry: false,
101
+ keyboardType: 'email-address',
102
+ autoCapitalize: 'none',
103
+ autoComplete: 'email',
104
+ textContentType: 'emailAddress'
105
+ };
106
+ case 'number':
107
+ return {
108
+ secureTextEntry: false,
109
+ keyboardType: 'numeric',
110
+ autoCapitalize: 'none',
111
+ autoComplete: 'off',
112
+ textContentType: 'none'
113
+ };
114
+ case 'phone':
115
+ return {
116
+ secureTextEntry: false,
117
+ keyboardType: 'phone-pad',
118
+ autoCapitalize: 'none',
119
+ autoComplete: 'tel',
120
+ textContentType: 'telephoneNumber'
121
+ };
122
+ case 'url':
123
+ return {
124
+ secureTextEntry: false,
125
+ keyboardType: 'url',
126
+ autoCapitalize: 'none',
127
+ autoComplete: 'url',
128
+ textContentType: 'URL'
129
+ };
130
+ case 'search':
131
+ return {
132
+ secureTextEntry: false,
133
+ keyboardType: 'default',
134
+ autoCapitalize: 'none',
135
+ autoComplete: 'off',
136
+ textContentType: 'none'
137
+ };
138
+ case 'text':
139
+ default:
140
+ return {
141
+ secureTextEntry: false,
142
+ keyboardType: 'default',
143
+ autoCapitalize: 'sentences',
144
+ autoComplete: 'off',
145
+ textContentType: 'none'
146
+ };
147
+ }
148
+ }
149
+ function firstError(error) {
150
+ if (!error) return undefined;
151
+ if (Array.isArray(error)) return error[0];
152
+ return error;
153
+ }
154
+
155
+ // ---------------------------------------------------------------------------
156
+ // Component
157
+ // ---------------------------------------------------------------------------
158
+
130
159
  function FormField({
131
160
  label,
132
161
  placeholder,
133
- value = '',
162
+ value,
134
163
  onChangeText,
135
164
  type = 'text',
165
+ name,
136
166
  leading,
137
167
  trailing,
138
168
  leadingIconName,
139
169
  isRequired = false,
140
170
  isDisabled = false,
141
171
  isInvalid = false,
172
+ isReadOnly = false,
142
173
  supportText,
143
174
  errorMessage,
144
- modes = EMPTY_MODES,
175
+ maxLength,
176
+ autoFocus = false,
177
+ modes: propModes = EMPTY_MODES,
145
178
  style,
179
+ inputStyle,
180
+ inputTextStyle,
146
181
  onFocus,
147
182
  onBlur,
183
+ onSubmitEditing,
148
184
  accessibilityLabel,
149
- accessibilityHint
185
+ accessibilityHint,
186
+ testID
150
187
  }) {
188
+ // -- Form context integration -------------------------------------------
189
+ const formCtx = useFormContext();
190
+ const formError = name && formCtx ? firstError(formCtx.validationErrors[name]) : undefined;
191
+ const resolvedIsInvalid = isInvalid || Boolean(formError);
192
+ const resolvedErrorMessage = errorMessage ?? formError;
193
+
194
+ // -- Mode resolution ----------------------------------------------------
151
195
  const {
152
- modes: resolvedModes,
153
- labelStyle,
154
- wrapperStyle,
155
- requiredIndicatorStyle,
156
- inputContainerStyle,
157
- inputTextStyle,
158
- supportStatus,
159
- supportLabel,
160
- secureTextEntry,
161
- keyboardType,
162
- autoCapitalize,
163
- handleFocus,
164
- handleBlur
165
- } = useFormField({
166
- type,
167
- isDisabled,
168
- isInvalid,
169
- supportText,
170
- errorMessage,
171
- modes,
172
- onFocus,
173
- onBlur
174
- });
196
+ modes: globalModes
197
+ } = useTokens();
198
+ const baseModes = useMemo(() => ({
199
+ ...globalModes,
200
+ ...propModes
201
+ }), [globalModes, propModes]);
202
+ const [isFocused, setIsFocused] = useState(false);
203
+ const interactive = !isDisabled && !isReadOnly;
204
+
205
+ // FormField States cascade — error > read only/disabled > active (focused) > idle.
206
+ // Disabled maps to "Read Only" since there is no dedicated disabled mode and
207
+ // the visual treatment is closest. This is only the DEFAULT — an explicit
208
+ // `modes['FormField States']` passed in via props or the global theme
209
+ // always wins so consumers can force a state (e.g. for documentation).
210
+ const derivedStateMode = useMemo(() => {
211
+ if (resolvedIsInvalid) return 'Error';
212
+ if (isReadOnly || isDisabled) return 'Read Only';
213
+ if (isFocused) return 'Active';
214
+ return 'Idle';
215
+ }, [resolvedIsInvalid, isReadOnly, isDisabled, isFocused]);
216
+ const modes = useMemo(() => {
217
+ const explicitStateMode = baseModes['FormField States'];
218
+ const stateMode = explicitStateMode ?? derivedStateMode;
219
+ const explicitStatus = baseModes.Status;
220
+ // Default SupportText token mode is Auto (Figma resolves foreground from
221
+ // context). Pass modes={{ Status: 'Error' }} etc. to override.
222
+ const status = explicitStatus ?? 'Auto';
223
+ return {
224
+ ...baseModes,
225
+ 'FormField States': stateMode,
226
+ Status: status
227
+ };
228
+ }, [baseModes, derivedStateMode]);
229
+ const tokens = useFormFieldTokens(modes);
230
+
231
+ // -- Type-derived input props ------------------------------------------
232
+ const typeProps = useMemo(() => deriveTypeProps(type), [type]);
233
+
234
+ // -- Event handlers ----------------------------------------------------
235
+ const handleFocus = useCallback(e => {
236
+ setIsFocused(true);
237
+ onFocus?.(e);
238
+ }, [onFocus]);
239
+ const handleBlur = useCallback(e => {
240
+ setIsFocused(false);
241
+ onBlur?.(e);
242
+ }, [onBlur]);
243
+ const handleChangeText = useCallback(next => {
244
+ onChangeText?.(next);
245
+ if (name && formCtx) formCtx.onFieldChange(name);
246
+ }, [onChangeText, name, formCtx]);
247
+
248
+ // -- Styles ------------------------------------------------------------
249
+ const wrapperStyle = useMemo(() => ({
250
+ gap: tokens.gap,
251
+ opacity: isDisabled ? 0.5 : 1
252
+ }), [tokens.gap, isDisabled]);
253
+ const labelRowStyle = useMemo(() => ({
254
+ flexDirection: 'row',
255
+ alignItems: 'baseline'
256
+ }), []);
257
+ const labelTextStyle = useMemo(() => ({
258
+ color: tokens.labelColor,
259
+ fontFamily: tokens.labelFontFamily,
260
+ fontSize: tokens.labelFontSize,
261
+ lineHeight: tokens.labelLineHeight,
262
+ fontWeight: tokens.labelFontWeight
263
+ }), [tokens.labelColor, tokens.labelFontFamily, tokens.labelFontSize, tokens.labelLineHeight, tokens.labelFontWeight]);
264
+ const requiredIndicatorStyle = useMemo(() => ({
265
+ ...labelTextStyle,
266
+ color: '#d93d3d'
267
+ }), [labelTextStyle]);
268
+ const inputRowStyle = useMemo(() => ({
269
+ flexDirection: 'row',
270
+ alignItems: 'center',
271
+ backgroundColor: tokens.inputBackground,
272
+ borderColor: tokens.inputBorderColor,
273
+ borderWidth: tokens.inputBorderSize,
274
+ borderStyle: 'solid',
275
+ borderRadius: tokens.inputRadius,
276
+ paddingHorizontal: tokens.inputPaddingH,
277
+ paddingVertical: 0,
278
+ gap: tokens.inputGap,
279
+ minHeight: tokens.inputLineHeight,
280
+ width: '100%'
281
+ }), [tokens.inputBackground, tokens.inputBorderColor, tokens.inputBorderSize, tokens.inputRadius, tokens.inputPaddingH, tokens.inputGap, tokens.inputLineHeight]);
282
+ const inputTextStyles = useMemo(() => ({
283
+ flex: 1,
284
+ color: tokens.inputTextColor,
285
+ fontFamily: tokens.inputFontFamily,
286
+ fontSize: tokens.inputFontSize,
287
+ lineHeight: tokens.inputLineHeight,
288
+ fontWeight: tokens.inputFontWeight,
289
+ padding: 0,
290
+ margin: 0,
291
+ // Remove the default web focus ring; the input row's border acts as the
292
+ // focus indicator via the FormField States cascade.
293
+ outlineStyle: 'none',
294
+ outlineWidth: 0,
295
+ outlineColor: 'transparent'
296
+ }), [tokens.inputTextColor, tokens.inputFontFamily, tokens.inputFontSize, tokens.inputLineHeight, tokens.inputFontWeight]);
297
+ const placeholderColor = useMemo(() => {
298
+ // Slightly muted version of the resolved text color, mirroring the
299
+ // sibling TextInput behavior.
300
+ const c = tokens.inputTextColor;
301
+ if (typeof c !== 'string') return undefined;
302
+ if (c.startsWith('rgb(')) {
303
+ return c.replace('rgb(', 'rgba(').replace(')', ', 0.55)');
304
+ }
305
+ return '#888a8d';
306
+ }, [tokens.inputTextColor]);
307
+
308
+ // -- Slots --------------------------------------------------------------
309
+ const leadingElement = leading ?? (leadingIconName ? /*#__PURE__*/_jsx(Icon, {
310
+ name: leadingIconName,
311
+ size: 20,
312
+ color: tokens.inputTextColor
313
+ }) : null);
314
+ const processedLeading = leadingElement ? cloneChildrenWithModes(leadingElement, modes) : null;
315
+ const processedTrailing = trailing ? cloneChildrenWithModes(trailing, modes) : null;
316
+
317
+ // -- Support text -------------------------------------------------------
318
+ const supportStatus = resolvedIsInvalid ? 'Error' : 'Neutral';
319
+ const supportLabel = resolvedIsInvalid && resolvedErrorMessage ? resolvedErrorMessage : supportText;
320
+
321
+ // -- Accessibility ------------------------------------------------------
175
322
  const resolvedA11yLabel = accessibilityLabel || label || placeholder || 'Form field';
176
323
  return /*#__PURE__*/_jsxs(View, {
177
324
  style: [wrapperStyle, style],
178
325
  pointerEvents: isDisabled ? 'none' : 'auto',
179
- accessible: true,
180
- accessibilityRole: "none",
181
- accessibilityLabel: resolvedA11yLabel,
182
- accessibilityState: {
183
- disabled: isDisabled
184
- },
326
+ testID: testID,
327
+ accessible: false,
185
328
  children: [label != null && /*#__PURE__*/_jsxs(View, {
186
- style: {
187
- flexDirection: 'row',
188
- alignItems: 'baseline'
189
- },
329
+ style: labelRowStyle,
190
330
  children: [/*#__PURE__*/_jsx(Text, {
191
- style: labelStyle,
331
+ style: labelTextStyle,
192
332
  children: label
193
333
  }), isRequired && /*#__PURE__*/_jsx(Text, {
194
334
  style: requiredIndicatorStyle,
195
335
  children: " *"
196
336
  })]
197
- }), /*#__PURE__*/_jsx(TextInput, {
198
- placeholder: placeholder || '',
199
- value: value,
200
- ...(onChangeText ? {
201
- onChangeText
202
- } : {}),
203
- leading: leading,
204
- trailing: trailing,
205
- leadingIconName: leadingIconName || 'ic_search',
206
- modes: resolvedModes,
207
- style: inputContainerStyle,
208
- inputStyle: inputTextStyle,
209
- onFocus: handleFocus,
210
- onBlur: handleBlur,
211
- secureTextEntry: secureTextEntry,
212
- keyboardType: keyboardType,
213
- autoCapitalize: autoCapitalize,
214
- editable: !isDisabled,
215
- accessibilityLabel: resolvedA11yLabel,
216
- accessibilityHint: accessibilityHint || ''
217
- }), supportLabel != null && /*#__PURE__*/_jsx(SupportText, {
337
+ }), /*#__PURE__*/_jsxs(View, {
338
+ style: [inputRowStyle, inputStyle],
339
+ children: [processedLeading != null && /*#__PURE__*/_jsx(View, {
340
+ accessibilityElementsHidden: true,
341
+ importantForAccessibility: "no",
342
+ children: processedLeading
343
+ }), /*#__PURE__*/_jsx(RNTextInput, {
344
+ style: [inputTextStyles, inputTextStyle],
345
+ value: value ?? '',
346
+ onChangeText: handleChangeText,
347
+ onFocus: handleFocus,
348
+ onBlur: handleBlur,
349
+ onSubmitEditing: onSubmitEditing,
350
+ placeholder: placeholder ?? '',
351
+ placeholderTextColor: placeholderColor,
352
+ editable: interactive,
353
+ maxLength: maxLength,
354
+ autoFocus: autoFocus,
355
+ secureTextEntry: typeProps.secureTextEntry,
356
+ keyboardType: typeProps.keyboardType,
357
+ autoCapitalize: typeProps.autoCapitalize,
358
+ autoComplete: typeProps.autoComplete,
359
+ textContentType: typeProps.textContentType,
360
+ accessibilityLabel: resolvedA11yLabel,
361
+ accessibilityHint: accessibilityHint
362
+ }), processedTrailing != null && /*#__PURE__*/_jsx(View, {
363
+ accessibilityElementsHidden: true,
364
+ importantForAccessibility: "no",
365
+ children: processedTrailing
366
+ })]
367
+ }), supportLabel != null && supportLabel !== '' && /*#__PURE__*/_jsx(SupportText, {
218
368
  label: supportLabel,
219
369
  status: supportStatus,
220
- modes: resolvedModes
370
+ modes: modes
221
371
  })]
222
372
  });
223
373
  }
@@ -6,6 +6,8 @@ import { getVariableByName } from '../../design-tokens/figma-variables-resolver'
6
6
  import Icon from '../../icons/Icon';
7
7
  import { usePressableWebSupport } from '../../utils/web-platform-utils';
8
8
  import { EMPTY_MODES } from '../../utils/react-utils';
9
+ import Skeleton from '../../skeleton/Skeleton';
10
+ import { useSkeleton } from '../../skeleton/SkeletonGroup';
9
11
  import { jsx as _jsx } from "react/jsx-runtime";
10
12
  // ---------------------------------------------------------------------------
11
13
  // Module-scope constants
@@ -93,6 +95,7 @@ function IconButton({
93
95
  inactiveIcon,
94
96
  inactiveSource,
95
97
  isActive = false,
98
+ loading,
96
99
  ...rest
97
100
  }) {
98
101
  // Merge explicit props with modes for token resolution. Memoize the merged
@@ -104,6 +107,13 @@ function IconButton({
104
107
  isActive
105
108
  }), [modes, isToggle, isActive]);
106
109
  const tokens = useMemo(() => resolveIconButtonTokens(componentModes, disabled), [componentModes, disabled]);
110
+
111
+ // Hook called unconditionally — short-circuit below comes AFTER all hooks
112
+ // to keep React's hook order stable across renders.
113
+ const {
114
+ active: groupActive
115
+ } = useSkeleton();
116
+ const isLoading = loading ?? groupActive;
107
117
  const [isFocused, setIsFocused] = useState(false);
108
118
  const [isHovered, setIsHovered] = useState(false);
109
119
  const userHandlersRef = useRef({});
@@ -175,6 +185,16 @@ function IconButton({
175
185
  const styleCallback = useCallback(({
176
186
  pressed
177
187
  }) => [tokens.baseContainerStyle, style, pressed && !disabled ? pressedOverlayStyle : null, isHovered && !disabled ? hoverOverlayStyle : null, isFocused && !disabled ? focusOverlayStyle : null], [tokens.baseContainerStyle, style, isHovered, isFocused, disabled]);
188
+ if (isLoading) {
189
+ const size = tokens.baseContainerStyle.width;
190
+ return /*#__PURE__*/_jsx(Skeleton, {
191
+ kind: "other",
192
+ width: size,
193
+ height: size,
194
+ style: style,
195
+ modes: componentModes
196
+ });
197
+ }
178
198
  return /*#__PURE__*/_jsx(Pressable, {
179
199
  accessibilityRole: "button",
180
200
  accessibilityLabel: undefined,
@@ -2,6 +2,8 @@
2
2
 
3
3
  import React, { useMemo } from 'react';
4
4
  import { Image as RNImage, View } from 'react-native';
5
+ import Skeleton from '../../skeleton/Skeleton';
6
+ import { useSkeleton } from '../../skeleton/SkeletonGroup';
5
7
  import { jsx as _jsx } from "react/jsx-runtime";
6
8
  function normalizeSource(imageSource) {
7
9
  if (imageSource == null) return undefined;
@@ -41,7 +43,8 @@ function Image({
41
43
  style,
42
44
  accessibilityLabel,
43
45
  accessibilityElementsHidden,
44
- importantForAccessibility
46
+ importantForAccessibility,
47
+ loading
45
48
  }) {
46
49
  const source = useMemo(() => normalizeSource(imageSource), [imageSource]);
47
50
  const layoutStyle = useMemo(() => {
@@ -63,6 +66,27 @@ function Image({
63
66
  if (borderRadius != null) s.borderRadius = borderRadius;
64
67
  return s;
65
68
  }, [ratio, width, height, borderRadius]);
69
+ const {
70
+ active: groupActive
71
+ } = useSkeleton();
72
+ const isLoading = loading ?? groupActive;
73
+ if (isLoading) {
74
+ // Match the loaded image's exact box. If height is unknown but a ratio
75
+ // is set, the skeleton uses `aspectRatio` the same way the loaded image
76
+ // would, so layout never jumps when the load resolves.
77
+ const skeletonStyle = {
78
+ width: width ?? '100%',
79
+ ...(height != null ? {
80
+ height: height
81
+ } : {
82
+ aspectRatio: ratio
83
+ })
84
+ };
85
+ return /*#__PURE__*/_jsx(Skeleton, {
86
+ kind: "image",
87
+ style: skeletonStyle
88
+ });
89
+ }
66
90
  if (!source) {
67
91
  return /*#__PURE__*/_jsx(View, {
68
92
  style: [layoutStyle, style]