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
@@ -1,13 +1,25 @@
1
1
  "use strict";
2
2
 
3
- import React, { useContext, useMemo } from 'react';
4
- import { Platform, Text, View } from 'react-native';
5
- import Animated, { useAnimatedKeyboard, useAnimatedStyle } from 'react-native-reanimated';
3
+ import React, { useContext, useEffect, useMemo } from 'react';
4
+ import { Platform, ScrollView, Text, View, useWindowDimensions } from 'react-native';
5
+ import Animated, { useAnimatedKeyboard, useAnimatedStyle, useSharedValue, withSpring } from 'react-native-reanimated';
6
6
  import { SafeAreaInsetsContext } from 'react-native-safe-area-context';
7
7
  import { getVariableByName } from '../../design-tokens/figma-variables-resolver';
8
8
  import { EMPTY_MODES, cloneChildrenWithModes, flattenChildren } from '../../utils/react-utils';
9
9
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
10
10
  const IS_WEB = Platform.OS === 'web';
11
+
12
+ // Spring config — mirrors the Drawer's bottom pop-up spring so the two
13
+ // bottom-sheet surfaces share the exact same entrance feel (slight bounce,
14
+ // snappy). Keep in sync with `Drawer.tsx` SPRING_CONFIG.
15
+ const SPRING_CONFIG = {
16
+ damping: 32,
17
+ stiffness: 300,
18
+ mass: 1,
19
+ overshootClamping: false,
20
+ restDisplacementThreshold: 0.1,
21
+ restSpeedThreshold: 0.1
22
+ };
11
23
  function resolveContentSheetStyle(modes) {
12
24
  const backgroundColor = getVariableByName('contentSheet/bg', modes);
13
25
  const gap = getVariableByName('contentSheet/gap', modes);
@@ -91,6 +103,8 @@ function ContentSheet({
91
103
  keyboardSpacing = 0,
92
104
  safeAreaBottom = true,
93
105
  pinToBottom = true,
106
+ visible = true,
107
+ maxHeightPercent = 0.7,
94
108
  style,
95
109
  ...rest
96
110
  }) {
@@ -102,14 +116,31 @@ function ContentSheet({
102
116
  const resolved = useMemo(() => resolveContentSheetStyle(modes), [modes]);
103
117
  const processedChildren = useMemo(() => cloneChildrenWithModes(flattenChildren(children), modes), [children, modes]);
104
118
  const paddingBottom = resolved.paddingBottom + (safeAreaBottom ? bottomInset : 0);
119
+
120
+ // Entrance / exit "pop up from bottom" animation — mirrors the Drawer. The
121
+ // sheet is bottom-anchored, so a positive `translateY` equal to the window
122
+ // height parks it fully below the bottom edge. Springing it back to `0`
123
+ // produces the same bottom pop-up the Drawer uses. The shared value is
124
+ // initialised off-screen so the very first mount pops in (no flash).
125
+ const {
126
+ height: windowHeight
127
+ } = useWindowDimensions();
128
+ const maxHeight = windowHeight * maxHeightPercent;
105
129
  const containerStyle = useMemo(() => [pinToBottom ? pinnedStyle : null, resolved.container, {
130
+ maxHeight,
106
131
  paddingBottom
107
- }, style], [pinToBottom, resolved.container, paddingBottom, style]);
132
+ }, style], [pinToBottom, resolved.container, maxHeight, paddingBottom, style]);
133
+ const entrance = useSharedValue(windowHeight);
134
+ useEffect(() => {
135
+ entrance.value = withSpring(visible ? 0 : windowHeight, SPRING_CONFIG);
136
+ }, [visible, windowHeight, entrance]);
108
137
 
109
138
  // Switching between the keyboard-aware and plain implementation is keyed off
110
139
  // `avoidKeyboard` (and platform). Because they are distinct component types,
111
140
  // React remounts on toggle, so the conditional `useAnimatedKeyboard` hook
112
- // inside `KeyboardAwareSheet` always runs in a stable hook order.
141
+ // inside `KeyboardAwareSheet` always runs in a stable hook order. Both
142
+ // branches receive the `entrance` shared value so the bottom pop-up plays in
143
+ // every configuration.
113
144
  const header = title ? /*#__PURE__*/_jsx(View, {
114
145
  style: resolved.headerStyle,
115
146
  children: /*#__PURE__*/_jsx(Text, {
@@ -121,12 +152,16 @@ function ContentSheet({
121
152
  return /*#__PURE__*/_jsxs(KeyboardAwareSheet, {
122
153
  style: containerStyle,
123
154
  spacing: keyboardSpacing,
155
+ entrance: entrance,
156
+ maxHeight: maxHeight,
124
157
  ...rest,
125
158
  children: [header, processedChildren]
126
159
  });
127
160
  }
128
- return /*#__PURE__*/_jsxs(View, {
161
+ return /*#__PURE__*/_jsxs(AnimatedSheet, {
129
162
  style: containerStyle,
163
+ entrance: entrance,
164
+ maxHeight: maxHeight,
130
165
  ...rest,
131
166
  children: [header, processedChildren]
132
167
  });
@@ -134,27 +169,75 @@ function ContentSheet({
134
169
  /**
135
170
  * Native-only wrapper that lifts the sheet by the live keyboard height. The
136
171
  * translation is computed on the UI thread from `useAnimatedKeyboard`, so the
137
- * sheet tracks the keyboard frame-for-frame without any JS work.
172
+ * sheet tracks the keyboard frame-for-frame without any JS work. The keyboard
173
+ * offset is summed with the entrance `translateY` (the bottom pop-up) inside a
174
+ * single `useAnimatedStyle` so both motions stay on the UI thread and never
175
+ * fight each other.
138
176
  */
139
177
  function KeyboardAwareSheet({
140
178
  style,
141
179
  spacing,
180
+ entrance,
181
+ maxHeight,
142
182
  children,
143
183
  ...rest
144
184
  }) {
145
185
  const keyboard = useAnimatedKeyboard();
146
- const keyboardStyle = useAnimatedStyle(() => {
186
+ const animatedStyle = useAnimatedStyle(() => {
147
187
  const height = keyboard.height.value;
188
+ const keyboardOffset = height > 0 ? -(height + spacing) : 0;
148
189
  return {
149
190
  transform: [{
150
- translateY: height > 0 ? -(height + spacing) : 0
191
+ translateY: entrance.value + keyboardOffset
151
192
  }]
152
193
  };
153
194
  });
154
195
  return /*#__PURE__*/_jsx(Animated.View, {
155
- style: [style, keyboardStyle],
196
+ style: [style, animatedStyle],
156
197
  ...rest,
157
- children: children
198
+ children: /*#__PURE__*/_jsx(ScrollView, {
199
+ style: {
200
+ maxHeight
201
+ },
202
+ bounces: false,
203
+ showsVerticalScrollIndicator: false,
204
+ keyboardShouldPersistTaps: "handled",
205
+ children: children
206
+ })
207
+ });
208
+ }
209
+
210
+ /**
211
+ * Entrance-only animated sheet (web, or when keyboard avoidance is disabled).
212
+ * Applies the same bottom pop-up `translateY` as `KeyboardAwareSheet`, minus
213
+ * the keyboard offset.
214
+ */
215
+ function AnimatedSheet({
216
+ style,
217
+ entrance,
218
+ maxHeight,
219
+ children,
220
+ ...rest
221
+ }) {
222
+ const animatedStyle = useAnimatedStyle(() => {
223
+ return {
224
+ transform: [{
225
+ translateY: entrance.value
226
+ }]
227
+ };
228
+ });
229
+ return /*#__PURE__*/_jsx(Animated.View, {
230
+ style: [style, animatedStyle],
231
+ ...rest,
232
+ children: /*#__PURE__*/_jsx(ScrollView, {
233
+ style: {
234
+ maxHeight
235
+ },
236
+ bounces: false,
237
+ showsVerticalScrollIndicator: false,
238
+ keyboardShouldPersistTaps: "handled",
239
+ children: children
240
+ })
158
241
  });
159
242
  }
160
243
  export default ContentSheet;
@@ -0,0 +1,73 @@
1
+ "use strict";
2
+
3
+ import React from 'react';
4
+ import { View, Text } from 'react-native';
5
+ import { getVariableByName } from '../../design-tokens/figma-variables-resolver';
6
+ import { EMPTY_MODES } from '../../utils/react-utils';
7
+ import { jsx as _jsx } from "react/jsx-runtime";
8
+ /**
9
+ * Counter Badge — a small token-driven pill that displays a value.
10
+ *
11
+ * All visual attributes resolve from the Figma `counterBadge/*` tokens via
12
+ * `getVariableByName`.
13
+ */
14
+ function CounterBadge({
15
+ value = '99',
16
+ modes = EMPTY_MODES,
17
+ style,
18
+ ...rest
19
+ }) {
20
+ // Resolve token values (fall back to the Figma defaults for the single
21
+ // `Default` mode so the badge still renders if a token is missing).
22
+ const backgroundColor = getVariableByName('counterBadge/background', modes) ?? '#ebebec';
23
+ const foreground = getVariableByName('counterBadge/foreground', modes) ?? '#0c0d10';
24
+ const fontFamily = getVariableByName('counterBadge/fontFamily', modes) ?? 'JioType Var';
25
+ const fontWeightRaw = getVariableByName('counterBadge/fontWeight', modes) ?? 500;
26
+ const fontWeight = typeof fontWeightRaw === 'number' ? String(fontWeightRaw) : fontWeightRaw;
27
+ const fontSize = Number(getVariableByName('counterBadge/fontSize', modes) ?? 8);
28
+ const lineHeight = Number(getVariableByName('counterBadge/lineHeight', modes) ?? 8);
29
+ const paddingHorizontal = Number(getVariableByName('counterBadge/padding/horizontal', modes) ?? 4);
30
+ // The Figma design pins vertical padding to 4px (matching horizontal), but the
31
+ // committed variables snapshot resolves `counterBadge/padding/vertical` to 5px
32
+ // (a stale export). Figma is the source of truth here, so hardcode 4px rather
33
+ // than trust the token, keeping the badge one pixel shorter to match the design.
34
+ const paddingVertical = 4;
35
+ const borderRadius = Number(getVariableByName('counterBadge/radius', modes) ?? 999);
36
+ const containerStyle = {
37
+ backgroundColor,
38
+ paddingHorizontal,
39
+ paddingVertical,
40
+ borderRadius,
41
+ alignSelf: 'flex-start',
42
+ alignItems: 'center',
43
+ justifyContent: 'center',
44
+ ...(style || {})
45
+ };
46
+ const textStyle = {
47
+ color: foreground,
48
+ fontFamily,
49
+ fontWeight: fontWeight,
50
+ fontSize,
51
+ lineHeight,
52
+ textAlign: 'center'
53
+ };
54
+ return /*#__PURE__*/_jsx(View, {
55
+ style: containerStyle,
56
+ ...rest,
57
+ children: /*#__PURE__*/_jsx(View, {
58
+ style: WRAP_STYLE,
59
+ children: /*#__PURE__*/_jsx(Text, {
60
+ style: textStyle,
61
+ children: value
62
+ })
63
+ })
64
+ });
65
+ }
66
+
67
+ // Figma `wrap` node (data-node-id 7701:10879): min-width 10, centered.
68
+ const WRAP_STYLE = {
69
+ minWidth: 10,
70
+ alignItems: 'center',
71
+ justifyContent: 'center'
72
+ };
73
+ export default CounterBadge;
@@ -0,0 +1,174 @@
1
+ "use strict";
2
+
3
+ import React, { useCallback, useMemo, useState } from 'react';
4
+ import { Pressable, Platform } from 'react-native';
5
+ import { getVariableByName } from '../../design-tokens/figma-variables-resolver';
6
+ import Icon from '../../icons/Icon';
7
+ import GlassFill from '../../utils/GlassFill/GlassFill';
8
+ import { usePressableWebSupport } from '../../utils/web-platform-utils';
9
+ import { EMPTY_MODES } from '../../utils/react-utils';
10
+ import Skeleton from '../../skeleton/Skeleton';
11
+ import { useSkeleton } from '../../skeleton/SkeletonGroup';
12
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
13
+ const IS_WEB = Platform.OS === 'web';
14
+ const IS_IOS = Platform.OS === 'ios';
15
+ const PRESS_DELAY = IS_IOS ? 130 : 0;
16
+ const pressedOverlayStyle = {
17
+ opacity: 0.7
18
+ };
19
+ // The `favoriteToggle/*` tokens are not yet in the committed variables snapshot,
20
+ // so `getVariableByName` returns undefined and these Figma-node values are used
21
+ // as fallbacks. Once the token export is re-synced the resolver takes over.
22
+ function resolveTokens(modes, isActive) {
23
+ const width = Number(getVariableByName('favoriteToggle/width', modes) ?? 29);
24
+ const height = Number(getVariableByName('favoriteToggle/height', modes) ?? 29);
25
+ const radiusRaw = Number(getVariableByName('favoriteToggle/icon/radius', modes) ?? 9999);
26
+ const iconSize = Number(getVariableByName('favoriteToggle/icon/width', modes) ?? 20);
27
+ // 9999 is the design-token sentinel for "perfect circle".
28
+ const size = Math.max(width, height);
29
+ const borderRadius = radiusRaw >= 9999 ? size / 2 : radiusRaw;
30
+
31
+ // Background + icon color differ per variant. In Figma this is driven by the
32
+ // `isActive` collection (modes `'False'` / `'True'`), so we pass that mode
33
+ // through by its string name. Until the `color/favoriteToggle/*` tokens are
34
+ // exported the resolver returns undefined and the fallbacks below — which
35
+ // encode the two Figma variants directly — take over.
36
+ const isActiveMode = {
37
+ ...modes,
38
+ isActive: isActive ? 'True' : 'False'
39
+ };
40
+ const backgroundColor = getVariableByName('color/favoriteToggle/background/color', isActiveMode) ?? (isActive ? '#ffffff' : '#ffffff33');
41
+ const iconColor = getVariableByName('color/favoriteToggle/icon/color', isActiveMode) ?? (isActive ? '#cea15a' : '#ffffff');
42
+
43
+ // Figma `blur/minimal` (px radius) -> GlassFill's 0-100 intensity scale, the
44
+ // same mapping Badge's glass path uses.
45
+ const blurRaw = Number(getVariableByName('blur/minimal', modes) ?? 29);
46
+ const blurIntensity = Math.max(0, Math.min(100, Math.round(blurRaw)));
47
+ return {
48
+ size,
49
+ borderRadius,
50
+ backgroundColor,
51
+ iconColor,
52
+ iconSize,
53
+ blurIntensity
54
+ };
55
+ }
56
+
57
+ /**
58
+ * Circular glass favorite (heart) toggle, mirroring the Figma `Favorite Toggle`
59
+ * component set's boolean `isActive` variant:
60
+ * - inactive: translucent frosted circle (`#ffffff33` over a `blur/minimal`
61
+ * backdrop) with a white heart.
62
+ * - active: solid white circle with a gold (`#cea15a`) heart.
63
+ * Both variants share the identical filled-heart glyph; only the background and
64
+ * icon colors change between states.
65
+ *
66
+ * Controlled or uncontrolled: pass `isActive` (+ `onChange`) to control it, or
67
+ * omit `isActive` and use `defaultActive` to let the component own the state.
68
+ *
69
+ * Visual attributes resolve from the `favoriteToggle/*` / `color/favoriteToggle/*`
70
+ * tokens via `getVariableByName`. Those tokens are not yet in the committed
71
+ * variables snapshot, so the component currently runs on the Figma-node values
72
+ * as fallbacks; once the tokens are exported (under the `isActive` collection,
73
+ * modes `False`/`True`) the resolver takes over.
74
+ */
75
+ function FavoriteToggle({
76
+ isActive: controlledActive,
77
+ defaultActive = false,
78
+ icon = 'ic_favorite',
79
+ onChange,
80
+ onPress,
81
+ modes = EMPTY_MODES,
82
+ disabled = false,
83
+ accessibilityLabel,
84
+ accessibilityHint,
85
+ accessibilityState,
86
+ webAccessibilityProps,
87
+ style,
88
+ loading,
89
+ ...rest
90
+ }) {
91
+ // Controlled when `isActive` is supplied; otherwise own the state internally
92
+ // (mirrors the Toggle / SegmentedControl controlled-or-uncontrolled pattern).
93
+ const isControlled = controlledActive !== undefined;
94
+ const [internalActive, setInternalActive] = useState(defaultActive);
95
+ const isActive = isControlled ? controlledActive : internalActive;
96
+ const tokens = useMemo(() => resolveTokens(modes, isActive), [modes, isActive]);
97
+ const {
98
+ active: groupActive
99
+ } = useSkeleton();
100
+ const isLoading = loading ?? groupActive;
101
+ const handlePress = useCallback(() => {
102
+ const next = !isActive;
103
+ if (!isControlled) {
104
+ setInternalActive(next);
105
+ }
106
+ onChange?.(next);
107
+ onPress?.();
108
+ }, [isActive, isControlled, onChange, onPress]);
109
+ const label = accessibilityLabel || 'Favorite';
110
+ const webProps = usePressableWebSupport({
111
+ restProps: rest,
112
+ onPress: disabled ? undefined : handlePress,
113
+ disabled,
114
+ accessibilityLabel: label,
115
+ webAccessibilityProps
116
+ });
117
+ const baseContainerStyle = {
118
+ width: tokens.size,
119
+ height: tokens.size,
120
+ borderRadius: tokens.borderRadius,
121
+ // Active is opaque white, so no glass is drawn and the background paints
122
+ // directly. Inactive keeps the container transparent so the GlassFill blur
123
+ // (clipped by overflow:hidden) shows through.
124
+ backgroundColor: isActive ? tokens.backgroundColor : 'transparent',
125
+ overflow: 'hidden',
126
+ alignItems: 'center',
127
+ justifyContent: 'center',
128
+ opacity: disabled ? 0.5 : 1
129
+ };
130
+
131
+ // Declared before the loading short-circuit so hook order stays stable.
132
+ const styleCallback = useCallback(({
133
+ pressed
134
+ }) => [baseContainerStyle, style, pressed && !disabled ? pressedOverlayStyle : null], [baseContainerStyle, style, disabled]);
135
+ if (isLoading) {
136
+ return /*#__PURE__*/_jsx(Skeleton, {
137
+ kind: "other",
138
+ width: tokens.size,
139
+ height: tokens.size,
140
+ style: [{
141
+ borderRadius: tokens.borderRadius
142
+ }, style],
143
+ modes: modes
144
+ });
145
+ }
146
+ return /*#__PURE__*/_jsxs(Pressable, {
147
+ accessibilityRole: IS_WEB ? 'switch' : 'button',
148
+ accessibilityLabel: undefined,
149
+ accessibilityHint: accessibilityHint,
150
+ accessibilityState: {
151
+ disabled,
152
+ checked: isActive,
153
+ ...accessibilityState
154
+ },
155
+ onPress: disabled ? undefined : handlePress,
156
+ disabled: disabled,
157
+ unstable_pressDelay: PRESS_DELAY,
158
+ style: styleCallback,
159
+ ...webProps,
160
+ children: [!isActive ? /*#__PURE__*/_jsx(GlassFill, {
161
+ tint: "light",
162
+ intensity: tokens.blurIntensity,
163
+ overlayColor: tokens.backgroundColor,
164
+ androidTintWash: false
165
+ }) : null, /*#__PURE__*/_jsx(Icon, {
166
+ name: icon,
167
+ size: tokens.iconSize,
168
+ color: tokens.iconColor,
169
+ accessibilityElementsHidden: true,
170
+ importantForAccessibility: "no"
171
+ })]
172
+ });
173
+ }
174
+ export default /*#__PURE__*/React.memo(FavoriteToggle);