jfs-components 0.0.79 → 0.0.84

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 (110) hide show
  1. package/lib/commonjs/components/AppBar/AppBar.js +56 -6
  2. package/lib/commonjs/components/Attached/Attached.js +46 -7
  3. package/lib/commonjs/components/Checkbox/Checkbox.js +18 -2
  4. package/lib/commonjs/components/Drawer/Drawer.js +6 -1
  5. package/lib/commonjs/components/DropdownInput/DropdownInput.js +30 -6
  6. package/lib/commonjs/components/ExpandableCheckbox/ExpandableCheckbox.js +17 -11
  7. package/lib/commonjs/components/FormField/FormField.js +1 -14
  8. package/lib/commonjs/components/FullscreenModal/FullscreenModal.js +5 -1
  9. package/lib/commonjs/components/ListItem/ListItem.js +6 -11
  10. package/lib/commonjs/components/MessageField/MessageField.js +1 -13
  11. package/lib/commonjs/components/PaymentFeedback/PaymentFeedback.js +12 -9
  12. package/lib/commonjs/components/PlanComparisonCard/PlanComparisonCard.js +69 -160
  13. package/lib/commonjs/components/Spinner/Spinner.js +217 -0
  14. package/lib/commonjs/components/TextInput/TextInput.js +33 -18
  15. package/lib/commonjs/components/index.js +7 -0
  16. package/lib/commonjs/icons/components/IconArrowdown.js +19 -0
  17. package/lib/commonjs/icons/components/IconArrowup.js +19 -0
  18. package/lib/commonjs/icons/components/IconChevrondowncircle.js +19 -0
  19. package/lib/commonjs/icons/components/IconChevronleftcircle.js +19 -0
  20. package/lib/commonjs/icons/components/IconChevronrightcircle.js +19 -0
  21. package/lib/commonjs/icons/components/IconChevronupcircle.js +19 -0
  22. package/lib/commonjs/icons/components/IconOsnavback.js +19 -0
  23. package/lib/commonjs/icons/components/IconOsnavcenter.js +19 -0
  24. package/lib/commonjs/icons/components/IconOsnavhome.js +19 -0
  25. package/lib/commonjs/icons/components/IconOsnavtask.js +19 -0
  26. package/lib/commonjs/icons/components/IconSignin.js +19 -0
  27. package/lib/commonjs/icons/components/IconSignout.js +19 -0
  28. package/lib/commonjs/icons/components/index.js +132 -0
  29. package/lib/commonjs/icons/registry.js +2 -2
  30. package/lib/module/components/AppBar/AppBar.js +56 -6
  31. package/lib/module/components/Attached/Attached.js +46 -7
  32. package/lib/module/components/Checkbox/Checkbox.js +18 -2
  33. package/lib/module/components/Drawer/Drawer.js +6 -1
  34. package/lib/module/components/DropdownInput/DropdownInput.js +30 -6
  35. package/lib/module/components/ExpandableCheckbox/ExpandableCheckbox.js +17 -11
  36. package/lib/module/components/FormField/FormField.js +3 -16
  37. package/lib/module/components/FullscreenModal/FullscreenModal.js +5 -1
  38. package/lib/module/components/ListItem/ListItem.js +6 -11
  39. package/lib/module/components/MessageField/MessageField.js +3 -15
  40. package/lib/module/components/PaymentFeedback/PaymentFeedback.js +13 -9
  41. package/lib/module/components/PlanComparisonCard/PlanComparisonCard.js +72 -160
  42. package/lib/module/components/Spinner/Spinner.js +212 -0
  43. package/lib/module/components/TextInput/TextInput.js +34 -19
  44. package/lib/module/components/index.js +1 -0
  45. package/lib/module/icons/components/IconArrowdown.js +12 -0
  46. package/lib/module/icons/components/IconArrowup.js +12 -0
  47. package/lib/module/icons/components/IconChevrondowncircle.js +12 -0
  48. package/lib/module/icons/components/IconChevronleftcircle.js +12 -0
  49. package/lib/module/icons/components/IconChevronrightcircle.js +12 -0
  50. package/lib/module/icons/components/IconChevronupcircle.js +12 -0
  51. package/lib/module/icons/components/IconOsnavback.js +12 -0
  52. package/lib/module/icons/components/IconOsnavcenter.js +12 -0
  53. package/lib/module/icons/components/IconOsnavhome.js +12 -0
  54. package/lib/module/icons/components/IconOsnavtask.js +12 -0
  55. package/lib/module/icons/components/IconSignin.js +12 -0
  56. package/lib/module/icons/components/IconSignout.js +12 -0
  57. package/lib/module/icons/components/index.js +12 -0
  58. package/lib/module/icons/registry.js +2 -2
  59. package/lib/typescript/src/components/AppBar/AppBar.d.ts +12 -1
  60. package/lib/typescript/src/components/Attached/Attached.d.ts +19 -16
  61. package/lib/typescript/src/components/DropdownInput/DropdownInput.d.ts +3 -2
  62. package/lib/typescript/src/components/ListItem/ListItem.d.ts +3 -3
  63. package/lib/typescript/src/components/PaymentFeedback/PaymentFeedback.d.ts +5 -1
  64. package/lib/typescript/src/components/PlanComparisonCard/PlanComparisonCard.d.ts +10 -8
  65. package/lib/typescript/src/components/Spinner/Spinner.d.ts +45 -0
  66. package/lib/typescript/src/components/index.d.ts +1 -0
  67. package/lib/typescript/src/icons/components/IconArrowdown.d.ts +3 -0
  68. package/lib/typescript/src/icons/components/IconArrowup.d.ts +3 -0
  69. package/lib/typescript/src/icons/components/IconChevrondowncircle.d.ts +3 -0
  70. package/lib/typescript/src/icons/components/IconChevronleftcircle.d.ts +3 -0
  71. package/lib/typescript/src/icons/components/IconChevronrightcircle.d.ts +3 -0
  72. package/lib/typescript/src/icons/components/IconChevronupcircle.d.ts +3 -0
  73. package/lib/typescript/src/icons/components/IconOsnavback.d.ts +3 -0
  74. package/lib/typescript/src/icons/components/IconOsnavcenter.d.ts +3 -0
  75. package/lib/typescript/src/icons/components/IconOsnavhome.d.ts +3 -0
  76. package/lib/typescript/src/icons/components/IconOsnavtask.d.ts +3 -0
  77. package/lib/typescript/src/icons/components/IconSignin.d.ts +3 -0
  78. package/lib/typescript/src/icons/components/IconSignout.d.ts +3 -0
  79. package/lib/typescript/src/icons/components/index.d.ts +12 -0
  80. package/lib/typescript/src/icons/registry.d.ts +1 -1
  81. package/package.json +3 -2
  82. package/src/components/AppBar/AppBar.tsx +79 -12
  83. package/src/components/Attached/Attached.tsx +63 -7
  84. package/src/components/Checkbox/Checkbox.tsx +14 -2
  85. package/src/components/Drawer/Drawer.tsx +4 -0
  86. package/src/components/DropdownInput/DropdownInput.tsx +54 -20
  87. package/src/components/ExpandableCheckbox/ExpandableCheckbox.tsx +13 -9
  88. package/src/components/FormField/FormField.tsx +3 -19
  89. package/src/components/FullscreenModal/FullscreenModal.tsx +3 -0
  90. package/src/components/ListItem/ListItem.tsx +14 -16
  91. package/src/components/MessageField/MessageField.tsx +3 -18
  92. package/src/components/PaymentFeedback/PaymentFeedback.tsx +15 -8
  93. package/src/components/PlanComparisonCard/PlanComparisonCard.tsx +82 -192
  94. package/src/components/Spinner/Spinner.tsx +273 -0
  95. package/src/components/TextInput/TextInput.tsx +37 -19
  96. package/src/components/index.ts +1 -0
  97. package/src/icons/components/IconArrowdown.tsx +11 -0
  98. package/src/icons/components/IconArrowup.tsx +11 -0
  99. package/src/icons/components/IconChevrondowncircle.tsx +11 -0
  100. package/src/icons/components/IconChevronleftcircle.tsx +11 -0
  101. package/src/icons/components/IconChevronrightcircle.tsx +11 -0
  102. package/src/icons/components/IconChevronupcircle.tsx +11 -0
  103. package/src/icons/components/IconOsnavback.tsx +11 -0
  104. package/src/icons/components/IconOsnavcenter.tsx +11 -0
  105. package/src/icons/components/IconOsnavhome.tsx +11 -0
  106. package/src/icons/components/IconOsnavtask.tsx +11 -0
  107. package/src/icons/components/IconSignin.tsx +11 -0
  108. package/src/icons/components/IconSignout.tsx +11 -0
  109. package/src/icons/components/index.ts +12 -0
  110. package/src/icons/registry.ts +49 -1
