jfs-components 0.1.28 → 0.1.32

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 (53) hide show
  1. package/CHANGELOG.md +28 -2
  2. package/lib/commonjs/components/CategoryCard/CategoryCard.js +264 -0
  3. package/lib/commonjs/components/CompareTable/CompareTable.js +247 -25
  4. package/lib/commonjs/components/ContentSheet/ContentSheet.js +91 -8
  5. package/lib/commonjs/components/CounterBadge/CounterBadge.js +78 -0
  6. package/lib/commonjs/components/FavoriteToggle/FavoriteToggle.js +180 -0
  7. package/lib/commonjs/components/HelloJioInput/HelloJioInput.js +287 -0
  8. package/lib/commonjs/components/MoneyValue/MoneyValue.js +179 -54
  9. package/lib/commonjs/components/PdpCcCard/PdpCcCard.js +7 -1
  10. package/lib/commonjs/components/Table/Table.js +27 -7
  11. package/lib/commonjs/components/ValueBackMetric/ValueBackMetric.js +219 -0
  12. package/lib/commonjs/components/index.js +35 -0
  13. package/lib/commonjs/design-tokens/figma-modes.generated.js +3 -0
  14. package/lib/commonjs/icons/registry.js +1 -1
  15. package/lib/module/components/CategoryCard/CategoryCard.js +258 -0
  16. package/lib/module/components/CompareTable/CompareTable.js +247 -26
  17. package/lib/module/components/ContentSheet/ContentSheet.js +94 -11
  18. package/lib/module/components/CounterBadge/CounterBadge.js +73 -0
  19. package/lib/module/components/FavoriteToggle/FavoriteToggle.js +174 -0
  20. package/lib/module/components/HelloJioInput/HelloJioInput.js +281 -0
  21. package/lib/module/components/MoneyValue/MoneyValue.js +182 -57
  22. package/lib/module/components/PdpCcCard/PdpCcCard.js +7 -1
  23. package/lib/module/components/Table/Table.js +27 -7
  24. package/lib/module/components/ValueBackMetric/ValueBackMetric.js +214 -0
  25. package/lib/module/components/index.js +6 -1
  26. package/lib/module/design-tokens/figma-modes.generated.js +3 -0
  27. package/lib/module/icons/registry.js +1 -1
  28. package/lib/typescript/src/components/CategoryCard/CategoryCard.d.ts +72 -0
  29. package/lib/typescript/src/components/CompareTable/CompareTable.d.ts +36 -1
  30. package/lib/typescript/src/components/ContentSheet/ContentSheet.d.ts +20 -1
  31. package/lib/typescript/src/components/CounterBadge/CounterBadge.d.ts +19 -0
  32. package/lib/typescript/src/components/FavoriteToggle/FavoriteToggle.d.ts +66 -0
  33. package/lib/typescript/src/components/HelloJioInput/HelloJioInput.d.ts +111 -0
  34. package/lib/typescript/src/components/MoneyValue/MoneyValue.d.ts +10 -1
  35. package/lib/typescript/src/components/PdpCcCard/PdpCcCard.d.ts +25 -1
  36. package/lib/typescript/src/components/Table/Table.d.ts +9 -1
  37. package/lib/typescript/src/components/ValueBackMetric/ValueBackMetric.d.ts +86 -0
  38. package/lib/typescript/src/components/index.d.ts +5 -0
  39. package/lib/typescript/src/icons/registry.d.ts +1 -1
  40. package/package.json +1 -1
  41. package/src/components/CategoryCard/CategoryCard.tsx +404 -0
  42. package/src/components/CompareTable/CompareTable.tsx +324 -30
  43. package/src/components/ContentSheet/ContentSheet.tsx +120 -11
  44. package/src/components/CounterBadge/CounterBadge.tsx +95 -0
  45. package/src/components/FavoriteToggle/FavoriteToggle.tsx +243 -0
  46. package/src/components/HelloJioInput/HelloJioInput.tsx +513 -0
  47. package/src/components/MoneyValue/MoneyValue.tsx +216 -65
  48. package/src/components/PdpCcCard/PdpCcCard.tsx +36 -1
  49. package/src/components/Table/Table.tsx +29 -4
  50. package/src/components/ValueBackMetric/ValueBackMetric.tsx +298 -0
  51. package/src/components/index.ts +5 -0
  52. package/src/design-tokens/figma-modes.generated.ts +3 -0
  53. package/src/icons/registry.ts +1 -1
