jfs-components 0.1.30 → 0.1.33

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/D2C.md +118 -0
  3. package/lib/commonjs/components/CategoryCard/CategoryCard.js +264 -0
  4. package/lib/commonjs/components/CompareTable/CompareTable.js +140 -84
  5. package/lib/commonjs/components/ContentSheet/ContentSheet.js +28 -5
  6. package/lib/commonjs/components/CounterBadge/CounterBadge.js +78 -0
  7. package/lib/commonjs/components/FavoriteToggle/FavoriteToggle.js +180 -0
  8. package/lib/commonjs/components/HelloJioInput/HelloJioInput.js +287 -0
  9. package/lib/commonjs/components/ValueBackMetric/ValueBackMetric.js +219 -0
  10. package/lib/commonjs/components/index.js +35 -0
  11. package/lib/commonjs/design-tokens/Coin Variables-variables-full.json +40095 -1
  12. package/lib/commonjs/design-tokens/figma-modes.generated.js +3 -0
  13. package/lib/commonjs/icons/registry.js +1 -1
  14. package/lib/module/components/CategoryCard/CategoryCard.js +258 -0
  15. package/lib/module/components/CompareTable/CompareTable.js +140 -84
  16. package/lib/module/components/ContentSheet/ContentSheet.js +29 -6
  17. package/lib/module/components/CounterBadge/CounterBadge.js +73 -0
  18. package/lib/module/components/FavoriteToggle/FavoriteToggle.js +174 -0
  19. package/lib/module/components/HelloJioInput/HelloJioInput.js +281 -0
  20. package/lib/module/components/ValueBackMetric/ValueBackMetric.js +214 -0
  21. package/lib/module/components/index.js +6 -1
  22. package/lib/module/design-tokens/Coin Variables-variables-full.json +40095 -1
  23. package/lib/module/design-tokens/figma-modes.generated.js +3 -0
  24. package/lib/module/icons/registry.js +1 -1
  25. package/lib/typescript/src/components/CategoryCard/CategoryCard.d.ts +72 -0
  26. package/lib/typescript/src/components/CompareTable/CompareTable.d.ts +16 -1
  27. package/lib/typescript/src/components/ContentSheet/ContentSheet.d.ts +7 -1
  28. package/lib/typescript/src/components/CounterBadge/CounterBadge.d.ts +19 -0
  29. package/lib/typescript/src/components/FavoriteToggle/FavoriteToggle.d.ts +66 -0
  30. package/lib/typescript/src/components/HelloJioInput/HelloJioInput.d.ts +111 -0
  31. package/lib/typescript/src/components/ValueBackMetric/ValueBackMetric.d.ts +86 -0
  32. package/lib/typescript/src/components/index.d.ts +5 -0
  33. package/lib/typescript/src/icons/registry.d.ts +1 -1
  34. package/package.json +7 -1
  35. package/scripts/extract-figma-modes.js +359 -0
  36. package/src/components/CategoryCard/CategoryCard.tsx +404 -0
  37. package/src/components/CompareTable/CompareTable.tsx +201 -101
  38. package/src/components/ContentSheet/ContentSheet.tsx +40 -11
  39. package/src/components/CounterBadge/CounterBadge.tsx +95 -0
  40. package/src/components/FavoriteToggle/FavoriteToggle.tsx +243 -0
  41. package/src/components/HelloJioInput/HelloJioInput.tsx +513 -0
  42. package/src/components/ValueBackMetric/ValueBackMetric.tsx +298 -0
  43. package/src/components/index.ts +5 -0
  44. package/src/design-tokens/Coin Variables-variables-full.json +40095 -1
  45. package/src/design-tokens/figma-modes.generated.ts +3 -0
  46. package/src/icons/registry.ts +1 -1
