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.
- package/CHANGELOG.md +29 -0
- package/D2C.md +118 -0
- package/lib/commonjs/components/CategoryCard/CategoryCard.js +264 -0
- package/lib/commonjs/components/CompareTable/CompareTable.js +140 -84
- package/lib/commonjs/components/ContentSheet/ContentSheet.js +28 -5
- package/lib/commonjs/components/CounterBadge/CounterBadge.js +78 -0
- package/lib/commonjs/components/FavoriteToggle/FavoriteToggle.js +180 -0
- package/lib/commonjs/components/HelloJioInput/HelloJioInput.js +287 -0
- package/lib/commonjs/components/ValueBackMetric/ValueBackMetric.js +219 -0
- package/lib/commonjs/components/index.js +35 -0
- package/lib/commonjs/design-tokens/Coin Variables-variables-full.json +40095 -1
- package/lib/commonjs/design-tokens/figma-modes.generated.js +3 -0
- package/lib/commonjs/icons/registry.js +1 -1
- package/lib/module/components/CategoryCard/CategoryCard.js +258 -0
- package/lib/module/components/CompareTable/CompareTable.js +140 -84
- package/lib/module/components/ContentSheet/ContentSheet.js +29 -6
- package/lib/module/components/CounterBadge/CounterBadge.js +73 -0
- package/lib/module/components/FavoriteToggle/FavoriteToggle.js +174 -0
- package/lib/module/components/HelloJioInput/HelloJioInput.js +281 -0
- package/lib/module/components/ValueBackMetric/ValueBackMetric.js +214 -0
- package/lib/module/components/index.js +6 -1
- package/lib/module/design-tokens/Coin Variables-variables-full.json +40095 -1
- package/lib/module/design-tokens/figma-modes.generated.js +3 -0
- package/lib/module/icons/registry.js +1 -1
- package/lib/typescript/src/components/CategoryCard/CategoryCard.d.ts +72 -0
- package/lib/typescript/src/components/CompareTable/CompareTable.d.ts +16 -1
- package/lib/typescript/src/components/ContentSheet/ContentSheet.d.ts +7 -1
- package/lib/typescript/src/components/CounterBadge/CounterBadge.d.ts +19 -0
- package/lib/typescript/src/components/FavoriteToggle/FavoriteToggle.d.ts +66 -0
- package/lib/typescript/src/components/HelloJioInput/HelloJioInput.d.ts +111 -0
- package/lib/typescript/src/components/ValueBackMetric/ValueBackMetric.d.ts +86 -0
- package/lib/typescript/src/components/index.d.ts +5 -0
- package/lib/typescript/src/icons/registry.d.ts +1 -1
- package/package.json +7 -1
- package/scripts/extract-figma-modes.js +359 -0
- package/src/components/CategoryCard/CategoryCard.tsx +404 -0
- package/src/components/CompareTable/CompareTable.tsx +201 -101
- package/src/components/ContentSheet/ContentSheet.tsx +40 -11
- package/src/components/CounterBadge/CounterBadge.tsx +95 -0
- package/src/components/FavoriteToggle/FavoriteToggle.tsx +243 -0
- package/src/components/HelloJioInput/HelloJioInput.tsx +513 -0
- package/src/components/ValueBackMetric/ValueBackMetric.tsx +298 -0
- package/src/components/index.ts +5 -0
- package/src/design-tokens/Coin Variables-variables-full.json +40095 -1
- package/src/design-tokens/figma-modes.generated.ts +3 -0
- package/src/icons/registry.ts +1 -1
|
@@ -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)
|