@@ -1,11 +1,16 @@
1
1
  "use strict";
2
2
 
3
- import React, { useState, useCallback } from 'react';
3
+ import React from 'react';
4
4
  import { View, Text, Pressable, Platform } from 'react-native';
5
5
  import { getVariableByName } from '../../design-tokens/figma-variables-resolver';
6
6
  import { EMPTY_MODES, cloneChildrenWithModes } from '../../utils/react-utils';
7
7
  import Icon from '../../icons/Icon';
8
8
 
9
+ /** Figma grid: label column 1.8fr, each plan column 1fr. */
10
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
11
+ const LABEL_COLUMN_FR = 1.8;
12
+ const PLAN_COLUMN_FR = 1;
13
+
9
14
  /**
10
15
  * A single plan column header (the label column has no header of its own).
11
16
  */
@@ -17,7 +22,7 @@ import Icon from '../../icons/Icon';
17
22
  * - any React node → rendered as-is (e.g. a `Badge`, `MoneyValue`, icon…).
18
23
  * - `null` / `undefined` / `true` → empty cell.
19
24
  */
20
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
25
+
21
26
  const DEFAULT_COLUMNS = [{
22
27
  label: 'Your plan'
23
28
  }, {
@@ -37,121 +42,65 @@ const DEFAULT_ROWS = [{
37
42
  values: [false, '1%']
38
43
  }];
39
44
 
45
+ /** Keeps every text layer on a single line. */
46
+ const NO_WRAP_TEXT = {
47
+ flexShrink: 0,
48
+ ...(Platform.OS === 'web' ? {
49
+ whiteSpace: 'nowrap'
50
+ } : {})
51
+ };
52
+ const labelColumnStyle = {
53
+ flex: LABEL_COLUMN_FR,
54
+ minWidth: 0
55
+ };
56
+ const planColumnStyle = {
57
+ flex: PLAN_COLUMN_FR,
58
+ minWidth: 0,
59
+ alignItems: 'center'
60
+ };
61
+
40
62
  /**
41
63
  * PlanComparisonCard renders a compact comparison table that pits the user's
42
64
  * current plan against one or more alternative plans across a set of feature
43
65
  * rows. Implementation of Figma node `4498:2968` (`PlanComparisonCard`).
44
66
  *
45
- * The leading column holds feature labels (with an optional info icon); every
46
- * other column maps to a plan in `columns`. Each cell value can be plain text,
47
- * a "not available" cross (`false`), or any custom React node.
67
+ * Columns use a 1.8fr / 1fr flex ratio (label vs plan), matching the Figma grid.
48
68
  *
49
69
  * @component
50
- * @example
51
- * ```tsx
52
- * <PlanComparisonCard
53
- * columns={[{ label: 'Your plan' }, { label: 'JioFinance+', brand: true }]}
54
- * rows={[
55
- * { label: 'JioPoints multiplier', values: ['1x', '1.25x'] },
56
- * { label: 'Cashback', showInfo: true, values: [false, 'Upto ₹5000'] },
57
- * ]}
58
- * />
59
- * ```
60
70
  */
61
- /** Keeps every text layer on a single line; columns grow to fit content. */
62
- const NO_WRAP_TEXT = {
63
- flexShrink: 0,
64
- ...(Platform.OS === 'web' ? {
65
- whiteSpace: 'nowrap'
66
- } : {})
67
- };
68
71
  function PlanComparisonCard({
69
72
  columns = DEFAULT_COLUMNS,
70
73
  rows = DEFAULT_ROWS,
71
- labelColumnFlex = 0,
72
74
  modes = EMPTY_MODES,
73
75
  style
74
76
  }) {
75
- /** Natural widths from header labels (plan columns only). */
76
- const [headerWidths, setHeaderWidths] = useState([]);
77
- /** Natural widths from table body columns. */
78
- const [bodyWidths, setBodyWidths] = useState([]);
79
- const setMeasuredWidth = useCallback((setter, index, width) => {
80
- setter(prev => {
81
- if (prev[index] === width) return prev;
82
- const next = [...prev];
83
- next[index] = width;
84
- return next;
85
- });
86
- }, []);
87
- const onHeaderColumnLayout = useCallback((index, event) => {
88
- setMeasuredWidth(setHeaderWidths, index, event.nativeEvent.layout.width);
89
- }, [setMeasuredWidth]);
90
- const onBodyColumnLayout = useCallback((index, event) => {
91
- setMeasuredWidth(setBodyWidths, index, event.nativeEvent.layout.width);
92
- }, [setMeasuredWidth]);
93
-
94
- /**
95
- * Shared width for header + body cells in a column (max of natural header
96
- * label vs body content). No columnGap between columns — gaps would shift
97
- * headers relative to the flush table grid below.
98
- */
99
- const columnWidthStyle = index => {
100
- const width = Math.max(headerWidths[index] ?? 0, bodyWidths[index] ?? 0);
101
- if (width > 0) {
102
- return {
103
- width,
104
- minWidth: width,
105
- flexShrink: 0,
106
- flexGrow: 0
107
- };
108
- }
109
- return {
110
- flexShrink: 0,
111
- flexGrow: 0
112
- };
113
- };
114
-
115
- // Container
116
77
  const gap = getVariableByName('planComparisonCard/gap', modes) ?? 16;
117
-
118
- // Header
119
78
  const headerFg = getVariableByName('planComparisonCard/header/fg', modes) ?? '#ffffff';
120
79
  const headerBrandFg = getVariableByName('planComparisonCard/header/brand/fg', modes) ?? '#cea15a';
121
80
  const headerFontSize = getVariableByName('planComparisonCard/header/fontSize', modes) ?? 14;
122
81
  const headerFontFamily = getVariableByName('planComparisonCard/header/fontFamily', modes) ?? 'JioType Var';
123
82
  const headerLineHeight = getVariableByName('planComparisonCard/header/lineHeight', modes) ?? 18;
124
83
  const headerFontWeight = getVariableByName('planComparisonCard/header/fontWeight', modes) ?? '500';
125
-
126
- // Table
127
84
  const tableBackground = getVariableByName('planComparisonCard/tableRow/background', modes) ?? '#141414';
128
85
  const tableRadius = getVariableByName('planComparisonCard/tableRow/radius', modes) ?? 16;
129
86
  const tableBorderSize = getVariableByName('planComparisonCard/tableRow/border/size', modes) ?? 1;
130
87
  const tableBorderColor = getVariableByName('planComparisonCard/tableRow/border/color', modes) ?? '#1e1a14';
131
-
132
- // Cell
133
88
  const cellPadding = getVariableByName('planComparisonCard/tableCell/padding', modes) ?? 12;
134
89
  const cellGap = getVariableByName('planComparisonCard/tableCell/gap', modes) ?? 2;
135
90
  const cellMinHeight = getVariableByName('planComparisonCard/tableCell/height', modes) ?? 46;
136
91
  const cellBorderSize = getVariableByName('planComparisonCard/tableCell/border/size', modes) ?? 1;
137
92
  const cellBorderColor = getVariableByName('planComparisonCard/tableCell/border/color', modes) ?? '#1e1a14';
138
-
139
- // Cell label
140
93
  const labelColor = getVariableByName('planComparisonCard/tableCell/label/color', modes) ?? '#ffffff';
141
94
  const labelDisabledColor = getVariableByName('planComparisonCard/tableCell/label/disabled/color', modes) ?? '#91949c';
142
95
  const labelFontSize = getVariableByName('planComparisonCard/tableCell/label/fontSize', modes) ?? 12;
143
96
  const labelFontFamily = getVariableByName('planComparisonCard/tableCell/label/fontFamily', modes) ?? 'JioType Var';
144
97
  const labelLineHeight = getVariableByName('planComparisonCard/tableCell/label/lineHeight', modes) ?? 16;
145
98
  const labelFontWeight = getVariableByName('planComparisonCard/tableCell/label/fontWeight', modes) ?? '400';
146
-
147
- // Cell value
148
99
  const valueColor = getVariableByName('planComparisonCard/tableCell/value/color', modes) ?? '#ffffff';
149
100
  const valueFontSize = getVariableByName('planComparisonCard/tableCell/value/fontSize', modes) ?? 12;
150
101
  const valueFontFamily = getVariableByName('planComparisonCard/tableCell/value/fontFamily', modes) ?? 'JioType Var';
151
102
  const valueLineHeight = getVariableByName('planComparisonCard/tableCell/value/lineHeight', modes) ?? 16;
152
103
  const valueFontWeight = getVariableByName('planComparisonCard/tableCell/value/fontWeight', modes) ?? '500';
153
-
154
- // Icon
155
104
  const iconColor = getVariableByName('planComparisonCard/icon/color', modes) ?? '#ffffff';
156
105
  const iconSize = getVariableByName('planComparisonCard/icon/size', modes) ?? 16;
157
106
  const toWeight = w => typeof w === 'number' ? `${w}` : w;
@@ -180,12 +129,26 @@ function PlanComparisonCard({
180
129
  fontWeight: toWeight(valueFontWeight),
181
130
  textAlign: 'center'
182
131
  };
183
- const planHeaderColumnStyle = {
132
+ const rowStyle = {
133
+ flexDirection: 'row',
134
+ width: '100%'
135
+ };
136
+ const labelCellStyle = {
137
+ flexDirection: 'row',
138
+ alignItems: 'center',
139
+ gap: cellGap,
140
+ padding: cellPadding,
141
+ minHeight: cellMinHeight
142
+ };
143
+ const valueCellStyle = {
144
+ flexDirection: 'row',
184
145
  alignItems: 'center',
185
- justifyContent: 'center'
146
+ justifyContent: 'center',
147
+ padding: cellPadding,
148
+ minHeight: cellMinHeight,
149
+ width: '100%'
186
150
  };
187
151
  const renderValue = (value, cellKey) => {
188
- // "Not available" → muted cross icon.
189
152
  if (value === false) {
190
153
  return /*#__PURE__*/_jsx(Icon, {
191
154
  name: "ic_close",
@@ -193,87 +156,54 @@ function PlanComparisonCard({
193
156
  color: labelDisabledColor
194
157
  }, cellKey);
195
158
  }
196
- // Empty cell.
197
159
  if (value === null || value === undefined || value === true) {
198
160
  return null;
199
161
  }
200
- // Text content.
201
162
  if (typeof value === 'string' || typeof value === 'number') {
202
163
  return /*#__PURE__*/_jsx(Text, {
203
164
  style: valueTextStyle,
204
165
  children: value
205
166
  }, cellKey);
206
167
  }
207
- // Custom node — forward modes so themed children stay in sync.
208
168
  return cloneChildrenWithModes(value, modes);
209
169
  };
210
- const labelCellStyle = {
211
- flexDirection: 'row',
212
- alignItems: 'center',
213
- gap: cellGap,
214
- padding: cellPadding,
215
- minHeight: cellMinHeight,
216
- flexShrink: 0
217
- };
218
- const valueCellStyle = {
219
- flexDirection: 'row',
220
- alignItems: 'center',
221
- justifyContent: 'center',
222
- padding: cellPadding,
223
- minHeight: cellMinHeight,
224
- flexShrink: 0
225
- };
226
170
  return /*#__PURE__*/_jsxs(View, {
227
171
  style: [{
228
172
  gap,
229
- alignSelf: 'flex-start'
173
+ width: '100%'
230
174
  }, style],
231
175
  children: [/*#__PURE__*/_jsxs(View, {
232
- style: {
233
- flexDirection: 'row',
234
- alignItems: 'flex-end'
235
- },
176
+ style: rowStyle,
236
177
  children: [/*#__PURE__*/_jsx(View, {
237
- style: [columnWidthStyle(0), labelColumnFlex > 0 ? {
238
- flexGrow: labelColumnFlex
239
- } : undefined]
240
- }), columns.map((column, index) => {
241
- const colIndex = index + 1;
242
- return /*#__PURE__*/_jsx(View, {
243
- onLayout: e => onHeaderColumnLayout(colIndex, e),
244
- style: [columnWidthStyle(colIndex), planHeaderColumnStyle],
245
- children: /*#__PURE__*/_jsx(Text, {
246
- style: [headerTextStyle, {
247
- color: column.brand ? headerBrandFg : headerFg,
248
- alignSelf: 'center'
249
- }],
250
- children: column.label
251
- })
252
- }, column.label ?? index);
253
- })]
254
- }), /*#__PURE__*/_jsxs(View, {
178
+ style: labelColumnStyle
179
+ }), columns.map((column, index) => /*#__PURE__*/_jsx(View, {
180
+ style: planColumnStyle,
181
+ children: /*#__PURE__*/_jsx(Text, {
182
+ style: [headerTextStyle, {
183
+ color: column.brand ? headerBrandFg : headerFg
184
+ }],
185
+ children: column.label
186
+ })
187
+ }, column.label ?? index))]
188
+ }), /*#__PURE__*/_jsx(View, {
255
189
  style: {
256
- flexDirection: 'row',
257
- alignSelf: 'flex-start',
190
+ width: '100%',
258
191
  backgroundColor: tableBackground,
259
192
  borderWidth: tableBorderSize,
260
193
  borderColor: tableBorderColor,
261
194
  borderRadius: tableRadius,
262
195
  overflow: 'hidden'
263
196
  },
264
- children: [/*#__PURE__*/_jsx(View, {
265
- onLayout: e => onBodyColumnLayout(0, e),
266
- style: [columnWidthStyle(0), labelColumnFlex > 0 ? {
267
- flexGrow: labelColumnFlex
268
- } : undefined],
269
- children: rows.map((row, rowIndex) => {
270
- const isLast = rowIndex === rows.length - 1;
271
- const showInfo = row.showInfo || row.onInfoPress != null;
272
- return /*#__PURE__*/_jsxs(View, {
273
- style: [labelCellStyle, {
274
- borderBottomWidth: isLast ? 0 : cellBorderSize,
275
- borderBottomColor: cellBorderColor
276
- }],
197
+ children: rows.map((row, rowIndex) => {
198
+ const isLast = rowIndex === rows.length - 1;
199
+ const showInfo = row.showInfo || row.onInfoPress != null;
200
+ return /*#__PURE__*/_jsxs(View, {
201
+ style: [rowStyle, {
202
+ borderBottomWidth: isLast ? 0 : cellBorderSize,
203
+ borderBottomColor: cellBorderColor
204
+ }],
205
+ children: [/*#__PURE__*/_jsxs(View, {
206
+ style: [labelColumnStyle, labelCellStyle],
277
207
  children: [/*#__PURE__*/_jsx(Text, {
278
208
  style: labelTextStyle,
279
209
  children: row.label
@@ -292,30 +222,12 @@ function PlanComparisonCard({
292
222
  size: iconSize,
293
223
  color: iconColor
294
224
  }))]
295
- }, row.key ?? `${row.label}-${rowIndex}`);
296
- })
297
- }), columns.map((column, colIndex) => {
298
- const colIndexWidth = colIndex + 1;
299
- return /*#__PURE__*/_jsx(View, {
300
- onLayout: e => onBodyColumnLayout(colIndexWidth, e),
301
- style: [columnWidthStyle(colIndexWidth), planHeaderColumnStyle],
302
- children: rows.map((row, rowIndex) => {
303
- const isLast = rowIndex === rows.length - 1;
304
- return /*#__PURE__*/_jsx(View, {
305
- style: [valueCellStyle, {
306
- borderBottomWidth: isLast ? 0 : cellBorderSize,
307
- borderBottomColor: cellBorderColor
308
- }],
309
- children: /*#__PURE__*/_jsx(View, {
310
- style: {
311
- flexShrink: 0
312
- },
313
- children: renderValue(row.values?.[colIndex], `${rowIndex}-${colIndex}`)
314
- })
315
- }, row.key ?? `${row.label}-${rowIndex}`);
316
- })
317
- }, column.label ?? colIndex);
318
- })]
225
+ }), columns.map((column, colIndex) => /*#__PURE__*/_jsx(View, {
226
+ style: [planColumnStyle, valueCellStyle],
227
+ children: renderValue(row.values?.[colIndex], `${rowIndex}-${colIndex}`)
228
+ }, column.label ?? colIndex))]
229
+ }, row.key ?? `${row.label}-${rowIndex}`);
230
+ })
319
231
  })]
320
232
  });
321
233
  }
@@ -0,0 +1,212 @@
1
+ "use strict";
2
+
3
+ import React, { useEffect } from 'react';
4
+ import { StyleSheet, View } from 'react-native';
5
+ import Animated, { Easing, cancelAnimation, useAnimatedStyle, useSharedValue, withRepeat, withTiming } from 'react-native-reanimated';
6
+ import Svg, { Path } from 'react-native-svg';
7
+ import { getVariableByName } from '../../design-tokens/figma-variables-resolver';
8
+ import { useTokens } from '../../design-tokens/JFSThemeProvider';
9
+ import { EMPTY_MODES } from '../../utils/react-utils';
10
+ import { useReducedMotion } from '../../skeleton/useReducedMotion';
11
+
12
+ /**
13
+ * Per-segment colours, resolved from the Figma `spiner/*` tokens. Consumers can
14
+ * override any subset via the `colors` prop.
15
+ */
16
+ import { jsx as _jsx } from "react/jsx-runtime";
17
+ const SEGMENT_COUNT = 3;
18
+ const DEFAULT_SIZE = 72;
19
+ const DEFAULT_DURATION_MS = 1500;
20
+ const DEFAULT_GRAVITY = 0.45;
21
+
22
+ // Stroke thickness as a fraction of the diameter (matches the Figma ring weight).
23
+ const STROKE_RATIO = 0.11;
24
+ // Angular length of each individual segment.
25
+ const ARC_LENGTH_DEG = 100;
26
+ // Spacing between consecutive heads when fully bunched at the top. Small but
27
+ // non-zero so all three colours stay faintly visible as they crest the top.
28
+ const SPREAD_MIN_DEG = 10;
29
+ // Spacing between consecutive heads at full spread. At this extent each segment's
30
+ // tail only overlaps the next head by `ARC_LENGTH_DEG - SPREAD_MAX_DEG` (16°) —
31
+ // the maximum extension the chain reaches while staying connected (never a gap).
32
+ const SPREAD_MAX_DEG = 84;
33
+ // Fraction of each revolution spent gradually fanning *out* (the rest is spent
34
+ // snapping back together over the top).
35
+ //
36
+ // This is the knob that balances "reaches full extension" against "never stalls
37
+ // and never recoils". The tail segment's velocity while spreading is
38
+ // `vLead * (1 - spreadRange / (SPREAD_OUT_FRAC * π))`. Spreading the fan-out over
39
+ // ~3/4 of the turn keeps that factor around ~0.45 (so the tail always carries
40
+ // clear forward momentum — it never crawls to a stall, and never reverses),
41
+ // while still letting the breath reach a full 1.0. The remaining ~1/4 is an
42
+ // energetic gather over the top where the trailing segments whip forward to
43
+ // rejoin the lead. A symmetric (sinusoidal/triangle) breath cannot do all three:
44
+ // reach full extension, avoid recoil, and avoid a sustained stall.
45
+ const SPREAD_OUT_FRAC = 0.75;
46
+ const DEG_TO_RAD = Math.PI / 180;
47
+ const TWO_PI = Math.PI * 2;
48
+ const clamp = (value, min, max) => Math.min(max, Math.max(min, value));
49
+ const toNumber = (value, fallback) => {
50
+ if (typeof value === 'number' && Number.isFinite(value)) {
51
+ return value;
52
+ }
53
+ if (typeof value === 'string') {
54
+ const parsed = Number(value);
55
+ if (Number.isFinite(parsed)) {
56
+ return parsed;
57
+ }
58
+ }
59
+ return fallback;
60
+ };
61
+
62
+ /**
63
+ * Builds the SVG path for a single fixed-length arc whose *head* sits at the
64
+ * top (12 o'clock) and whose body trails counter-clockwise behind it. Rotating
65
+ * the containing view clockwise then places the head at the desired angle.
66
+ */
67
+ const buildArcPath = (center, radius, arcLengthDeg) => {
68
+ const arc = arcLengthDeg * DEG_TO_RAD;
69
+ // Head at the top: phi = 0 -> (center, center - radius).
70
+ const headX = center;
71
+ const headY = center - radius;
72
+ // Tail trails counter-clockwise by `arc`: phi = -arc.
73
+ const tailX = center + radius * Math.sin(-arc);
74
+ const tailY = center - radius * Math.cos(-arc);
75
+ const largeArc = arcLengthDeg > 180 ? 1 : 0;
76
+ // Sweep from tail -> head is clockwise (sweep flag = 1 in SVG y-down space).
77
+ return `M ${tailX} ${tailY} A ${radius} ${radius} 0 ${largeArc} 1 ${headX} ${headY}`;
78
+ };
79
+
80
+ /**
81
+ * Animated rotation for one segment.
82
+ *
83
+ * A single linear clock drives a gravity-warped lead angle: it advances faster
84
+ * over the top and slower through the bottom, giving the fall its weight. Each
85
+ * segment trails the lead by `index * offset`, where `offset` breathes between
86
+ * its bunched (top) and spread (bottom) extents in lock-step with the lead's
87
+ * vertical position. Because the offset is bounded by `SPREAD_MAX_DEG`, the
88
+ * three segments form a continuously-overlapping chain that gathers at the top
89
+ * and fans out — fully connected — through the free fall.
90
+ */
91
+ const useSegmentRotation = (clock, index, gravity, spreadMinRad, spreadMaxRad, spreadOutFrac) => useAnimatedStyle(() => {
92
+ 'worklet';
93
+
94
+ const tau = clock.value * TWO_PI;
95
+ // Lead angle (clockwise from top). d(lead)/dtau = 1 + gravity*cos(tau) is
96
+ // maximal at the top (tau = 0) and minimal at the bottom (tau = PI), giving
97
+ // the fall its weight.
98
+ const lead = tau + gravity * Math.sin(tau);
99
+ // Breathing is an asymmetric saw in the lead angle: it ramps *gradually* from
100
+ // 0 (bunched, top) up to 1 (fully spread) over `spreadOutFrac` of the turn,
101
+ // then drops back to 0 over the remaining arc (the quick gather over the top).
102
+ // The gentle fan-out slope keeps the trailing segment moving forward at a
103
+ // healthy fraction of the lead's speed — it never stalls and never recoils —
104
+ // while still reaching full extension; the steeper gather is a forward whip,
105
+ // so momentum only ever increases there.
106
+ const leadMod = lead - TWO_PI * Math.floor(lead / TWO_PI);
107
+ const splitLead = spreadOutFrac * TWO_PI;
108
+ const breath = leadMod < splitLead ? leadMod / splitLead : (TWO_PI - leadMod) / (TWO_PI - splitLead);
109
+ const offset = spreadMinRad + breath * (spreadMaxRad - spreadMinRad);
110
+ const head = lead - index * offset;
111
+ return {
112
+ transform: [{
113
+ rotate: `${head * 180 / Math.PI}deg`
114
+ }]
115
+ };
116
+ }, [gravity, index, spreadMinRad, spreadMaxRad, spreadOutFrac]);
117
+ const fullSize = {
118
+ ...StyleSheet.absoluteFillObject
119
+ };
120
+ function Spinner({
121
+ size = DEFAULT_SIZE,
122
+ durationMs = DEFAULT_DURATION_MS,
123
+ gravity = DEFAULT_GRAVITY,
124
+ colors,
125
+ animating = true,
126
+ modes: propModes = EMPTY_MODES,
127
+ style,
128
+ accessibilityLabel = 'Loading',
129
+ ...rest
130
+ }) {
131
+ const {
132
+ modes: globalModes
133
+ } = useTokens();
134
+ const modes = {
135
+ ...globalModes,
136
+ ...propModes
137
+ };
138
+ const systemReducedMotion = useReducedMotion();
139
+ const isAnimated = animating && !systemReducedMotion;
140
+ const resolvedSize = toNumber(size, DEFAULT_SIZE);
141
+ const safeGravity = clamp(toNumber(gravity, DEFAULT_GRAVITY), 0, 0.9);
142
+ const strokeWidth = Math.max(1, resolvedSize * STROKE_RATIO);
143
+ const radius = Math.max(0, (resolvedSize - strokeWidth) / 2);
144
+ const center = resolvedSize / 2;
145
+ const arcPath = buildArcPath(center, radius, ARC_LENGTH_DEG);
146
+ const segmentColors = [colors?.primary ?? getVariableByName('spiner/primary/bg', modes) ?? '#d0a259', colors?.secondary ?? getVariableByName('spiner/secondary/bg', modes) ?? '#5b00b5', colors?.tertiary ?? getVariableByName('spiner/tertiary/bg', modes) ?? '#066b99'];
147
+ const clock = useSharedValue(0);
148
+ useEffect(() => {
149
+ if (!isAnimated) {
150
+ cancelAnimation(clock);
151
+ clock.value = 0;
152
+ return;
153
+ }
154
+ clock.value = 0;
155
+ clock.value = withRepeat(withTiming(1, {
156
+ duration: Math.max(1, durationMs),
157
+ easing: Easing.linear
158
+ }), -1, false);
159
+ return () => {
160
+ cancelAnimation(clock);
161
+ };
162
+ }, [isAnimated, durationMs, clock]);
163
+
164
+ // Hooks must run unconditionally and in a stable order, so all three segment
165
+ // styles are always computed even when the spinner renders statically.
166
+ const spreadMinRad = SPREAD_MIN_DEG * DEG_TO_RAD;
167
+ const spreadMaxRad = SPREAD_MAX_DEG * DEG_TO_RAD;
168
+ const style0 = useSegmentRotation(clock, 0, safeGravity, spreadMinRad, spreadMaxRad, SPREAD_OUT_FRAC);
169
+ const style1 = useSegmentRotation(clock, 1, safeGravity, spreadMinRad, spreadMaxRad, SPREAD_OUT_FRAC);
170
+ const style2 = useSegmentRotation(clock, 2, safeGravity, spreadMinRad, spreadMaxRad, SPREAD_OUT_FRAC);
171
+ const animatedStyles = [style0, style1, style2];
172
+
173
+ // Static resting fan (evenly spaced) used when animation is disabled.
174
+ const restingRotations = [0, -120, -240];
175
+ const containerStyle = {
176
+ height: resolvedSize,
177
+ width: resolvedSize,
178
+ position: 'relative'
179
+ };
180
+ return /*#__PURE__*/_jsx(View, {
181
+ accessibilityRole: "progressbar",
182
+ accessibilityLabel: accessibilityLabel,
183
+ style: [containerStyle, style],
184
+ ...rest,
185
+ children: Array.from({
186
+ length: SEGMENT_COUNT
187
+ }, (_, i) => SEGMENT_COUNT - 1 - i).map(segmentIndex => {
188
+ const segmentStyle = isAnimated ? animatedStyles[segmentIndex] : {
189
+ transform: [{
190
+ rotate: `${restingRotations[segmentIndex]}deg`
191
+ }]
192
+ };
193
+ return /*#__PURE__*/_jsx(Animated.View, {
194
+ style: [fullSize, segmentStyle],
195
+ pointerEvents: "none",
196
+ children: /*#__PURE__*/_jsx(Svg, {
197
+ width: resolvedSize,
198
+ height: resolvedSize,
199
+ viewBox: `0 0 ${resolvedSize} ${resolvedSize}`,
200
+ children: /*#__PURE__*/_jsx(Path, {
201
+ d: arcPath,
202
+ stroke: segmentColors[segmentIndex],
203
+ strokeWidth: strokeWidth,
204
+ strokeLinecap: "round",
205
+ fill: "none"
206
+ })
207
+ })
208
+ }, segmentIndex);
209
+ })
210
+ });
211
+ }
212
+ export default Spinner;
@@ -1,7 +1,7 @@
1
1
  "use strict";
2
2
 
3
3
  import React, { useRef, useState } from 'react';
4
- import { Pressable, View, TextInput as RNTextInput } from 'react-native';
4
+ import { Platform, Pressable, View, TextInput as RNTextInput } from 'react-native';
5
5
  import { getVariableByName } from '../../design-tokens/figma-variables-resolver';
6
6
  import Icon from '../../icons/Icon';
7
7
  import { EMPTY_MODES, cloneChildrenWithModes } from '../../utils/react-utils';
@@ -38,7 +38,8 @@ import { EMPTY_MODES, cloneChildrenWithModes } from '../../utils/react-utils';
38
38
  * Helper function to convert a color to a more transparent version for placeholder text.
39
39
  * Takes a color string (hex, rgb, rgba) and returns it with reduced opacity.
40
40
  */
41
- import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
41
+ import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
42
+ const IS_WEB = Platform.OS === 'web';
42
43
  function makePlaceholderColor(color, opacity = 0.5) {
43
44
  if (!color || typeof color !== 'string') {
44
45
  return color || '';
@@ -114,10 +115,9 @@ function TextInput({
114
115
  // Track focus state to hide placeholder when focused
115
116
  const [isFocused, setIsFocused] = useState(false);
116
117
  const [isHovered, setIsHovered] = useState(false);
117
- // Ref to the underlying native input so a tap anywhere inside the Pressable
118
- // wrapper can programmatically focus it. Without this, on Android the
119
- // wrapping Pressable becomes the touch responder on the first tap and the
120
- // native input only gains focus on the *second* tap.
118
+ // On web we keep a ref so a click anywhere inside the (Pressable) wrapper can
119
+ // focus the input. On native the wrapper is a plain View and the native
120
+ // input focuses itself on the first tap (see container note below).
121
121
  const inputRef = useRef(null);
122
122
 
123
123
  // Resolve container tokens
@@ -209,19 +209,18 @@ function TextInput({
209
209
 
210
210
  // Generate default accessibility label from placeholder if not provided
211
211
  const defaultAccessibilityLabel = accessibilityLabel || placeholder || 'Text input';
212
- return /*#__PURE__*/_jsxs(Pressable, {
213
- style: [containerStyle, focusContainerStyle, hoverStyle, style],
214
- onHoverIn: () => setIsHovered(true),
215
- onHoverOut: () => setIsHovered(false)
216
- // Forward taps on the wrapper (padding, leading icon gutter, etc.) to the
217
- // native input. This guarantees the keyboard opens on the FIRST tap on
218
- // Android instead of requiring a second tap.
219
- ,
220
- onPress: () => inputRef.current?.focus()
221
- // The native input is the real accessible element; don't add a redundant
222
- // focusable node for screen readers.
223
- ,
224
- accessible: false,
212
+
213
+ // IMPORTANT (Android focus reliability):
214
+ // Do NOT wrap the native <TextInput> in a Pressable/Touchable on native.
215
+ // A touch-responder-claiming wrapper steals the first tap, which is the
216
+ // classic cause of the "needs 2–3 taps to focus" Android bug — and forwarding
217
+ // focus from `onPress` is unreliable because the press is cancelled by the
218
+ // tiniest finger movement. A plain <View> does not claim the responder, so
219
+ // the native input receives the tap and focuses on the FIRST tap.
220
+ // On web there is no such issue, so we keep the Pressable for the hover
221
+ // affordance plus click-anywhere-to-focus.
222
+ const containerStyleArray = [containerStyle, focusContainerStyle, hoverStyle, style];
223
+ const inner = /*#__PURE__*/_jsxs(_Fragment, {
225
224
  children: [processedLeading && /*#__PURE__*/_jsx(View, {
226
225
  accessibilityElementsHidden: true,
227
226
  importantForAccessibility: "no",
@@ -244,6 +243,22 @@ function TextInput({
244
243
  children: processedTrailing
245
244
  })]
246
245
  });
246
+ if (IS_WEB) {
247
+ return /*#__PURE__*/_jsx(Pressable, {
248
+ style: containerStyleArray,
249
+ onHoverIn: () => setIsHovered(true),
250
+ onHoverOut: () => setIsHovered(false)
251
+ // Web: clicking the padding / icon gutter focuses the input too.
252
+ ,
253
+ onPress: () => inputRef.current?.focus(),
254
+ accessible: false,
255
+ children: inner
256
+ });
257
+ }
258
+ return /*#__PURE__*/_jsx(View, {
259
+ style: containerStyleArray,
260
+ children: inner
261
+ });
247
262
  }
248
263
 
249
264
  /**
@@ -66,6 +66,7 @@ export { default as Title } from './Title/Title';
66
66
  export { default as Screen } from './Screen/Screen';
67
67
  export { default as Section } from './Section/Section';
68
68
  export { default as Slot } from './Slot/Slot';
69
+ export { default as Spinner } from './Spinner/Spinner';
69
70
  export { default as Stepper } from './Stepper/Stepper';
70
71
  export { Step } from './Stepper/Step';
71
72
  export { StepLabel } from './Stepper/StepLabel';
@@ -0,0 +1,12 @@
1
+ "use strict";
2
+
3
+ import React from 'react';
4
+ import Svg, { Path } from 'react-native-svg';
5
+ import { jsx as _jsx } from "react/jsx-runtime";
6
+ export const IconArrowdown = props => /*#__PURE__*/_jsx(Svg, {
7
+ viewBox: "0 0 24 24",
8
+ ...props,
9
+ children: /*#__PURE__*/_jsx(Path, {
10
+ d: "M18.7099 14.29C18.617 14.1963 18.5064 14.1219 18.3845 14.0711C18.2627 14.0203 18.132 13.9942 17.9999 13.9942C17.8679 13.9942 17.7372 14.0203 17.6154 14.0711C17.4935 14.1219 17.3829 14.1963 17.2899 14.29L12.9999 18.59V3C12.9999 2.73478 12.8946 2.48043 12.707 2.29289C12.5195 2.10536 12.2652 2 11.9999 2C11.7347 2 11.4804 2.10536 11.2928 2.29289C11.1053 2.48043 10.9999 2.73478 10.9999 3V18.59L6.70994 14.29C6.52164 14.1017 6.26624 13.9959 5.99994 13.9959C5.73364 13.9959 5.47825 14.1017 5.28994 14.29C5.10164 14.4783 4.99585 14.7337 4.99585 15C4.99585 15.1319 5.02182 15.2624 5.07228 15.3842C5.12274 15.5061 5.1967 15.6168 5.28994 15.71L11.2899 21.71C11.3829 21.8037 11.4935 21.8781 11.6154 21.9289C11.7372 21.9797 11.8679 22.0058 11.9999 22.0058C12.132 22.0058 12.2627 21.9797 12.3845 21.9289C12.5064 21.8781 12.617 21.8037 12.7099 21.71L18.7099 15.71C18.8037 15.617 18.8781 15.5064 18.9288 15.3846C18.9796 15.2627 19.0057 15.132 19.0057 15C19.0057 14.868 18.9796 14.7373 18.9288 14.6154C18.8781 14.4936 18.8037 14.383 18.7099 14.29Z"
11
+ })
12
+ });