@@ -0,0 +1,513 @@
1
+ import React, {
2
+ forwardRef,
3
+ useCallback,
4
+ useMemo,
5
+ useRef,
6
+ useState,
7
+ } from 'react'
8
+ import {
9
+ Platform,
10
+ Pressable,
11
+ View,
12
+ TextInput as RNTextInput,
13
+ type StyleProp,
14
+ type ViewStyle,
15
+ type TextStyle,
16
+ type TextInputProps as RNTextInputProps,
17
+ } from 'react-native'
18
+ import { getVariableByName } from '../../design-tokens/figma-variables-resolver'
19
+ import { useTokens } from '../../design-tokens/JFSThemeProvider'
20
+ import Icon from '../../icons/Icon'
21
+ import {
22
+ EMPTY_MODES,
23
+ cloneChildrenWithModes,
24
+ mergeRefs,
25
+ } from '../../utils/react-utils'
26
+ import GlassFill from '../../utils/GlassFill/GlassFill'
27
+ import IconButton from '../IconButton/IconButton'
28
+ import type { Modes } from '../../design-tokens'
29
+
30
+ const IS_WEB = Platform.OS === 'web'
31
+ const STATE_COLLECTION = 'HelloJioInput State'
32
+
33
+ /** Default IconButton modes matching the Figma send control. */
34
+ const DEFAULT_SEND_BUTTON_MODES = Object.freeze({
35
+ AppearanceBrand: 'Primary',
36
+ 'Button / Size': 'S',
37
+ }) as Modes
38
+
39
+ const toNumber = (value: unknown, fallback: number): number => {
40
+ if (typeof value === 'number') {
41
+ return Number.isFinite(value) ? value : fallback
42
+ }
43
+ if (typeof value === 'string') {
44
+ const parsed = Number(value)
45
+ return Number.isFinite(parsed) ? parsed : fallback
46
+ }
47
+ return fallback
48
+ }
49
+
50
+ const toFontWeight = (
51
+ value: unknown,
52
+ fallback: TextStyle['fontWeight']
53
+ ): TextStyle['fontWeight'] => {
54
+ if (typeof value === 'number') return String(value) as TextStyle['fontWeight']
55
+ if (typeof value === 'string') return value as TextStyle['fontWeight']
56
+ return fallback
57
+ }
58
+
59
+ type VisualState = 'Idle' | 'Active' | 'IdleJioPlus'
60
+
61
+ /**
62
+ * Fallback values from the Figma `HelloJioInput` component when local token
63
+ * JSON has not been synced yet. Mode-driven `getVariableByName` wins when
64
+ * tokens resolve.
65
+ */
66
+ const STATE_FALLBACKS: Record<
67
+ VisualState,
68
+ {
69
+ background: string
70
+ borderColor: string
71
+ labelColor: string
72
+ iconColor: string
73
+ }
74
+ > = {
75
+ Idle: {
76
+ background: '#f5f5f5',
77
+ borderColor: '#f5f5f5',
78
+ labelColor: '#545961',
79
+ iconColor: '#545961',
80
+ },
81
+ Active: {
82
+ background: '#ffffff',
83
+ borderColor: '#b5b5b5',
84
+ labelColor: '#24262b',
85
+ iconColor: '#24262b',
86
+ },
87
+ IdleJioPlus: {
88
+ background: '#f5f5f5',
89
+ borderColor: '#f5f5f5',
90
+ labelColor: '#545961',
91
+ iconColor: '#545961',
92
+ },
93
+ }
94
+
95
+ export type HelloJioInputProps = {
96
+ /** Placeholder shown when empty. Defaults to `"Ask me anything"`. */
97
+ placeholder?: string
98
+ /** Controlled value. Pair with `onChangeText`. */
99
+ value?: string
100
+ /** Uncontrolled initial value. Ignored when `value` is provided. */
101
+ defaultValue?: string
102
+ /** Called whenever the text changes. */
103
+ onChangeText?: (text: string) => void
104
+ /**
105
+ * Called when the user submits — either via the send button or the keyboard
106
+ * return key. Receives the current text. This is the primary action hook.
107
+ */
108
+ onSubmit?: (text: string) => void
109
+ /**
110
+ * Leading icon name from the registry. Defaults to `"ic_hellojio"`.
111
+ * Ignored when `leading` is provided.
112
+ */
113
+ leadingIconName?: string
114
+ /**
115
+ * Slot replacing the default leading icon (Figma Slot "start"). Receives
116
+ * `modes` recursively.
117
+ */
118
+ leading?: React.ReactNode
119
+ /**
120
+ * Slot replacing the default send `IconButton` (Figma Slot "end").
121
+ * Pass `null` to hide the trailing control entirely.
122
+ */
123
+ trailing?: React.ReactNode | null
124
+ /**
125
+ * Icon name for the default send button. Defaults to `"ic_send_message"`.
126
+ * Ignored when `trailing` is provided.
127
+ */
128
+ sendIconName?: string
129
+ /**
130
+ * When `true`, renders the IdleJioPlus glass surface while unfocused.
131
+ * Focus still switches to the Active (solid white + border) state.
132
+ */
133
+ jioPlus?: boolean
134
+ /** Disables typing and the send button. */
135
+ disabled?: boolean
136
+ /** Design token modes. Visual Idle/Active/IdleJioPlus is derived from focus + `jioPlus`. */
137
+ modes?: Modes
138
+ /** Container style override. */
139
+ style?: StyleProp<ViewStyle>
140
+ /** Text input style override. */
141
+ inputStyle?: StyleProp<TextStyle>
142
+ accessibilityLabel?: string
143
+ accessibilityHint?: string
144
+ testID?: string
145
+ onFocus?: RNTextInputProps['onFocus']
146
+ onBlur?: RNTextInputProps['onBlur']
147
+ } & Omit<
148
+ RNTextInputProps,
149
+ | 'style'
150
+ | 'value'
151
+ | 'defaultValue'
152
+ | 'onChangeText'
153
+ | 'onFocus'
154
+ | 'onBlur'
155
+ | 'placeholder'
156
+ | 'editable'
157
+ | 'onSubmitEditing'
158
+ >
159
+
160
+ interface HelloJioInputTokens {
161
+ containerStyle: ViewStyle
162
+ inputStyle: TextStyle
163
+ iconColor: string
164
+ iconSize: number
165
+ blurIntensity: number
166
+ useGlass: boolean
167
+ }
168
+
169
+ function resolveHelloJioInputTokens(
170
+ modes: Modes,
171
+ visualState: VisualState
172
+ ): HelloJioInputTokens {
173
+ const fallback = STATE_FALLBACKS[visualState]
174
+
175
+ const background =
176
+ (getVariableByName('helloJioInput/background', modes) as string | null) ??
177
+ fallback.background
178
+ const borderColor =
179
+ (getVariableByName('helloJioInput/border/color', modes) as string | null) ??
180
+ fallback.borderColor
181
+ const strokeWidth = toNumber(
182
+ getVariableByName('helloJioInput/strokeWidth', modes),
183
+ 1
184
+ )
185
+ const gap = toNumber(getVariableByName('helloJioInput/gap', modes), 8)
186
+ const paddingLeft = toNumber(
187
+ getVariableByName('helloJioInput/padding/left', modes),
188
+ 12
189
+ )
190
+ const paddingRight = toNumber(
191
+ getVariableByName('helloJioInput/padding/right', modes),
192
+ 4
193
+ )
194
+ const paddingTop = toNumber(
195
+ getVariableByName('helloJioInput/padding/top', modes),
196
+ 4
197
+ )
198
+ const paddingBottom = toNumber(
199
+ getVariableByName('helloJioInput/padding/bottom', modes),
200
+ 4
201
+ )
202
+ const height = toNumber(getVariableByName('helloJioInput/height', modes), 36)
203
+ const radiusRaw = toNumber(
204
+ getVariableByName('helloJioInput/radius', modes),
205
+ 99999
206
+ )
207
+ const borderRadius = radiusRaw >= 9999 ? 99999 : radiusRaw
208
+
209
+ const labelColor =
210
+ (getVariableByName('helloJioInput/label/color', modes) as string | null) ??
211
+ fallback.labelColor
212
+ const fontSize = toNumber(
213
+ getVariableByName('helloJioInput/fontSize', modes),
214
+ 14
215
+ )
216
+ const lineHeight = toNumber(
217
+ getVariableByName('helloJioInput/lineHeight', modes),
218
+ 18
219
+ )
220
+ const fontFamily =
221
+ (getVariableByName('helloJioInput/fontFamily', modes) as string | null) ??
222
+ 'JioType Var'
223
+ const fontWeight = toFontWeight(
224
+ getVariableByName('helloJioInput/fontWeight', modes),
225
+ '400'
226
+ )
227
+
228
+ const iconColor =
229
+ (getVariableByName('helloJioInput/icon/color', modes) as string | null) ??
230
+ fallback.iconColor
231
+ const iconSize = toNumber(
232
+ getVariableByName('textInput/icon/size', modes) ??
233
+ getVariableByName('helloJioInput/icon/size', modes),
234
+ 18
235
+ )
236
+
237
+ const blurMinimal = toNumber(getVariableByName('blur/minimal', modes), 29)
238
+ const blurIntensity = Math.max(0, Math.min(100, Math.round(blurMinimal)))
239
+ const useGlass = visualState === 'IdleJioPlus'
240
+
241
+ return {
242
+ containerStyle: {
243
+ position: 'relative',
244
+ flexDirection: 'row',
245
+ alignItems: 'center',
246
+ // Glass keeps the container transparent so GlassFill shows through.
247
+ backgroundColor: useGlass ? 'transparent' : background,
248
+ borderColor,
249
+ borderWidth: strokeWidth,
250
+ borderStyle: 'solid',
251
+ borderRadius,
252
+ paddingLeft,
253
+ paddingRight,
254
+ paddingTop,
255
+ paddingBottom,
256
+ gap,
257
+ height,
258
+ overflow: 'hidden',
259
+ width: '100%',
260
+ },
261
+ inputStyle: {
262
+ flex: 1,
263
+ color: labelColor,
264
+ fontSize,
265
+ fontFamily,
266
+ fontWeight,
267
+ // iOS single-line TextInput + explicit lineHeight sits below center —
268
+ // same quirk TextInput works around. Omit on iOS.
269
+ ...(Platform.OS === 'ios' ? null : { lineHeight }),
270
+ padding: 0,
271
+ margin: 0,
272
+ minHeight: lineHeight,
273
+ outlineStyle: 'none' as any,
274
+ outlineWidth: 0,
275
+ outlineColor: 'transparent',
276
+ },
277
+ iconColor,
278
+ iconSize,
279
+ blurIntensity,
280
+ useGlass,
281
+ }
282
+ }
283
+
284
+ /**
285
+ * `HelloJioInput` is the pill-shaped HelloJio prompt field: leading brand
286
+ * icon, single-line text input, and a gold send `IconButton` on the right.
287
+ *
288
+ * Visual state is derived automatically:
289
+ * - **Idle** — gray fill, gray label (default)
290
+ * - **Active** — white fill + border while focused
291
+ * - **IdleJioPlus** — frosted glass when `jioPlus` and unfocused
292
+ *
293
+ * @component
294
+ */
295
+ const HelloJioInput = forwardRef<RNTextInput, HelloJioInputProps>(
296
+ function HelloJioInput(
297
+ {
298
+ placeholder = 'Ask me anything',
299
+ value,
300
+ defaultValue = '',
301
+ onChangeText,
302
+ onSubmit,
303
+ leadingIconName = 'ic_hellojio',
304
+ leading,
305
+ trailing,
306
+ sendIconName = 'ic_send_message',
307
+ jioPlus = false,
308
+ disabled = false,
309
+ modes: propModes = EMPTY_MODES,
310
+ style,
311
+ inputStyle,
312
+ accessibilityLabel,
313
+ accessibilityHint,
314
+ testID,
315
+ onFocus,
316
+ onBlur,
317
+ returnKeyType = 'send',
318
+ ...rest
319
+ },
320
+ ref
321
+ ) {
322
+ const { modes: globalModes } = useTokens()
323
+ const inputRef = useRef<RNTextInput>(null)
324
+ const [isFocused, setIsFocused] = useState(false)
325
+ const [isHovered, setIsHovered] = useState(false)
326
+ const [uncontrolledValue, setUncontrolledValue] = useState(defaultValue)
327
+
328
+ const isControlled = value !== undefined
329
+ const currentValue = isControlled ? value : uncontrolledValue
330
+
331
+ const visualState: VisualState = isFocused
332
+ ? 'Active'
333
+ : jioPlus
334
+ ? 'IdleJioPlus'
335
+ : 'Idle'
336
+
337
+ const modes = useMemo(
338
+ () => ({
339
+ ...globalModes,
340
+ ...propModes,
341
+ [STATE_COLLECTION]: visualState,
342
+ }),
343
+ [globalModes, propModes, visualState]
344
+ )
345
+
346
+ const sendButtonModes = useMemo(
347
+ () => ({ ...DEFAULT_SEND_BUTTON_MODES, ...modes }),
348
+ [modes]
349
+ )
350
+
351
+ const tokens = useMemo(
352
+ () => resolveHelloJioInputTokens(modes, visualState),
353
+ [modes, visualState]
354
+ )
355
+
356
+ const handleChangeText = useCallback(
357
+ (text: string) => {
358
+ if (!isControlled) setUncontrolledValue(text)
359
+ onChangeText?.(text)
360
+ },
361
+ [isControlled, onChangeText]
362
+ )
363
+
364
+ const handleSubmit = useCallback(() => {
365
+ if (disabled) return
366
+ onSubmit?.(currentValue)
367
+ }, [disabled, onSubmit, currentValue])
368
+
369
+ const handleFocus = useCallback(
370
+ (e: any) => {
371
+ setIsFocused(true)
372
+ onFocus?.(e)
373
+ },
374
+ [onFocus]
375
+ )
376
+
377
+ const handleBlur = useCallback(
378
+ (e: any) => {
379
+ setIsFocused(false)
380
+ onBlur?.(e)
381
+ },
382
+ [onBlur]
383
+ )
384
+
385
+ const leadingElement =
386
+ leading ??
387
+ (leadingIconName ? (
388
+ <Icon
389
+ name={leadingIconName}
390
+ size={tokens.iconSize}
391
+ color={tokens.iconColor}
392
+ />
393
+ ) : null)
394
+
395
+ const processedLeading = leadingElement
396
+ ? cloneChildrenWithModes(React.Children.toArray(leadingElement), modes)
397
+ : null
398
+
399
+ const defaultTrailing = (
400
+ <IconButton
401
+ iconName={sendIconName}
402
+ modes={sendButtonModes}
403
+ onPress={handleSubmit}
404
+ disabled={disabled}
405
+ accessibilityLabel="Send"
406
+ />
407
+ )
408
+
409
+ const trailingElement =
410
+ trailing === undefined
411
+ ? defaultTrailing
412
+ : trailing === null
413
+ ? null
414
+ : trailing
415
+
416
+ const processedTrailing = trailingElement
417
+ ? cloneChildrenWithModes(React.Children.toArray(trailingElement), modes)
418
+ : null
419
+
420
+ const a11yLabel =
421
+ accessibilityLabel ?? (placeholder || 'HelloJio input')
422
+
423
+ const hoverStyle: ViewStyle =
424
+ isHovered && !disabled && !isFocused ? { opacity: 0.95 } : {}
425
+
426
+ const containerStyleArray: StyleProp<ViewStyle> = [
427
+ tokens.containerStyle,
428
+ hoverStyle,
429
+ disabled ? { opacity: 0.5 } : null,
430
+ style,
431
+ ]
432
+
433
+ const glassOverlay =
434
+ (getVariableByName('helloJioInput/background', modes) as string | null) ??
435
+ STATE_FALLBACKS.IdleJioPlus.background
436
+
437
+ const inner = (
438
+ <>
439
+ {tokens.useGlass ? (
440
+ <GlassFill
441
+ tint="light"
442
+ intensity={tokens.blurIntensity}
443
+ overlayColor={glassOverlay}
444
+ androidTintWash={false}
445
+ />
446
+ ) : null}
447
+ {processedLeading ? (
448
+ <View
449
+ accessibilityElementsHidden
450
+ importantForAccessibility="no"
451
+ style={SLOT_CENTER}
452
+ >
453
+ {processedLeading}
454
+ </View>
455
+ ) : null}
456
+ <RNTextInput
457
+ ref={mergeRefs(inputRef, ref)}
458
+ accessibilityLabel={a11yLabel}
459
+ accessibilityHint={accessibilityHint}
460
+ placeholder={isFocused ? '' : placeholder}
461
+ placeholderTextColor={tokens.inputStyle.color}
462
+ value={currentValue}
463
+ onChangeText={handleChangeText}
464
+ onFocus={handleFocus}
465
+ onBlur={handleBlur}
466
+ onSubmitEditing={handleSubmit}
467
+ returnKeyType={returnKeyType}
468
+ editable={!disabled}
469
+ style={[tokens.inputStyle, inputStyle]}
470
+ testID={testID}
471
+ {...rest}
472
+ />
473
+ {processedTrailing ? (
474
+ <View
475
+ // Keep the send button in the a11y tree — it has its own label.
476
+ style={SLOT_CENTER}
477
+ >
478
+ {processedTrailing}
479
+ </View>
480
+ ) : null}
481
+ </>
482
+ )
483
+
484
+ // Same Android focus rule as TextInput: never wrap native TextInput in
485
+ // Pressable on native (steals the first tap). Web keeps Pressable for
486
+ // hover + click-anywhere-to-focus.
487
+ if (IS_WEB) {
488
+ return (
489
+ <Pressable
490
+ style={containerStyleArray}
491
+ onHoverIn={() => setIsHovered(true)}
492
+ onHoverOut={() => setIsHovered(false)}
493
+ onPress={() => {
494
+ if (!disabled) inputRef.current?.focus()
495
+ }}
496
+ accessible={false}
497
+ disabled={disabled}
498
+ >
499
+ {inner}
500
+ </Pressable>
501
+ )
502
+ }
503
+
504
+ return <View style={containerStyleArray}>{inner}</View>
505
+ }
506
+ )
507
+
508
+ const SLOT_CENTER: ViewStyle = {
509
+ justifyContent: 'center',
510
+ alignItems: 'center',
511
+ }
512
+
513
+ export default React.memo(HelloJioInput)