@@ -0,0 +1,95 @@
1
+ import React from 'react'
2
+ import {
3
+ View,
4
+ Text,
5
+ type ViewStyle,
6
+ type TextStyle,
7
+ } from 'react-native'
8
+ import { getVariableByName } from '../../design-tokens/figma-variables-resolver'
9
+ import { EMPTY_MODES } from '../../utils/react-utils'
10
+ import type { Modes } from '../../design-tokens'
11
+
12
+ export type CounterBadgeProps = {
13
+ /** The value shown inside the badge. */
14
+ value?: string
15
+ /** Modes used to resolve design tokens. Counter Badge exposes a single `Default` mode. */
16
+ modes?: Modes
17
+ style?: ViewStyle
18
+ } & Omit<React.ComponentProps<typeof View>, 'style'>
19
+
20
+ /**
21
+ * Counter Badge — a small token-driven pill that displays a value.
22
+ *
23
+ * All visual attributes resolve from the Figma `counterBadge/*` tokens via
24
+ * `getVariableByName`.
25
+ */
26
+ function CounterBadge({
27
+ value = '99',
28
+ modes = EMPTY_MODES,
29
+ style,
30
+ ...rest
31
+ }: CounterBadgeProps) {
32
+ // Resolve token values (fall back to the Figma defaults for the single
33
+ // `Default` mode so the badge still renders if a token is missing).
34
+ const backgroundColor =
35
+ (getVariableByName('counterBadge/background', modes) as string) ?? '#ebebec'
36
+ const foreground =
37
+ (getVariableByName('counterBadge/foreground', modes) as string) ?? '#0c0d10'
38
+ const fontFamily =
39
+ (getVariableByName('counterBadge/fontFamily', modes) as string) ?? 'JioType Var'
40
+ const fontWeightRaw =
41
+ getVariableByName('counterBadge/fontWeight', modes) ?? 500
42
+ const fontWeight =
43
+ typeof fontWeightRaw === 'number' ? String(fontWeightRaw) : (fontWeightRaw as string)
44
+ const fontSize =
45
+ Number(getVariableByName('counterBadge/fontSize', modes) ?? 8)
46
+ const lineHeight =
47
+ Number(getVariableByName('counterBadge/lineHeight', modes) ?? 8)
48
+ const paddingHorizontal =
49
+ Number(getVariableByName('counterBadge/padding/horizontal', modes) ?? 4)
50
+ // The Figma design pins vertical padding to 4px (matching horizontal), but the
51
+ // committed variables snapshot resolves `counterBadge/padding/vertical` to 5px
52
+ // (a stale export). Figma is the source of truth here, so hardcode 4px rather
53
+ // than trust the token, keeping the badge one pixel shorter to match the design.
54
+ const paddingVertical = 4
55
+ const borderRadius =
56
+ Number(getVariableByName('counterBadge/radius', modes) ?? 999)
57
+
58
+ const containerStyle: ViewStyle = {
59
+ backgroundColor,
60
+ paddingHorizontal,
61
+ paddingVertical,
62
+ borderRadius,
63
+ alignSelf: 'flex-start',
64
+ alignItems: 'center',
65
+ justifyContent: 'center',
66
+ ...((style as any) || {}),
67
+ }
68
+
69
+ const textStyle: TextStyle = {
70
+ color: foreground,
71
+ fontFamily,
72
+ fontWeight: fontWeight as TextStyle['fontWeight'],
73
+ fontSize,
74
+ lineHeight,
75
+ textAlign: 'center',
76
+ }
77
+
78
+ return (
79
+ <View style={containerStyle} {...rest}>
80
+ {/* Figma `wrap`: a centered box with a 10px minimum. */}
81
+ <View style={WRAP_STYLE}>
82
+ <Text style={textStyle}>{value}</Text>
83
+ </View>
84
+ </View>
85
+ )
86
+ }
87
+
88
+ // Figma `wrap` node (data-node-id 7701:10879): min-width 10, centered.
89
+ const WRAP_STYLE: ViewStyle = {
90
+ minWidth: 10,
91
+ alignItems: 'center',
92
+ justifyContent: 'center',
93
+ }
94
+
95
+ export default CounterBadge
@@ -0,0 +1,243 @@
1
+ import React, { useCallback, useMemo, useState } from 'react'
2
+ import {
3
+ Pressable,
4
+ Platform,
5
+ type AccessibilityState,
6
+ type PressableStateCallbackType,
7
+ type StyleProp,
8
+ type ViewStyle,
9
+ } from 'react-native'
10
+ import { getVariableByName } from '../../design-tokens/figma-variables-resolver'
11
+ import Icon from '../../icons/Icon'
12
+ import GlassFill from '../../utils/GlassFill/GlassFill'
13
+ import {
14
+ usePressableWebSupport,
15
+ type SafePressableProps,
16
+ type WebAccessibilityProps,
17
+ } from '../../utils/web-platform-utils'
18
+ import { EMPTY_MODES } from '../../utils/react-utils'
19
+ import Skeleton from '../../skeleton/Skeleton'
20
+ import { useSkeleton } from '../../skeleton/SkeletonGroup'
21
+ import type { Modes } from '../../design-tokens'
22
+
23
+ export type FavoriteToggleProps = SafePressableProps & {
24
+ /**
25
+ * Whether the toggle is favorited (active) — **controlled**. When provided,
26
+ * the component reflects this value and defers state ownership to the parent
27
+ * (update it in `onChange`). Omit it to use the component uncontrolled with
28
+ * `defaultActive`.
29
+ */
30
+ isActive?: boolean
31
+ /**
32
+ * Initial favorited state for **uncontrolled** usage. Ignored when `isActive`
33
+ * is provided. Defaults to `false`.
34
+ */
35
+ defaultActive?: boolean
36
+ /**
37
+ * Icon rendered inside the toggle. Defaults to the heart glyph
38
+ * (`ic_favorite`). Both Figma variants use the same filled heart; only its
39
+ * color changes with state (white when inactive, gold when active). Figma
40
+ * hardcodes the heart, so this prop is exposed purely for future reuse.
41
+ */
42
+ icon?: string
43
+ /** Fired on press with the next (toggled) active state. */
44
+ onChange?: (next: boolean) => void
45
+ /** Raw press handler, fired alongside `onChange`. */
46
+ onPress?: () => void
47
+ /** Modes used to resolve design tokens. */
48
+ modes?: Modes
49
+ disabled?: boolean
50
+ /** Accessibility label; defaults to `"Favorite"`. */
51
+ accessibilityLabel?: string
52
+ accessibilityHint?: string
53
+ accessibilityState?: AccessibilityState
54
+ webAccessibilityProps?: WebAccessibilityProps
55
+ style?: StyleProp<ViewStyle>
56
+ /**
57
+ * Explicit per-instance loading override. When `true`, renders a same-size
58
+ * circular skeleton instead of the toggle. Defaults to inheriting from the
59
+ * surrounding `<SkeletonGroup>`.
60
+ */
61
+ loading?: boolean
62
+ }
63
+
64
+ const IS_WEB = Platform.OS === 'web'
65
+ const IS_IOS = Platform.OS === 'ios'
66
+ const PRESS_DELAY = IS_IOS ? 130 : 0
67
+ const pressedOverlayStyle: ViewStyle = { opacity: 0.7 }
68
+
69
+ interface FavoriteToggleTokens {
70
+ size: number
71
+ borderRadius: number
72
+ backgroundColor: string
73
+ iconColor: string
74
+ iconSize: number
75
+ blurIntensity: number
76
+ }
77
+
78
+ // The `favoriteToggle/*` tokens are not yet in the committed variables snapshot,
79
+ // so `getVariableByName` returns undefined and these Figma-node values are used
80
+ // as fallbacks. Once the token export is re-synced the resolver takes over.
81
+ function resolveTokens(modes: Modes, isActive: boolean): FavoriteToggleTokens {
82
+ const width = Number(getVariableByName('favoriteToggle/width', modes) ?? 29)
83
+ const height = Number(getVariableByName('favoriteToggle/height', modes) ?? 29)
84
+ const radiusRaw = Number(getVariableByName('favoriteToggle/icon/radius', modes) ?? 9999)
85
+ const iconSize = Number(getVariableByName('favoriteToggle/icon/width', modes) ?? 20)
86
+ // 9999 is the design-token sentinel for "perfect circle".
87
+ const size = Math.max(width, height)
88
+ const borderRadius = radiusRaw >= 9999 ? size / 2 : radiusRaw
89
+
90
+ // Background + icon color differ per variant. In Figma this is driven by the
91
+ // `isActive` collection (modes `'False'` / `'True'`), so we pass that mode
92
+ // through by its string name. Until the `color/favoriteToggle/*` tokens are
93
+ // exported the resolver returns undefined and the fallbacks below — which
94
+ // encode the two Figma variants directly — take over.
95
+ const isActiveMode = { ...modes, isActive: isActive ? 'True' : 'False' }
96
+ const backgroundColor =
97
+ (getVariableByName('color/favoriteToggle/background/color', isActiveMode) as string) ??
98
+ (isActive ? '#ffffff' : '#ffffff33')
99
+ const iconColor =
100
+ (getVariableByName('color/favoriteToggle/icon/color', isActiveMode) as string) ??
101
+ (isActive ? '#cea15a' : '#ffffff')
102
+
103
+ // Figma `blur/minimal` (px radius) -> GlassFill's 0-100 intensity scale, the
104
+ // same mapping Badge's glass path uses.
105
+ const blurRaw = Number(getVariableByName('blur/minimal', modes) ?? 29)
106
+ const blurIntensity = Math.max(0, Math.min(100, Math.round(blurRaw)))
107
+
108
+ return { size, borderRadius, backgroundColor, iconColor, iconSize, blurIntensity }
109
+ }
110
+
111
+ /**
112
+ * Circular glass favorite (heart) toggle, mirroring the Figma `Favorite Toggle`
113
+ * component set's boolean `isActive` variant:
114
+ * - inactive: translucent frosted circle (`#ffffff33` over a `blur/minimal`
115
+ * backdrop) with a white heart.
116
+ * - active: solid white circle with a gold (`#cea15a`) heart.
117
+ * Both variants share the identical filled-heart glyph; only the background and
118
+ * icon colors change between states.
119
+ *
120
+ * Controlled or uncontrolled: pass `isActive` (+ `onChange`) to control it, or
121
+ * omit `isActive` and use `defaultActive` to let the component own the state.
122
+ *
123
+ * Visual attributes resolve from the `favoriteToggle/*` / `color/favoriteToggle/*`
124
+ * tokens via `getVariableByName`. Those tokens are not yet in the committed
125
+ * variables snapshot, so the component currently runs on the Figma-node values
126
+ * as fallbacks; once the tokens are exported (under the `isActive` collection,
127
+ * modes `False`/`True`) the resolver takes over.
128
+ */
129
+ function FavoriteToggle({
130
+ isActive: controlledActive,
131
+ defaultActive = false,
132
+ icon = 'ic_favorite',
133
+ onChange,
134
+ onPress,
135
+ modes = EMPTY_MODES,
136
+ disabled = false,
137
+ accessibilityLabel,
138
+ accessibilityHint,
139
+ accessibilityState,
140
+ webAccessibilityProps,
141
+ style,
142
+ loading,
143
+ ...rest
144
+ }: FavoriteToggleProps) {
145
+ // Controlled when `isActive` is supplied; otherwise own the state internally
146
+ // (mirrors the Toggle / SegmentedControl controlled-or-uncontrolled pattern).
147
+ const isControlled = controlledActive !== undefined
148
+ const [internalActive, setInternalActive] = useState(defaultActive)
149
+ const isActive = isControlled ? (controlledActive as boolean) : internalActive
150
+
151
+ const tokens = useMemo(() => resolveTokens(modes, isActive), [modes, isActive])
152
+
153
+ const { active: groupActive } = useSkeleton()
154
+ const isLoading = loading ?? groupActive
155
+
156
+ const handlePress = useCallback(() => {
157
+ const next = !isActive
158
+ if (!isControlled) {
159
+ setInternalActive(next)
160
+ }
161
+ onChange?.(next)
162
+ onPress?.()
163
+ }, [isActive, isControlled, onChange, onPress])
164
+
165
+ const label = accessibilityLabel || 'Favorite'
166
+
167
+ const webProps = usePressableWebSupport({
168
+ restProps: rest,
169
+ onPress: disabled ? undefined : handlePress,
170
+ disabled,
171
+ accessibilityLabel: label,
172
+ webAccessibilityProps,
173
+ })
174
+
175
+ const baseContainerStyle: ViewStyle = {
176
+ width: tokens.size,
177
+ height: tokens.size,
178
+ borderRadius: tokens.borderRadius,
179
+ // Active is opaque white, so no glass is drawn and the background paints
180
+ // directly. Inactive keeps the container transparent so the GlassFill blur
181
+ // (clipped by overflow:hidden) shows through.
182
+ backgroundColor: isActive ? tokens.backgroundColor : 'transparent',
183
+ overflow: 'hidden',
184
+ alignItems: 'center',
185
+ justifyContent: 'center',
186
+ opacity: disabled ? 0.5 : 1,
187
+ }
188
+
189
+ // Declared before the loading short-circuit so hook order stays stable.
190
+ const styleCallback = useCallback(
191
+ ({ pressed }: PressableStateCallbackType): StyleProp<ViewStyle> => [
192
+ baseContainerStyle,
193
+ style,
194
+ pressed && !disabled ? pressedOverlayStyle : null,
195
+ ],
196
+ [baseContainerStyle, style, disabled]
197
+ )
198
+
199
+ if (isLoading) {
200
+ return (
201
+ <Skeleton
202
+ kind="other"
203
+ width={tokens.size}
204
+ height={tokens.size}
205
+ style={[{ borderRadius: tokens.borderRadius }, style as any]}
206
+ modes={modes}
207
+ />
208
+ )
209
+ }
210
+
211
+ return (
212
+ <Pressable
213
+ accessibilityRole={IS_WEB ? ('switch' as any) : 'button'}
214
+ accessibilityLabel={undefined}
215
+ accessibilityHint={accessibilityHint}
216
+ accessibilityState={{ disabled, checked: isActive, ...accessibilityState }}
217
+ onPress={disabled ? undefined : handlePress}
218
+ disabled={disabled}
219
+ unstable_pressDelay={PRESS_DELAY}
220
+ style={styleCallback}
221
+ {...webProps}
222
+ >
223
+ {/* Inactive: translucent frosted glass behind the icon. */}
224
+ {!isActive ? (
225
+ <GlassFill
226
+ tint="light"
227
+ intensity={tokens.blurIntensity}
228
+ overlayColor={tokens.backgroundColor}
229
+ androidTintWash={false}
230
+ />
231
+ ) : null}
232
+ <Icon
233
+ name={icon}
234
+ size={tokens.iconSize}
235
+ color={tokens.iconColor}
236
+ accessibilityElementsHidden
237
+ importantForAccessibility="no"
238
+ />
239
+ </Pressable>
240
+ )
241
+ }
242
+
243
+ export default React.memo(FavoriteToggle)