jfs-components 0.0.85 → 0.0.95

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 (28) hide show
  1. package/CHANGELOG.md +15 -0
  2. package/lib/commonjs/assets.d.js +1 -0
  3. package/lib/commonjs/components/AllocationComparisonChart/AllocationComparisonChart.js +299 -0
  4. package/lib/commonjs/components/FullscreenModal/FullscreenModal.js +104 -94
  5. package/lib/commonjs/components/Icon/Icon.js +112 -0
  6. package/lib/commonjs/components/index.js +14 -0
  7. package/lib/commonjs/design-tokens/Coin Variables-variables-full.json +1 -1
  8. package/lib/commonjs/icons/registry.js +1 -1
  9. package/lib/module/assets.d.js +1 -0
  10. package/lib/module/components/AllocationComparisonChart/AllocationComparisonChart.js +293 -0
  11. package/lib/module/components/FullscreenModal/FullscreenModal.js +106 -96
  12. package/lib/module/components/Icon/Icon.js +106 -0
  13. package/lib/module/components/index.js +2 -0
  14. package/lib/module/design-tokens/Coin Variables-variables-full.json +1 -1
  15. package/lib/module/icons/registry.js +1 -1
  16. package/lib/typescript/src/components/AllocationComparisonChart/AllocationComparisonChart.d.ts +118 -0
  17. package/lib/typescript/src/components/FullscreenModal/FullscreenModal.d.ts +39 -29
  18. package/lib/typescript/src/components/Icon/Icon.d.ts +75 -0
  19. package/lib/typescript/src/components/index.d.ts +2 -0
  20. package/lib/typescript/src/icons/registry.d.ts +1 -1
  21. package/package.json +1 -1
  22. package/src/assets.d.ts +24 -0
  23. package/src/components/AllocationComparisonChart/AllocationComparisonChart.tsx +450 -0
  24. package/src/components/FullscreenModal/FullscreenModal.tsx +131 -126
  25. package/src/components/Icon/Icon.tsx +167 -0
  26. package/src/components/index.ts +2 -0
  27. package/src/design-tokens/Coin Variables-variables-full.json +1 -1
  28. package/src/icons/registry.ts +1 -1
