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,404 @@
|
|
|
1
|
+
import React, { useCallback, useMemo } from 'react'
|
|
2
|
+
import {
|
|
3
|
+
Pressable,
|
|
4
|
+
Text,
|
|
5
|
+
View,
|
|
6
|
+
type AccessibilityState,
|
|
7
|
+
type ImageSourcePropType,
|
|
8
|
+
type PressableStateCallbackType,
|
|
9
|
+
type StyleProp,
|
|
10
|
+
type TextStyle,
|
|
11
|
+
type ViewStyle,
|
|
12
|
+
} from 'react-native'
|
|
13
|
+
import { getVariableByName } from '../../design-tokens/figma-variables-resolver'
|
|
14
|
+
import { useTokens } from '../../design-tokens/JFSThemeProvider'
|
|
15
|
+
import {
|
|
16
|
+
EMPTY_MODES,
|
|
17
|
+
cloneChildrenWithModes,
|
|
18
|
+
resolveTextLayout,
|
|
19
|
+
} from '../../utils/react-utils'
|
|
20
|
+
import {
|
|
21
|
+
usePressableWebSupport,
|
|
22
|
+
type SafePressableProps,
|
|
23
|
+
type WebAccessibilityProps,
|
|
24
|
+
} from '../../utils/web-platform-utils'
|
|
25
|
+
import Badge from '../Badge/Badge'
|
|
26
|
+
import Image from '../Image/Image'
|
|
27
|
+
import type { Modes } from '../../design-tokens'
|
|
28
|
+
|
|
29
|
+
const CATEGORY_CARD_STATE = 'Category Card State'
|
|
30
|
+
const BADGE_OVERLAP = 11
|
|
31
|
+
const DEFAULT_CARD_WIDTH = 60
|
|
32
|
+
|
|
33
|
+
// Default modes from the Figma CategoryCard component. Overridable via `modes`.
|
|
34
|
+
const CATEGORY_CARD_DEFAULT_MODES = Object.freeze({
|
|
35
|
+
AppearanceBrand: 'Tertiary',
|
|
36
|
+
'Badge Size': 'Small',
|
|
37
|
+
Context4: 'Badge',
|
|
38
|
+
"Radius": "None"
|
|
39
|
+
})
|
|
40
|
+
|
|
41
|
+
const toNumber = (value: unknown, fallback: number): number => {
|
|
42
|
+
if (typeof value === 'number') {
|
|
43
|
+
return Number.isFinite(value) ? value : fallback
|
|
44
|
+
}
|
|
45
|
+
if (typeof value === 'string') {
|
|
46
|
+
const parsed = Number(value)
|
|
47
|
+
return Number.isFinite(parsed) ? parsed : fallback
|
|
48
|
+
}
|
|
49
|
+
return fallback
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const toFontWeight = (
|
|
53
|
+
value: unknown,
|
|
54
|
+
fallback: TextStyle['fontWeight']
|
|
55
|
+
): TextStyle['fontWeight'] => {
|
|
56
|
+
if (typeof value === 'number') return String(value) as TextStyle['fontWeight']
|
|
57
|
+
if (typeof value === 'string') return value as TextStyle['fontWeight']
|
|
58
|
+
return fallback
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export type CategoryCardProps = SafePressableProps & {
|
|
62
|
+
/** Bottom label (e.g. `"Popular"`). */
|
|
63
|
+
label?: string
|
|
64
|
+
/**
|
|
65
|
+
* Image shown in the card centre. Ignored when `imageSlot` is provided.
|
|
66
|
+
* Accepts a remote URL string, `{ uri }` object, or `require()`-ed asset.
|
|
67
|
+
*/
|
|
68
|
+
imageSource?: ImageSourcePropType | string
|
|
69
|
+
/**
|
|
70
|
+
* Slot replacing the default `Image`. Receives `modes` recursively so inner
|
|
71
|
+
* components inherit theming.
|
|
72
|
+
*/
|
|
73
|
+
imageSlot?: React.ReactNode
|
|
74
|
+
/**
|
|
75
|
+
* Optional badge label rendered as an overlapping pill at the top edge.
|
|
76
|
+
* Omit or pass `undefined` to hide the badge.
|
|
77
|
+
*/
|
|
78
|
+
badge?: string
|
|
79
|
+
/** Whether the card is in the selected / active state. */
|
|
80
|
+
active?: boolean
|
|
81
|
+
/**
|
|
82
|
+
* Fixed width of the card in dp. Defaults to `60` per the Figma spec.
|
|
83
|
+
*/
|
|
84
|
+
width?: number
|
|
85
|
+
/**
|
|
86
|
+
* When `true`, disables text truncation: the trailing ellipsis (`…`) is never
|
|
87
|
+
* shown and any `numberOfLines` clamp is ignored, so the label wraps to as many
|
|
88
|
+
* lines as it needs even when horizontal space is tight. Same behaviour as
|
|
89
|
+
* {@link Text}'s `disableTruncation`.
|
|
90
|
+
*/
|
|
91
|
+
disableTruncation?: boolean
|
|
92
|
+
/**
|
|
93
|
+
* When `true`, forces the label onto a single line with no wrapping (implies
|
|
94
|
+
* `disableTruncation`). On web the line overflows horizontally instead of
|
|
95
|
+
* wrapping or truncating. Same behaviour as {@link Text}'s `singleLine`.
|
|
96
|
+
*/
|
|
97
|
+
singleLine?: boolean
|
|
98
|
+
/** Press handler — the card is always a `Pressable`. */
|
|
99
|
+
onPress?: () => void
|
|
100
|
+
disabled?: boolean
|
|
101
|
+
/** Design token modes for theming (e.g. `{ 'Color Mode': 'Light' }`). */
|
|
102
|
+
modes?: Modes
|
|
103
|
+
/** Container style override. */
|
|
104
|
+
style?: StyleProp<ViewStyle>
|
|
105
|
+
/** Label style override. */
|
|
106
|
+
labelStyle?: StyleProp<TextStyle>
|
|
107
|
+
accessibilityLabel?: string
|
|
108
|
+
accessibilityHint?: string
|
|
109
|
+
accessibilityState?: AccessibilityState
|
|
110
|
+
webAccessibilityProps?: WebAccessibilityProps
|
|
111
|
+
testID?: string
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
interface CategoryCardTokens {
|
|
115
|
+
containerStyle: ViewStyle
|
|
116
|
+
labelStyle: TextStyle
|
|
117
|
+
imageSize: number
|
|
118
|
+
imageRadius: number
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function resolveCategoryCardTokens(
|
|
122
|
+
modes: Modes,
|
|
123
|
+
active: boolean
|
|
124
|
+
): CategoryCardTokens {
|
|
125
|
+
const idleModes = { ...modes, [CATEGORY_CARD_STATE]: 'Idle' }
|
|
126
|
+
const activeModes = { ...modes, [CATEGORY_CARD_STATE]: 'Active' }
|
|
127
|
+
const resolvedModes = active ? activeModes : idleModes
|
|
128
|
+
|
|
129
|
+
const gap = toNumber(getVariableByName('cardTab/gap', resolvedModes), 6)
|
|
130
|
+
const paddingHorizontal = toNumber(
|
|
131
|
+
getVariableByName('cardTab/padding/horizontal', resolvedModes),
|
|
132
|
+
8
|
|
133
|
+
)
|
|
134
|
+
const paddingVertical = toNumber(
|
|
135
|
+
getVariableByName('cardTab/padding/vertical', resolvedModes),
|
|
136
|
+
8
|
|
137
|
+
)
|
|
138
|
+
const radius = toNumber(getVariableByName('cardTab/radius', resolvedModes), 8)
|
|
139
|
+
const strokeWidth = toNumber(
|
|
140
|
+
getVariableByName('cardTab/strokeWidth', resolvedModes),
|
|
141
|
+
1
|
|
142
|
+
)
|
|
143
|
+
const strokeColor =
|
|
144
|
+
(getVariableByName('cardTab/stroke/color', resolvedModes) as string | null) ??
|
|
145
|
+
'#f5f5f5'
|
|
146
|
+
|
|
147
|
+
const idleBackground =
|
|
148
|
+
(getVariableByName('cardTab/background/color', idleModes) as string | null) ??
|
|
149
|
+
'#ffffff'
|
|
150
|
+
const activeBackground =
|
|
151
|
+
(getVariableByName('cardTab/background/color', activeModes) as string | null) ??
|
|
152
|
+
'#f5f5f5'
|
|
153
|
+
const backgroundColor = active ? activeBackground : idleBackground
|
|
154
|
+
|
|
155
|
+
const imageSize = toNumber(
|
|
156
|
+
getVariableByName('cardTab/image/width', resolvedModes),
|
|
157
|
+
36
|
|
158
|
+
)
|
|
159
|
+
const imageRadius = toNumber(
|
|
160
|
+
getVariableByName('image/radius', resolvedModes),
|
|
161
|
+
0
|
|
162
|
+
)
|
|
163
|
+
|
|
164
|
+
const fontFamily =
|
|
165
|
+
(getVariableByName('cardTab/label/fontFamily', resolvedModes) as string | null) ??
|
|
166
|
+
'JioType Var'
|
|
167
|
+
const fontSize = toNumber(
|
|
168
|
+
getVariableByName('cardTab/label/fontSize', resolvedModes),
|
|
169
|
+
12
|
|
170
|
+
)
|
|
171
|
+
const lineHeight = toNumber(
|
|
172
|
+
getVariableByName('cardTab/label/lineHeight', resolvedModes),
|
|
173
|
+
Math.round(fontSize * 1.3)
|
|
174
|
+
)
|
|
175
|
+
const fontWeight = toFontWeight(
|
|
176
|
+
getVariableByName('cardTab/label/fontWeight', resolvedModes),
|
|
177
|
+
'500'
|
|
178
|
+
)
|
|
179
|
+
const labelColor =
|
|
180
|
+
(getVariableByName('cardTab/label/color', resolvedModes) as string | null) ??
|
|
181
|
+
'#000000'
|
|
182
|
+
|
|
183
|
+
return {
|
|
184
|
+
containerStyle: {
|
|
185
|
+
position: 'relative',
|
|
186
|
+
alignItems: 'center',
|
|
187
|
+
justifyContent: 'center',
|
|
188
|
+
backgroundColor,
|
|
189
|
+
borderColor: strokeColor,
|
|
190
|
+
borderWidth: strokeWidth,
|
|
191
|
+
borderStyle: 'solid',
|
|
192
|
+
borderRadius: radius,
|
|
193
|
+
paddingHorizontal,
|
|
194
|
+
paddingTop: paddingVertical,
|
|
195
|
+
paddingBottom: paddingVertical,
|
|
196
|
+
gap,
|
|
197
|
+
},
|
|
198
|
+
labelStyle: {
|
|
199
|
+
color: labelColor,
|
|
200
|
+
fontFamily,
|
|
201
|
+
fontSize,
|
|
202
|
+
fontWeight,
|
|
203
|
+
lineHeight,
|
|
204
|
+
textAlign: 'center',
|
|
205
|
+
},
|
|
206
|
+
imageSize,
|
|
207
|
+
imageRadius,
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const pressedOverlayStyle: ViewStyle = { opacity: 0.85 }
|
|
212
|
+
const disabledOverlayStyle: ViewStyle = { opacity: 0.5 }
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* `CategoryCard` is a compact, tappable tile for category pickers and
|
|
216
|
+
* merchandising rails. It stacks a square image above a short label and can
|
|
217
|
+
* optionally show an overlapping badge at the top edge. Cards are typically
|
|
218
|
+
* laid out in a horizontal `ScrollView` or `HStack`.
|
|
219
|
+
*
|
|
220
|
+
* All visual values resolve through the `cardTab/*` design tokens with
|
|
221
|
+
* sensible Figma defaults so the card renders correctly out of the box.
|
|
222
|
+
*
|
|
223
|
+
* @component
|
|
224
|
+
* @param {CategoryCardProps} props
|
|
225
|
+
*/
|
|
226
|
+
function CategoryCard({
|
|
227
|
+
label = 'Popular',
|
|
228
|
+
imageSource,
|
|
229
|
+
imageSlot,
|
|
230
|
+
badge,
|
|
231
|
+
active = false,
|
|
232
|
+
width = DEFAULT_CARD_WIDTH,
|
|
233
|
+
disableTruncation,
|
|
234
|
+
singleLine,
|
|
235
|
+
onPress,
|
|
236
|
+
disabled = false,
|
|
237
|
+
modes: propModes = EMPTY_MODES,
|
|
238
|
+
style,
|
|
239
|
+
labelStyle,
|
|
240
|
+
accessibilityLabel,
|
|
241
|
+
accessibilityHint,
|
|
242
|
+
accessibilityState,
|
|
243
|
+
webAccessibilityProps,
|
|
244
|
+
testID,
|
|
245
|
+
...rest
|
|
246
|
+
}: CategoryCardProps) {
|
|
247
|
+
const { modes: globalModes } = useTokens()
|
|
248
|
+
const modes = useMemo(
|
|
249
|
+
() => ({
|
|
250
|
+
...globalModes,
|
|
251
|
+
...CATEGORY_CARD_DEFAULT_MODES,
|
|
252
|
+
...propModes,
|
|
253
|
+
}),
|
|
254
|
+
[globalModes, propModes]
|
|
255
|
+
)
|
|
256
|
+
|
|
257
|
+
const resolvedModes = useMemo(
|
|
258
|
+
() => ({
|
|
259
|
+
...modes,
|
|
260
|
+
[CATEGORY_CARD_STATE]: active ? 'Active' : 'Idle',
|
|
261
|
+
}),
|
|
262
|
+
[modes, active]
|
|
263
|
+
)
|
|
264
|
+
|
|
265
|
+
const tokens = useMemo(
|
|
266
|
+
() => resolveCategoryCardTokens(resolvedModes, active),
|
|
267
|
+
[resolvedModes, active]
|
|
268
|
+
)
|
|
269
|
+
|
|
270
|
+
const { style: labelLayoutStyle, ...labelTruncation } = useMemo(
|
|
271
|
+
() =>
|
|
272
|
+
resolveTextLayout({
|
|
273
|
+
disableTruncation,
|
|
274
|
+
singleLine,
|
|
275
|
+
numberOfLines: 1,
|
|
276
|
+
ellipsizeMode: 'tail',
|
|
277
|
+
}),
|
|
278
|
+
[disableTruncation, singleLine]
|
|
279
|
+
)
|
|
280
|
+
|
|
281
|
+
const processedImageSlot = useMemo(() => {
|
|
282
|
+
if (!imageSlot) return null
|
|
283
|
+
const processed = cloneChildrenWithModes(imageSlot, resolvedModes)
|
|
284
|
+
return processed.length === 1 ? processed[0] : processed
|
|
285
|
+
}, [imageSlot, resolvedModes])
|
|
286
|
+
|
|
287
|
+
const imageNode =
|
|
288
|
+
processedImageSlot ??
|
|
289
|
+
(imageSource !== undefined ? (
|
|
290
|
+
<Image
|
|
291
|
+
imageSource={imageSource}
|
|
292
|
+
width={tokens.imageSize}
|
|
293
|
+
height={tokens.imageSize}
|
|
294
|
+
ratio={1}
|
|
295
|
+
resizeMode="cover"
|
|
296
|
+
borderRadius={tokens.imageRadius}
|
|
297
|
+
accessibilityElementsHidden
|
|
298
|
+
importantForAccessibility="no"
|
|
299
|
+
/>
|
|
300
|
+
) : (
|
|
301
|
+
<Image
|
|
302
|
+
imageSource="https://www.figma.com/api/mcp/asset/0ee34871-646f-4736-be06-22fb6c0a4eed"
|
|
303
|
+
width={tokens.imageSize}
|
|
304
|
+
height={tokens.imageSize}
|
|
305
|
+
ratio={1}
|
|
306
|
+
resizeMode="cover"
|
|
307
|
+
borderRadius={tokens.imageRadius}
|
|
308
|
+
accessibilityElementsHidden
|
|
309
|
+
importantForAccessibility="no"
|
|
310
|
+
/>
|
|
311
|
+
))
|
|
312
|
+
|
|
313
|
+
const a11yLabel = accessibilityLabel ?? (badge ? `${label}, ${badge}` : label)
|
|
314
|
+
|
|
315
|
+
const webProps = usePressableWebSupport({
|
|
316
|
+
restProps: rest,
|
|
317
|
+
onPress: disabled ? undefined : onPress,
|
|
318
|
+
disabled,
|
|
319
|
+
accessibilityLabel: a11yLabel,
|
|
320
|
+
webAccessibilityProps,
|
|
321
|
+
})
|
|
322
|
+
|
|
323
|
+
const badgeNode = badge ? (
|
|
324
|
+
<View style={BADGE_ANCHOR} pointerEvents="none">
|
|
325
|
+
<Badge label={badge} modes={resolvedModes} style={BADGE_CENTER_STYLE} />
|
|
326
|
+
</View>
|
|
327
|
+
) : null
|
|
328
|
+
|
|
329
|
+
// Truncation needs a width bound so the ellipsis engages. `singleLine`
|
|
330
|
+
// must NOT fill the content box — otherwise the text is pinned to the left
|
|
331
|
+
// padding edge and only grows rightward. Intrinsic width + centered
|
|
332
|
+
// alignment lets overflow expand equally on both sides.
|
|
333
|
+
const labelWidthStyle = singleLine
|
|
334
|
+
? LABEL_OVERFLOW_CENTER_STYLE
|
|
335
|
+
: LABEL_FILL_STYLE
|
|
336
|
+
|
|
337
|
+
const pressableStyle = useCallback(
|
|
338
|
+
({ pressed }: PressableStateCallbackType): StyleProp<ViewStyle> => [
|
|
339
|
+
tokens.containerStyle,
|
|
340
|
+
{ width },
|
|
341
|
+
badge ? { marginTop: BADGE_OVERLAP } : null,
|
|
342
|
+
disabled ? disabledOverlayStyle : null,
|
|
343
|
+
pressed && !disabled ? pressedOverlayStyle : null,
|
|
344
|
+
style,
|
|
345
|
+
],
|
|
346
|
+
[tokens.containerStyle, width, badge, disabled, style]
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
return (
|
|
350
|
+
<Pressable
|
|
351
|
+
accessibilityRole="button"
|
|
352
|
+
accessibilityLabel={a11yLabel}
|
|
353
|
+
accessibilityHint={accessibilityHint}
|
|
354
|
+
accessibilityState={{
|
|
355
|
+
disabled,
|
|
356
|
+
selected: active,
|
|
357
|
+
...accessibilityState,
|
|
358
|
+
}}
|
|
359
|
+
onPress={onPress}
|
|
360
|
+
disabled={disabled}
|
|
361
|
+
style={pressableStyle}
|
|
362
|
+
testID={testID}
|
|
363
|
+
{...webProps}
|
|
364
|
+
>
|
|
365
|
+
{badgeNode}
|
|
366
|
+
{imageNode}
|
|
367
|
+
<Text
|
|
368
|
+
style={[tokens.labelStyle, labelWidthStyle, labelLayoutStyle, labelStyle]}
|
|
369
|
+
{...labelTruncation}
|
|
370
|
+
accessibilityElementsHidden
|
|
371
|
+
importantForAccessibility="no"
|
|
372
|
+
>
|
|
373
|
+
{label}
|
|
374
|
+
</Text>
|
|
375
|
+
</Pressable>
|
|
376
|
+
)
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
const BADGE_ANCHOR: ViewStyle = {
|
|
380
|
+
position: 'absolute',
|
|
381
|
+
top: -BADGE_OVERLAP,
|
|
382
|
+
left: 0,
|
|
383
|
+
right: 0,
|
|
384
|
+
alignItems: 'center',
|
|
385
|
+
justifyContent: 'center',
|
|
386
|
+
zIndex: 1,
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
const BADGE_CENTER_STYLE: ViewStyle = { alignSelf: 'center' }
|
|
390
|
+
|
|
391
|
+
/** Lets the label fill the card width so the one-line ellipsis has a bound. */
|
|
392
|
+
const LABEL_FILL_STYLE: TextStyle = { width: '100%', alignSelf: 'stretch' }
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Intrinsic width, horizontally centred. When the label is wider than the
|
|
396
|
+
* card it overflows left and right equally instead of growing only rightward
|
|
397
|
+
* from the padded content edge.
|
|
398
|
+
*/
|
|
399
|
+
const LABEL_OVERFLOW_CENTER_STYLE: TextStyle = {
|
|
400
|
+
alignSelf: 'center',
|
|
401
|
+
textAlign: 'center',
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
export default React.memo(CategoryCard)
|