jfs-components 0.0.72 → 0.0.73

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 (116) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/lib/commonjs/components/AccordionCheckbox/AccordionCheckbox.js +239 -0
  3. package/lib/commonjs/components/BrandChip/BrandChip.js +149 -0
  4. package/lib/commonjs/components/CardBankAccount/CardBankAccount.js +213 -0
  5. package/lib/commonjs/components/CardInsight/CardInsight.js +166 -0
  6. package/lib/commonjs/components/CheckboxGroup/CheckboxGroup.js +67 -0
  7. package/lib/commonjs/components/CheckboxItem/CheckboxItem.js +125 -0
  8. package/lib/commonjs/components/CircularProgressBar/CircularProgressBar.js +56 -9
  9. package/lib/commonjs/components/CoverageBarComparison/CoverageBarComparison.js +272 -0
  10. package/lib/commonjs/components/CoverageRing/CoverageRing.js +141 -0
  11. package/lib/commonjs/components/DonutChart/DonutChart.js +309 -0
  12. package/lib/commonjs/components/DonutChartSummary/DonutChartSummary.js +155 -0
  13. package/lib/commonjs/components/LinearMeter/LinearMeter.js +9 -28
  14. package/lib/commonjs/components/LinearProgress/LinearProgress.js +68 -0
  15. package/lib/commonjs/components/MetricLegendItem/MetricLegendItem.js +95 -0
  16. package/lib/commonjs/components/MonthlyStatusGrid/MonthlyStatusGrid.js +286 -0
  17. package/lib/commonjs/components/OTP/OTP.js +381 -37
  18. package/lib/commonjs/components/ProductOverview/ProductOverview.js +147 -0
  19. package/lib/commonjs/components/RangeTrack/RangeTrack.js +269 -0
  20. package/lib/commonjs/components/SavingsGoalSummary/SavingsGoalSummary.js +181 -0
  21. package/lib/commonjs/components/SegmentedTrack/SegmentedTrack.js +171 -0
  22. package/lib/commonjs/components/StatGroup/StatGroup.js +128 -0
  23. package/lib/commonjs/components/StatItem/StatItem.js +65 -35
  24. package/lib/commonjs/components/StrengthIndicator/StrengthIndicator.js +157 -0
  25. package/lib/commonjs/components/SummaryTile/SummaryTile.js +150 -0
  26. package/lib/commonjs/components/index.js +171 -1
  27. package/lib/commonjs/design-tokens/Coin Variables-variables-full.json +1 -1
  28. package/lib/commonjs/icons/registry.js +1 -1
  29. package/lib/commonjs/utils/index.js +7 -0
  30. package/lib/commonjs/utils/number-utils.js +57 -0
  31. package/lib/module/components/AccordionCheckbox/AccordionCheckbox.js +233 -0
  32. package/lib/module/components/BrandChip/BrandChip.js +143 -0
  33. package/lib/module/components/CardBankAccount/CardBankAccount.js +208 -0
  34. package/lib/module/components/CardInsight/CardInsight.js +161 -0
  35. package/lib/module/components/CheckboxGroup/CheckboxGroup.js +62 -0
  36. package/lib/module/components/CheckboxItem/CheckboxItem.js +119 -0
  37. package/lib/module/components/CircularProgressBar/CircularProgressBar.js +56 -9
  38. package/lib/module/components/CoverageBarComparison/CoverageBarComparison.js +266 -0
  39. package/lib/module/components/CoverageRing/CoverageRing.js +136 -0
  40. package/lib/module/components/DonutChart/DonutChart.js +303 -0
  41. package/lib/module/components/DonutChartSummary/DonutChartSummary.js +150 -0
  42. package/lib/module/components/LinearMeter/LinearMeter.js +9 -28
  43. package/lib/module/components/LinearProgress/LinearProgress.js +63 -0
  44. package/lib/module/components/MetricLegendItem/MetricLegendItem.js +90 -0
  45. package/lib/module/components/MonthlyStatusGrid/MonthlyStatusGrid.js +281 -0
  46. package/lib/module/components/OTP/OTP.js +381 -38
  47. package/lib/module/components/ProductOverview/ProductOverview.js +142 -0
  48. package/lib/module/components/RangeTrack/RangeTrack.js +263 -0
  49. package/lib/module/components/SavingsGoalSummary/SavingsGoalSummary.js +175 -0
  50. package/lib/module/components/SegmentedTrack/SegmentedTrack.js +166 -0
  51. package/lib/module/components/StatGroup/StatGroup.js +123 -0
  52. package/lib/module/components/StatItem/StatItem.js +66 -36
  53. package/lib/module/components/StrengthIndicator/StrengthIndicator.js +152 -0
  54. package/lib/module/components/SummaryTile/SummaryTile.js +145 -0
  55. package/lib/module/components/index.js +21 -1
  56. package/lib/module/design-tokens/Coin Variables-variables-full.json +1 -1
  57. package/lib/module/icons/registry.js +1 -1
  58. package/lib/module/utils/index.js +2 -1
  59. package/lib/module/utils/number-utils.js +53 -0
  60. package/lib/typescript/src/components/AccordionCheckbox/AccordionCheckbox.d.ts +71 -0
  61. package/lib/typescript/src/components/BrandChip/BrandChip.d.ts +43 -0
  62. package/lib/typescript/src/components/CardBankAccount/CardBankAccount.d.ts +79 -0
  63. package/lib/typescript/src/components/CardInsight/CardInsight.d.ts +48 -0
  64. package/lib/typescript/src/components/CheckboxGroup/CheckboxGroup.d.ts +41 -0
  65. package/lib/typescript/src/components/CheckboxItem/CheckboxItem.d.ts +56 -0
  66. package/lib/typescript/src/components/CircularProgressBar/CircularProgressBar.d.ts +11 -1
  67. package/lib/typescript/src/components/CoverageBarComparison/CoverageBarComparison.d.ts +105 -0
  68. package/lib/typescript/src/components/CoverageRing/CoverageRing.d.ts +90 -0
  69. package/lib/typescript/src/components/DonutChart/DonutChart.d.ts +117 -0
  70. package/lib/typescript/src/components/DonutChartSummary/DonutChartSummary.d.ts +103 -0
  71. package/lib/typescript/src/components/LinearProgress/LinearProgress.d.ts +17 -0
  72. package/lib/typescript/src/components/MetricLegendItem/MetricLegendItem.d.ts +37 -0
  73. package/lib/typescript/src/components/MonthlyStatusGrid/MonthlyStatusGrid.d.ts +119 -0
  74. package/lib/typescript/src/components/OTP/OTP.d.ts +88 -2
  75. package/lib/typescript/src/components/ProductOverview/ProductOverview.d.ts +39 -0
  76. package/lib/typescript/src/components/RangeTrack/RangeTrack.d.ts +173 -0
  77. package/lib/typescript/src/components/SavingsGoalSummary/SavingsGoalSummary.d.ts +95 -0
  78. package/lib/typescript/src/components/SegmentedTrack/SegmentedTrack.d.ts +108 -0
  79. package/lib/typescript/src/components/StatGroup/StatGroup.d.ts +45 -0
  80. package/lib/typescript/src/components/StatItem/StatItem.d.ts +24 -7
  81. package/lib/typescript/src/components/StrengthIndicator/StrengthIndicator.d.ts +58 -0
  82. package/lib/typescript/src/components/SummaryTile/SummaryTile.d.ts +60 -0
  83. package/lib/typescript/src/components/index.d.ts +22 -2
  84. package/lib/typescript/src/icons/registry.d.ts +1 -1
  85. package/lib/typescript/src/utils/index.d.ts +1 -0
  86. package/lib/typescript/src/utils/number-utils.d.ts +29 -0
  87. package/package.json +1 -1
  88. package/src/components/AccordionCheckbox/AccordionCheckbox.tsx +323 -0
  89. package/src/components/BrandChip/BrandChip.tsx +235 -0
  90. package/src/components/CardBankAccount/CardBankAccount.tsx +295 -0
  91. package/src/components/CardInsight/CardInsight.tsx +239 -0
  92. package/src/components/CheckboxGroup/CheckboxGroup.tsx +86 -0
  93. package/src/components/CheckboxItem/CheckboxItem.tsx +174 -0
  94. package/src/components/CircularProgressBar/CircularProgressBar.tsx +74 -9
  95. package/src/components/CoverageBarComparison/CoverageBarComparison.tsx +378 -0
  96. package/src/components/CoverageRing/CoverageRing.tsx +225 -0
  97. package/src/components/DonutChart/DonutChart.tsx +503 -0
  98. package/src/components/DonutChartSummary/DonutChartSummary.tsx +256 -0
  99. package/src/components/LinearMeter/LinearMeter.tsx +9 -39
  100. package/src/components/LinearProgress/LinearProgress.tsx +92 -0
  101. package/src/components/MetricLegendItem/MetricLegendItem.tsx +167 -0
  102. package/src/components/MonthlyStatusGrid/MonthlyStatusGrid.tsx +438 -0
  103. package/src/components/OTP/OTP.tsx +476 -29
  104. package/src/components/ProductOverview/ProductOverview.tsx +236 -0
  105. package/src/components/RangeTrack/RangeTrack.tsx +394 -0
  106. package/src/components/SavingsGoalSummary/SavingsGoalSummary.tsx +269 -0
  107. package/src/components/SegmentedTrack/SegmentedTrack.tsx +268 -0
  108. package/src/components/StatGroup/StatGroup.tsx +169 -0
  109. package/src/components/StatItem/StatItem.tsx +117 -40
  110. package/src/components/StrengthIndicator/StrengthIndicator.tsx +205 -0
  111. package/src/components/SummaryTile/SummaryTile.tsx +251 -0
  112. package/src/components/index.ts +32 -2
  113. package/src/design-tokens/Coin Variables-variables-full.json +1 -1
  114. package/src/icons/registry.ts +1 -1
  115. package/src/utils/index.ts +1 -0
  116. package/src/utils/number-utils.ts +60 -0