@@ -0,0 +1,450 @@
1
+ import React from 'react'
2
+ import {
3
+ View,
4
+ Text,
5
+ type StyleProp,
6
+ type TextStyle,
7
+ type ViewStyle,
8
+ } from 'react-native'
9
+ import Svg, { Line } from 'react-native-svg'
10
+ import { getVariableByName } from '../../design-tokens/figma-variables-resolver'
11
+ import { useTokens } from '../../design-tokens/JFSThemeProvider'
12
+ import { EMPTY_MODES } from '../../utils/react-utils'
13
+ import MetricLegendItem from '../MetricLegendItem/MetricLegendItem'
14
+
15
+ /**
16
+ * One vertical pill in the {@link AllocationComparisonChartProps.data} array.
17
+ *
18
+ * Each segment renders a single bar whose **height encodes `value`** (the
19
+ * "current" reading) and, when supplied, a **`baseline`** overlay drawn from
20
+ * the bottom up with a dashed marker line (the "recommended" reading). Both
21
+ * are measured against the same shared scale so bars and baselines are
22
+ * directly comparable across segments.
23
+ */
24
+ export type AllocationSegment = {
25
+ /** Stable React key (falls back to the array index). */
26
+ key?: React.Key
27
+ /** Caption rendered under the bar — e.g. `"Small & Mid"`. */
28
+ label: React.ReactNode
29
+ /**
30
+ * Primary value driving the bar's height (the "current" reading). Scaled
31
+ * against {@link AllocationComparisonChartProps.max}.
32
+ */
33
+ value: number
34
+ /**
35
+ * Optional comparison value (the "recommended" reading). When provided, a
36
+ * filled overlay is drawn from the bottom of the bar up to this level.
37
+ * Omit to render a plain bar.
38
+ */
39
+ baseline?: number
40
+ /**
41
+ * Whether to draw the dashed reference line + value label at the top of the
42
+ * `baseline` overlay. Defaults to `true` **only for the first segment that
43
+ * has a `baseline`** and `false` for the rest, so the marker stays a
44
+ * focused callout rather than repeating on every bar. Set explicitly to
45
+ * force it on/off per segment. Has no effect without a `baseline`.
46
+ */
47
+ showMarker?: boolean
48
+ /**
49
+ * Text shown above the bar. Defaults to `formatValue(value)`. Pass `null`
50
+ * to hide it.
51
+ */
52
+ valueLabel?: React.ReactNode
53
+ /**
54
+ * Text shown beside the dashed baseline marker. Defaults to
55
+ * `formatValue(baseline)`. Pass `null` to hide just the marker label while
56
+ * keeping the dashed line.
57
+ */
58
+ baselineLabel?: React.ReactNode
59
+ /** Hard-override the bar (current) fill color. */
60
+ color?: string
61
+ /** Hard-override the baseline (recommended) overlay color. */
62
+ baselineColor?: string
63
+ /** Per-segment accessibility label. */
64
+ accessibilityLabel?: string
65
+ }
66
+
67
+ export type AllocationComparisonChartProps = {
68
+ /**
69
+ * The segments to plot, left → right. Each carries its `value`, optional
70
+ * `baseline` and its `label`, so a bar can never drift from its caption.
71
+ */
72
+ data?: AllocationSegment[]
73
+ /**
74
+ * Maximum value used to scale every bar **and** baseline. Defaults to the
75
+ * largest `value`/`baseline` across `data`. Pass an explicit `max` (e.g.
76
+ * `100` for percentages) for a fixed scale.
77
+ */
78
+ max?: number
79
+ /**
80
+ * Height in px of the bar area — i.e. the height of a bar whose value
81
+ * equals `max`. Excludes the value/caption label rows. Default: `154`.
82
+ */
83
+ height?: number
84
+ /** Width of each pill bar in px. Default: the `segmentIndicator/track/width` token (`28`). */
85
+ barWidth?: number
86
+ /** Show the legend row above the chart. Default: `true`. */
87
+ showLegend?: boolean
88
+ /** Legend label for the bar (current) series. Default: `"Current"`. */
89
+ valueLegendLabel?: React.ReactNode
90
+ /**
91
+ * Legend label for the baseline (recommended) series. Default:
92
+ * `"Recommended"`. The baseline legend item only appears when at least one
93
+ * segment defines a `baseline`.
94
+ */
95
+ baselineLegendLabel?: React.ReactNode
96
+ /**
97
+ * Formats numeric `value`/`baseline` into the default labels. Default:
98
+ * `(v) => \`${v}%\``.
99
+ */
100
+ formatValue?: (value: number) => string
101
+ /** Design token modes for theming (e.g. `{ 'Color Mode': 'Light' }`). */
102
+ modes?: Record<string, any>
103
+ /** Container style override. */
104
+ style?: StyleProp<ViewStyle>
105
+ /** Style applied to the bars row. */
106
+ chartStyle?: StyleProp<ViewStyle>
107
+ /** Style applied to the legend row. */
108
+ legendStyle?: StyleProp<ViewStyle>
109
+ /** Accessibility label for the whole chart. */
110
+ accessibilityLabel?: string
111
+ }
112
+
113
+ const DEFAULT_DATA: AllocationSegment[] = [
114
+ { label: 'Small & Mid', value: 65, baseline: 35 },
115
+ { label: 'Large', value: 25 },
116
+ { label: 'Others', value: 10 },
117
+ ]
118
+
119
+ const toNumber = (value: unknown, fallback: number): number => {
120
+ if (typeof value === 'number') {
121
+ return Number.isFinite(value) ? value : fallback
122
+ }
123
+ if (typeof value === 'string') {
124
+ const parsed = Number(value)
125
+ return Number.isFinite(parsed) ? parsed : fallback
126
+ }
127
+ return fallback
128
+ }
129
+
130
+ const toFontWeight = (
131
+ value: unknown,
132
+ fallback: TextStyle['fontWeight']
133
+ ): TextStyle['fontWeight'] => {
134
+ if (typeof value === 'number') {
135
+ return String(value) as TextStyle['fontWeight']
136
+ }
137
+ if (typeof value === 'string') {
138
+ return value as TextStyle['fontWeight']
139
+ }
140
+ return fallback
141
+ }
142
+
143
+ const isShown = (node: React.ReactNode): boolean =>
144
+ node !== undefined && node !== null && node !== false
145
+
146
+ type SegmentTheme = {
147
+ barWidth: number
148
+ pillRadius: number
149
+ gap: number
150
+ currentColor: string
151
+ baselineColor: string
152
+ lineColor: string
153
+ lineSize: number
154
+ labelStyle: TextStyle
155
+ }
156
+
157
+ type SegmentBarProps = {
158
+ segment: AllocationSegment
159
+ barHeightPx: number
160
+ baselineHeightPx: number | null
161
+ baselineLabel: React.ReactNode
162
+ showMarker: boolean
163
+ theme: SegmentTheme
164
+ }
165
+
166
+ /**
167
+ * Internal: one vertical pill column (the Figma "Segment Indicator"). Not
168
+ * exported — the ergonomic public unit is the chart driven by `data`. The
169
+ * `segmentIndicator/*` token names are mirrored here so design ↔ code token
170
+ * alignment is preserved.
171
+ */
172
+ function SegmentBar({
173
+ segment,
174
+ barHeightPx,
175
+ baselineHeightPx,
176
+ baselineLabel,
177
+ showMarker,
178
+ theme,
179
+ }: SegmentBarProps) {
180
+ const { barWidth, pillRadius, gap, currentColor, baselineColor, lineColor, lineSize, labelStyle } =
181
+ theme
182
+
183
+ const fillColor = segment.color ?? currentColor
184
+ const overlayColor = segment.baselineColor ?? baselineColor
185
+ const showValueLabel = isShown(segment.valueLabel)
186
+
187
+ const hasBaseline = baselineHeightPx !== null && baselineHeightPx > 0
188
+ const overlayHeight = hasBaseline ? Math.min(baselineHeightPx as number, barHeightPx) : 0
189
+ const overlayRadius = Math.min(pillRadius, barWidth / 2, overlayHeight / 2)
190
+
191
+ return (
192
+ <View
193
+ style={{ flex: 1, alignItems: 'center', justifyContent: 'flex-end', gap }}
194
+ accessibilityLabel={segment.accessibilityLabel}
195
+ >
196
+ {showValueLabel ? (
197
+ <Text numberOfLines={1} style={labelStyle}>
198
+ {segment.valueLabel}
199
+ </Text>
200
+ ) : null}
201
+
202
+ <View
203
+ style={{
204
+ width: barWidth,
205
+ height: Math.max(barHeightPx, 1),
206
+ borderRadius: pillRadius,
207
+ backgroundColor: fillColor,
208
+ position: 'relative',
209
+ }}
210
+ >
211
+ {hasBaseline ? (
212
+ <>
213
+ <View
214
+ style={{
215
+ position: 'absolute',
216
+ left: 0,
217
+ right: 0,
218
+ bottom: 0,
219
+ height: overlayHeight,
220
+ backgroundColor: overlayColor,
221
+ borderBottomLeftRadius: overlayRadius,
222
+ borderBottomRightRadius: overlayRadius,
223
+ }}
224
+ />
225
+ {showMarker ? (
226
+ <View
227
+ style={{
228
+ position: 'absolute',
229
+ left: 0,
230
+ bottom: overlayHeight,
231
+ height: 0,
232
+ flexDirection: 'row',
233
+ alignItems: 'center',
234
+ }}
235
+ pointerEvents="none"
236
+ >
237
+ <Svg width={barWidth} height={Math.max(lineSize, 1)}>
238
+ <Line
239
+ x1={0}
240
+ y1={Math.max(lineSize, 1) / 2}
241
+ x2={barWidth}
242
+ y2={Math.max(lineSize, 1) / 2}
243
+ stroke={lineColor}
244
+ strokeWidth={lineSize}
245
+ strokeDasharray="2 2"
246
+ />
247
+ </Svg>
248
+ {isShown(baselineLabel) ? (
249
+ <Text numberOfLines={1} style={[labelStyle, { marginLeft: 6 }]}>
250
+ {baselineLabel}
251
+ </Text>
252
+ ) : null}
253
+ </View>
254
+ ) : null}
255
+ </>
256
+ ) : null}
257
+ </View>
258
+
259
+ <Text numberOfLines={1} style={labelStyle}>
260
+ {segment.label}
261
+ </Text>
262
+ </View>
263
+ )
264
+ }
265
+
266
+ /**
267
+ * `AllocationComparisonChart` plots a row of vertical pill bars that compare a
268
+ * **current** reading (each bar's height) against an optional **recommended**
269
+ * baseline (a filled overlay drawn from the bottom up, marked with a dashed
270
+ * line). Every bar and baseline shares a single scale, so heights are directly
271
+ * comparable across segments — no axes required.
272
+ *
273
+ * The chart is driven entirely by the `data` array: each entry pairs a
274
+ * `value`, an optional `baseline` and its `label`, so a bar can never drift
275
+ * out of sync with its caption or its baseline marker.
276
+ *
277
+ * Colors, fonts, spacing and the pill radius resolve from the Figma
278
+ * `segmentIndicator/*`, `metricLegendItem/*` and `allocationComparisonChart/*`
279
+ * tokens via the `modes` prop.
280
+ *
281
+ * @component
282
+ */
283
+ function AllocationComparisonChart({
284
+ data = DEFAULT_DATA,
285
+ max,
286
+ height = 154,
287
+ barWidth,
288
+ showLegend = true,
289
+ valueLegendLabel = 'Current',
290
+ baselineLegendLabel = 'Recommended',
291
+ formatValue = (value: number) => `${value}%`,
292
+ modes: propModes = EMPTY_MODES,
293
+ style,
294
+ chartStyle,
295
+ legendStyle,
296
+ accessibilityLabel,
297
+ }: AllocationComparisonChartProps) {
298
+ const { modes: globalModes } = useTokens()
299
+ const modes = React.useMemo(
300
+ () => ({ ...globalModes, ...propModes }),
301
+ [globalModes, propModes]
302
+ )
303
+
304
+ const trackWidth = toNumber(getVariableByName('segmentIndicator/track/width', modes), 28)
305
+ const resolvedBarWidth = barWidth ?? trackWidth
306
+ const radiusToken = toNumber(getVariableByName('segmentIndicator/indicator/radius', modes), 99999)
307
+ const pillRadius = Math.min(radiusToken, resolvedBarWidth / 2)
308
+ const gap = toNumber(getVariableByName('segmentIndicator/gap', modes), 4)
309
+ const chartGap = toNumber(getVariableByName('allocationComparisonChart/gap', modes), 8)
310
+
311
+ const currentColor =
312
+ (getVariableByName('segmentIndicator/indicator/background', modes) as string | null) ??
313
+ '#5d00b5'
314
+ const baselineColor =
315
+ (getVariableByName('segmentIndicator/indicator/foreground', modes) as string | null) ??
316
+ '#b84fbd'
317
+ const lineColor =
318
+ (getVariableByName('segmentIndicator/indicator/line/color', modes) as string | null) ??
319
+ '#ffffff'
320
+ const lineSize = toNumber(getVariableByName('segmentIndicator/indicator/line/size', modes), 1)
321
+
322
+ const foreground =
323
+ (getVariableByName('segmentIndicator/foreground', modes) as string | null) ?? '#0c0d10'
324
+ const fontFamily =
325
+ (getVariableByName('segmentIndicator/fontFamily', modes) as string | null) ?? 'JioType Var'
326
+ const fontSize = toNumber(getVariableByName('segmentIndicator/fontSize', modes), 12)
327
+ const lineHeight = toNumber(getVariableByName('segmentIndicator/lineHeight', modes), 12)
328
+ const fontWeight = toFontWeight(getVariableByName('segmentIndicator/fontWeight', modes), '400')
329
+
330
+ const labelStyle: TextStyle = {
331
+ color: foreground,
332
+ fontFamily,
333
+ fontSize,
334
+ lineHeight,
335
+ fontWeight,
336
+ textAlign: 'center',
337
+ }
338
+
339
+ const computedMax =
340
+ max ??
341
+ data.reduce(
342
+ (acc, seg) => Math.max(acc, seg.value, seg.baseline ?? 0),
343
+ 0
344
+ )
345
+ const safeMax = computedMax > 0 ? computedMax : 1
346
+
347
+ const firstBaselineIndex = data.findIndex(
348
+ (seg) => typeof seg.baseline === 'number'
349
+ )
350
+ const hasAnyBaseline = firstBaselineIndex !== -1
351
+
352
+ const theme: SegmentTheme = {
353
+ barWidth: resolvedBarWidth,
354
+ pillRadius,
355
+ gap,
356
+ currentColor,
357
+ baselineColor,
358
+ lineColor,
359
+ lineSize,
360
+ labelStyle,
361
+ }
362
+
363
+ const defaultAccessibilityLabel =
364
+ accessibilityLabel ??
365
+ `Allocation comparison of ${data.length} segment${data.length === 1 ? '' : 's'}: ` +
366
+ data
367
+ .map((seg) => {
368
+ const label = typeof seg.label === 'string' ? seg.label : 'segment'
369
+ const base =
370
+ typeof seg.baseline === 'number'
371
+ ? `, recommended ${seg.baseline}`
372
+ : ''
373
+ return `${label} ${seg.value}${base}`
374
+ })
375
+ .join('; ')
376
+
377
+ return (
378
+ <View style={[{ width: '100%' }, style]} accessibilityLabel={defaultAccessibilityLabel}>
379
+ {showLegend ? (
380
+ <View
381
+ style={[
382
+ { flexDirection: 'row', alignItems: 'center', gap: 8, marginBottom: chartGap },
383
+ legendStyle,
384
+ ]}
385
+ >
386
+ <MetricLegendItem
387
+ label={valueLegendLabel}
388
+ indicatorColor={currentColor}
389
+ modes={modes}
390
+ style={{ flexGrow: 0, flexShrink: 1 }}
391
+ />
392
+ {hasAnyBaseline ? (
393
+ <MetricLegendItem
394
+ label={baselineLegendLabel}
395
+ indicatorColor={baselineColor}
396
+ modes={modes}
397
+ style={{ flexGrow: 0, flexShrink: 1 }}
398
+ />
399
+ ) : null}
400
+ </View>
401
+ ) : null}
402
+
403
+ <View
404
+ accessibilityRole="image"
405
+ style={[
406
+ { flexDirection: 'row', alignItems: 'flex-end', gap: 8, width: '100%' },
407
+ chartStyle,
408
+ ]}
409
+ >
410
+ {data.map((segment, index) => {
411
+ const ratio = Math.max(0, Math.min(1, segment.value / safeMax))
412
+ const barHeightPx = Math.max(0, height * ratio)
413
+ const baselineHeightPx =
414
+ typeof segment.baseline === 'number'
415
+ ? Math.max(0, Math.min(1, segment.baseline / safeMax)) * height
416
+ : null
417
+ const baselineLabel =
418
+ segment.baselineLabel === undefined
419
+ ? typeof segment.baseline === 'number'
420
+ ? formatValue(segment.baseline)
421
+ : undefined
422
+ : segment.baselineLabel
423
+ const resolvedSegment: AllocationSegment = {
424
+ ...segment,
425
+ valueLabel:
426
+ segment.valueLabel === undefined
427
+ ? formatValue(segment.value)
428
+ : segment.valueLabel,
429
+ }
430
+ const showMarker =
431
+ segment.showMarker ?? index === firstBaselineIndex
432
+
433
+ return (
434
+ <SegmentBar
435
+ key={segment.key ?? `segment-${index}`}
436
+ segment={resolvedSegment}
437
+ barHeightPx={barHeightPx}
438
+ baselineHeightPx={baselineHeightPx}
439
+ baselineLabel={baselineLabel}
440
+ showMarker={showMarker}
441
+ theme={theme}
442
+ />
443
+ )
444
+ })}
445
+ </View>
446
+ </View>
447
+ )
448
+ }
449
+
450
+ export default AllocationComparisonChart