@@ -0,0 +1,438 @@
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 { getVariableByName } from '../../design-tokens/figma-variables-resolver'
10
+ import { useTokens } from '../../design-tokens/JFSThemeProvider'
11
+ import { EMPTY_MODES } from '../../utils/react-utils'
12
+ import MetricLegendItem from '../MetricLegendItem/MetricLegendItem'
13
+
14
+ /**
15
+ * The three semantic states a calendar glyph can be in. Maps 1:1 to the
16
+ * Figma `Calendar Glyph State` collection modes (`Idle`, `notSaved`,
17
+ * `saved`) and drives both the glyph fill/foreground tokens and the
18
+ * matching legend dot.
19
+ */
20
+ export type MonthlyStatus = 'Idle' | 'notSaved' | 'saved'
21
+
22
+ /** One month entry in the grid. */
23
+ export type MonthlyStatusGridMonth = {
24
+ /** Stable React key. */
25
+ key?: React.Key
26
+ /**
27
+ * Text rendered inside the glyph (e.g. `'Jan'`). When omitted, defaults
28
+ * to a 3-letter English month name based on the entry's index.
29
+ */
30
+ label?: React.ReactNode
31
+ /**
32
+ * Semantic status. Drives the glyph background/foreground colors and the
33
+ * legend dot via the Figma `Calendar Glyph State` collection.
34
+ */
35
+ status: MonthlyStatus
36
+ /**
37
+ * Per-month design token mode overrides. Merged on top of the parent
38
+ * `modes` and the per-month `Calendar Glyph State` derived from `status`.
39
+ */
40
+ modes?: Record<string, any>
41
+ /** Per-month accessibility label. */
42
+ accessibilityLabel?: string
43
+ }
44
+
45
+ export type MonthlyStatusGridProps = {
46
+ /**
47
+ * The months to render. Layout flows left-to-right, top-to-bottom into
48
+ * `columns` per row. Defaults to 12 `Idle` months (Jan…Dec) so the
49
+ * component renders something meaningful with no props.
50
+ */
51
+ months?: MonthlyStatusGridMonth[]
52
+ /**
53
+ * Number of glyphs per row. Default `4` (matching the Figma reference of
54
+ * 12 months × 4 columns = 3 rows).
55
+ */
56
+ columns?: number
57
+ /**
58
+ * Override the legend labels. Pass `false` to hide the legend entirely.
59
+ * Missing keys fall back to the defaults: `No data`, `Not saved`,
60
+ * `Saved`.
61
+ */
62
+ legend?: false | Partial<Record<MonthlyStatus, React.ReactNode>>
63
+ /**
64
+ * Restrict which legend entries are shown and in what order. Defaults to
65
+ * all three statuses in `[Idle, notSaved, saved]` order — matching the
66
+ * Figma reference.
67
+ */
68
+ legendStatuses?: MonthlyStatus[]
69
+ /** Design token modes (e.g. `{ 'Color Mode': 'Light' }`). */
70
+ modes?: Record<string, any>
71
+ /** Container style override. */
72
+ style?: StyleProp<ViewStyle>
73
+ /** Style applied to the months section (the rows wrapper). */
74
+ monthsStyle?: StyleProp<ViewStyle>
75
+ /** Style applied to each row of glyphs. */
76
+ rowStyle?: StyleProp<ViewStyle>
77
+ /** Style applied to every calendar glyph. */
78
+ glyphStyle?: StyleProp<ViewStyle>
79
+ /** Style applied to the glyph label text. */
80
+ labelStyle?: StyleProp<TextStyle>
81
+ /** Style applied to the legend row. */
82
+ legendStyle?: StyleProp<ViewStyle>
83
+ /** Accessibility label for the entire component. */
84
+ accessibilityLabel?: string
85
+ }
86
+
87
+ const DEFAULT_MONTH_LABELS: readonly string[] = [
88
+ 'Jan',
89
+ 'Feb',
90
+ 'Mar',
91
+ 'Apr',
92
+ 'May',
93
+ 'Jun',
94
+ 'Jul',
95
+ 'Aug',
96
+ 'Sep',
97
+ 'Oct',
98
+ 'Nov',
99
+ 'Dec',
100
+ ]
101
+
102
+ const DEFAULT_LEGEND_LABELS: Readonly<Record<MonthlyStatus, string>> = Object.freeze({
103
+ Idle: 'No data',
104
+ notSaved: 'Not saved',
105
+ saved: 'Saved',
106
+ })
107
+
108
+ const DEFAULT_LEGEND_STATUSES: readonly MonthlyStatus[] = ['Idle', 'notSaved', 'saved']
109
+
110
+ const DEFAULT_MONTHS: MonthlyStatusGridMonth[] = DEFAULT_MONTH_LABELS.map((label) => ({
111
+ label,
112
+ status: 'Idle' as const,
113
+ }))
114
+
115
+ /**
116
+ * Default tokens used as the safe fallbacks when token resolution returns
117
+ * `null` (e.g. modes are missing or the resolver hasn't been initialized).
118
+ * They mirror the Figma source values for the `Calendar Glyph State` modes.
119
+ */
120
+ const FALLBACK_BG: Record<MonthlyStatus, string> = {
121
+ Idle: '#e0e0e3',
122
+ notSaved: '#b84fbd',
123
+ saved: '#5d00b5',
124
+ }
125
+
126
+ const FALLBACK_FG: Record<MonthlyStatus, string> = {
127
+ Idle: '#000000',
128
+ notSaved: '#ffffff',
129
+ saved: '#ffffff',
130
+ }
131
+
132
+ const toNumber = (value: unknown, fallback: number) => {
133
+ if (typeof value === 'number') {
134
+ return Number.isFinite(value) ? value : fallback
135
+ }
136
+ if (typeof value === 'string') {
137
+ const parsed = Number(value)
138
+ return Number.isFinite(parsed) ? parsed : fallback
139
+ }
140
+ return fallback
141
+ }
142
+
143
+ const toFontWeight = (
144
+ value: unknown,
145
+ fallback: TextStyle['fontWeight']
146
+ ): TextStyle['fontWeight'] => {
147
+ if (typeof value === 'number') {
148
+ return String(value) as TextStyle['fontWeight']
149
+ }
150
+ if (typeof value === 'string') {
151
+ return value as TextStyle['fontWeight']
152
+ }
153
+ return fallback
154
+ }
155
+
156
+ export type CalendarGlyphProps = {
157
+ /** Text rendered inside the glyph (e.g. `'Jan'`). */
158
+ label?: React.ReactNode
159
+ /**
160
+ * Semantic status — drives the glyph fill/foreground via the Figma
161
+ * `Calendar Glyph State` collection.
162
+ */
163
+ status?: MonthlyStatus
164
+ /** Design token mode overrides. */
165
+ modes?: Record<string, any>
166
+ /** Container style override. */
167
+ style?: StyleProp<ViewStyle>
168
+ /** Label text style override. */
169
+ labelStyle?: StyleProp<TextStyle>
170
+ /** Accessibility label. Defaults to `"{label}, {status}"`. */
171
+ accessibilityLabel?: string
172
+ }
173
+
174
+ /**
175
+ * Single calendar-month glyph: a small pill with a status-driven background
176
+ * and foreground, and a centered month label. Exposed for callers who want
177
+ * to compose glyphs themselves; most consumers should use
178
+ * {@link MonthlyStatusGrid} instead, which keeps the count, layout and
179
+ * legend in sync automatically.
180
+ */
181
+ function CalendarGlyph({
182
+ label = 'Jan',
183
+ status = 'Idle',
184
+ modes: propModes = EMPTY_MODES,
185
+ style,
186
+ labelStyle,
187
+ accessibilityLabel,
188
+ }: CalendarGlyphProps) {
189
+ const { modes: globalModes } = useTokens()
190
+ const modes = React.useMemo(
191
+ () => ({ ...globalModes, ...propModes, 'Calendar Glyph State': status }),
192
+ [globalModes, propModes, status]
193
+ )
194
+
195
+ const width = toNumber(getVariableByName('calendarGlyph/width', modes), 46)
196
+ const height = toNumber(getVariableByName('calendarGlyph/height', modes), 46)
197
+ const radius = toNumber(getVariableByName('calendarGlyph/radius', modes), 29)
198
+ const paddingTop = toNumber(getVariableByName('calendarGlyph/padding/top', modes), 16)
199
+ const paddingBottom = toNumber(getVariableByName('calendarGlyph/padding/bottom', modes), 16)
200
+ const paddingLeft = toNumber(getVariableByName('calendarGlyph/padding/left', modes), 13)
201
+ const paddingRight = toNumber(getVariableByName('calendarGlyph/padding/right', modes), 12)
202
+ const gap = toNumber(getVariableByName('calendarGlyph/gap', modes), 0)
203
+
204
+ const background =
205
+ (getVariableByName('calendarGlyph/background', modes) as string | null) ??
206
+ FALLBACK_BG[status]
207
+ const foreground =
208
+ (getVariableByName('calendarGlyph/foreground', modes) as string | null) ??
209
+ FALLBACK_FG[status]
210
+
211
+ const fontFamily =
212
+ (getVariableByName('calendarGlyph/fontFamily', modes) as string | null) ?? 'JioType Var'
213
+ const fontSize = toNumber(getVariableByName('calendarGlyph/fontSize', modes), 12)
214
+ const lineHeight = toNumber(getVariableByName('calendarGlyph/lineHeight', modes), 14)
215
+ const fontWeight = toFontWeight(getVariableByName('calendarGlyph/fontWeight', modes), '500')
216
+
217
+ const defaultAccessibilityLabel =
218
+ accessibilityLabel ??
219
+ `${typeof label === 'string' ? label : 'Month'}, ${DEFAULT_LEGEND_LABELS[status].toLowerCase()}`
220
+
221
+ return (
222
+ <View
223
+ accessibilityLabel={defaultAccessibilityLabel}
224
+ style={[
225
+ {
226
+ // Use min* so the pill is always at least the Figma size, but
227
+ // grows naturally when the label is wider (e.g. localized
228
+ // months). Avoids the ellipsis-truncation that happens when a
229
+ // 3-letter label barely overflows the strict inner width.
230
+ minWidth: width,
231
+ minHeight: height,
232
+ borderRadius: radius,
233
+ backgroundColor: background,
234
+ paddingTop,
235
+ paddingBottom,
236
+ paddingLeft,
237
+ paddingRight,
238
+ alignItems: 'center',
239
+ justifyContent: 'center',
240
+ gap,
241
+ },
242
+ style,
243
+ ]}
244
+ >
245
+ <Text
246
+ numberOfLines={1}
247
+ style={[
248
+ {
249
+ color: foreground,
250
+ fontFamily,
251
+ fontSize,
252
+ lineHeight,
253
+ fontWeight,
254
+ textAlign: 'center',
255
+ },
256
+ labelStyle,
257
+ ]}
258
+ >
259
+ {label}
260
+ </Text>
261
+ </View>
262
+ )
263
+ }
264
+
265
+ /**
266
+ * Resolve the canonical color pair for a status. Used by both the glyph and
267
+ * the matching legend item so they can never visually drift apart.
268
+ */
269
+ function resolveStatusColors(
270
+ status: MonthlyStatus,
271
+ modes: Record<string, any>
272
+ ): { bg: string; fg: string; statusModes: Record<string, any> } {
273
+ const statusModes = { ...modes, 'Calendar Glyph State': status }
274
+ const bg =
275
+ (getVariableByName('calendarGlyph/background', statusModes) as string | null) ??
276
+ FALLBACK_BG[status]
277
+ const fg =
278
+ (getVariableByName('calendarGlyph/foreground', statusModes) as string | null) ??
279
+ FALLBACK_FG[status]
280
+ return { bg, fg, statusModes }
281
+ }
282
+
283
+ /**
284
+ * `MonthlyStatusGrid` shows a year (or any contiguous range of months) as a
285
+ * grid of small pill-shaped calendar glyphs, each colored by its semantic
286
+ * status (`Idle` = "No data", `notSaved` = "Not saved", `saved` = "Saved").
287
+ * A legend below the grid explains the colors.
288
+ *
289
+ * Cohesiveness guarantees:
290
+ * - The number of glyphs is **always** `months.length`. There is no way to
291
+ * render a glyph that has no underlying status.
292
+ * - The legend dot color for a status is resolved through the **same**
293
+ * `calendarGlyph/background` token + `Calendar Glyph State` mode as the
294
+ * glyph itself, so the visual mapping cannot drift.
295
+ *
296
+ * The chart is rendered with plain RN primitives (`View` + `Text`); SVG is
297
+ * unnecessary here because the glyph shape is a simple rounded rectangle.
298
+ *
299
+ * @component
300
+ */
301
+ function MonthlyStatusGrid({
302
+ months = DEFAULT_MONTHS,
303
+ columns = 4,
304
+ legend,
305
+ legendStatuses = DEFAULT_LEGEND_STATUSES as MonthlyStatus[],
306
+ modes: propModes = EMPTY_MODES,
307
+ style,
308
+ monthsStyle,
309
+ rowStyle,
310
+ glyphStyle,
311
+ labelStyle,
312
+ legendStyle,
313
+ accessibilityLabel,
314
+ }: MonthlyStatusGridProps) {
315
+ const { modes: globalModes } = useTokens()
316
+ const modes = React.useMemo(
317
+ () => ({ ...globalModes, ...propModes }),
318
+ [globalModes, propModes]
319
+ )
320
+
321
+ const gridGap = toNumber(getVariableByName('monthlyStatusGrid/gap', modes), 16)
322
+ const rowGap = toNumber(getVariableByName('monthlyStatusGrid/months/gap', modes), 12)
323
+
324
+ const safeColumns = Math.max(1, Math.floor(columns))
325
+
326
+ // Group months into rows of `safeColumns`. Render-time only; cheap.
327
+ const rows = React.useMemo(() => {
328
+ const out: MonthlyStatusGridMonth[][] = []
329
+ for (let i = 0; i < months.length; i += safeColumns) {
330
+ out.push(months.slice(i, i + safeColumns))
331
+ }
332
+ return out
333
+ }, [months, safeColumns])
334
+
335
+ const showLegend = legend !== false
336
+ const legendOverrides: Partial<Record<MonthlyStatus, React.ReactNode>> =
337
+ legend && typeof legend === 'object' ? legend : {}
338
+
339
+ const defaultAccessibilityLabel =
340
+ accessibilityLabel ??
341
+ `Monthly status grid: ${months.length} month${months.length === 1 ? '' : 's'}`
342
+
343
+ return (
344
+ <View
345
+ accessibilityLabel={defaultAccessibilityLabel}
346
+ style={[
347
+ {
348
+ width: '100%',
349
+ gap: gridGap,
350
+ alignItems: 'stretch',
351
+ justifyContent: 'center',
352
+ },
353
+ style,
354
+ ]}
355
+ >
356
+ <View
357
+ style={[
358
+ {
359
+ width: '100%',
360
+ gap: rowGap,
361
+ alignItems: 'stretch',
362
+ },
363
+ monthsStyle,
364
+ ]}
365
+ >
366
+ {rows.map((row, rowIndex) => (
367
+ <View
368
+ key={`row-${rowIndex}`}
369
+ style={[
370
+ {
371
+ flexDirection: 'row',
372
+ alignItems: 'center',
373
+ justifyContent: 'space-between',
374
+ width: '100%',
375
+ },
376
+ rowStyle,
377
+ ]}
378
+ >
379
+ {row.map((month, colIndex) => {
380
+ const absoluteIndex = rowIndex * safeColumns + colIndex
381
+ const fallbackLabel =
382
+ DEFAULT_MONTH_LABELS[absoluteIndex % DEFAULT_MONTH_LABELS.length]
383
+ const label = month.label ?? fallbackLabel
384
+ const glyphModes = month.modes
385
+ ? { ...modes, ...month.modes }
386
+ : modes
387
+ return (
388
+ <CalendarGlyph
389
+ key={month.key ?? `glyph-${absoluteIndex}`}
390
+ label={label}
391
+ status={month.status}
392
+ modes={glyphModes}
393
+ {...(glyphStyle !== undefined ? { style: glyphStyle } : {})}
394
+ {...(labelStyle !== undefined ? { labelStyle } : {})}
395
+ {...(month.accessibilityLabel !== undefined
396
+ ? { accessibilityLabel: month.accessibilityLabel }
397
+ : {})}
398
+ />
399
+ )
400
+ })}
401
+ </View>
402
+ ))}
403
+ </View>
404
+
405
+ {showLegend ? (
406
+ <View
407
+ style={[
408
+ {
409
+ flexDirection: 'row',
410
+ alignItems: 'center',
411
+ justifyContent: 'space-between',
412
+ width: '100%',
413
+ },
414
+ legendStyle,
415
+ ]}
416
+ >
417
+ {legendStatuses.map((status) => {
418
+ const { bg, statusModes } = resolveStatusColors(status, modes)
419
+ const label =
420
+ legendOverrides[status] ?? DEFAULT_LEGEND_LABELS[status]
421
+ return (
422
+ <MetricLegendItem
423
+ key={`legend-${status}`}
424
+ label={label}
425
+ indicatorColor={bg}
426
+ modes={statusModes}
427
+ style={{ flex: 1, minWidth: 0 }}
428
+ />
429
+ )
430
+ })}
431
+ </View>
432
+ ) : null}
433
+ </View>
434
+ )
435
+ }
436
+
437
+ export { CalendarGlyph }
438
+ export default MonthlyStatusGrid