@titan-design/react-ui 0.10.0 → 0.11.0

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 (84) hide show
  1. package/dist/index.d.mts +399 -4
  2. package/dist/index.d.ts +399 -4
  3. package/dist/index.js +2295 -895
  4. package/dist/index.js.map +1 -1
  5. package/dist/index.mjs +2279 -905
  6. package/dist/index.mjs.map +1 -1
  7. package/dist/{pages-DyEx-lBf.d.ts → pages-B1hqnGIC.d.ts} +149 -15
  8. package/dist/{pages-D7hOD4Vj.d.mts → pages-CvsFqNtx.d.mts} +149 -15
  9. package/dist/pages.d.mts +1 -1
  10. package/dist/pages.d.ts +1 -1
  11. package/dist/pages.js +1974 -1740
  12. package/dist/pages.js.map +1 -1
  13. package/dist/pages.mjs +1975 -1741
  14. package/dist/pages.mjs.map +1 -1
  15. package/package.json +8 -3
  16. package/src/components/custom/Fatigue/DualGhostSpark.stories.tsx +258 -0
  17. package/src/components/custom/Fatigue/DualGhostSpark.test.tsx +138 -0
  18. package/src/components/custom/Fatigue/DualGhostSpark.tsx +208 -0
  19. package/src/components/custom/Fatigue/FatigueLights.stories.tsx +74 -0
  20. package/src/components/custom/Fatigue/FatigueLights.test.tsx +33 -0
  21. package/src/components/custom/Fatigue/FatigueLights.tsx +83 -0
  22. package/src/components/custom/Fatigue/GhostBand.test.tsx +235 -0
  23. package/src/components/custom/Fatigue/GhostBand.tsx +246 -0
  24. package/src/components/custom/Fatigue/GhostBloom.tsx +132 -0
  25. package/src/components/custom/Fatigue/GhostSpark.stories.tsx +175 -0
  26. package/src/components/custom/Fatigue/GhostSpark.test.tsx +45 -0
  27. package/src/components/custom/Fatigue/GhostSpark.tsx +126 -0
  28. package/src/components/custom/Fatigue/LiveFatigueCard.stories.tsx +126 -0
  29. package/src/components/custom/Fatigue/LiveFatigueCard.test.tsx +28 -0
  30. package/src/components/custom/Fatigue/LiveFatigueCard.tsx +86 -0
  31. package/src/components/custom/Fatigue/LiveFatiguePanel.stories.tsx +95 -0
  32. package/src/components/custom/Fatigue/LiveFatiguePanel.tsx +151 -0
  33. package/src/components/custom/Fatigue/README.md +93 -0
  34. package/src/components/custom/Fatigue/RomProgressionChart.stories.tsx +94 -0
  35. package/src/components/custom/Fatigue/RomProgressionChart.test.tsx +31 -0
  36. package/src/components/custom/Fatigue/RomProgressionChart.tsx +132 -0
  37. package/src/components/custom/Fatigue/SilverRedPalette.stories.tsx +226 -0
  38. package/src/components/custom/Fatigue/VelocityHero.stories.tsx +70 -0
  39. package/src/components/custom/Fatigue/VelocityHero.test.tsx +26 -0
  40. package/src/components/custom/Fatigue/VelocityHero.tsx +46 -0
  41. package/src/components/custom/Fatigue/VerdictHero.stories.tsx +59 -0
  42. package/src/components/custom/Fatigue/VerdictHero.test.tsx +49 -0
  43. package/src/components/custom/Fatigue/VerdictHero.tsx +79 -0
  44. package/src/components/custom/Fatigue/fatigue-mock.test.ts +59 -0
  45. package/src/components/custom/Fatigue/fatigue-mock.ts +268 -0
  46. package/src/components/custom/Fatigue/fatigue-model.ts +129 -0
  47. package/src/components/custom/Fatigue/fatigue-tokens.test.ts +70 -0
  48. package/src/components/custom/Fatigue/fatigue-tokens.ts +180 -0
  49. package/src/components/custom/Fatigue/index.ts +41 -0
  50. package/src/components/custom/Fatigue/tempo-pacing.test.ts +176 -0
  51. package/src/components/custom/Fatigue/tempo-pacing.ts +112 -0
  52. package/src/components/custom/Workout/DualVelocityStrip.test.tsx +685 -0
  53. package/src/components/custom/Workout/ExerciseCard.test.tsx +4 -4
  54. package/src/components/custom/Workout/ExerciseCard.tsx +3 -1
  55. package/src/components/custom/Workout/ExerciseDetailPage.tsx +7 -1
  56. package/src/components/custom/Workout/README.md +62 -16
  57. package/src/components/custom/Workout/SegmentedBar.stories.tsx +3 -3
  58. package/src/components/custom/Workout/SessionRail.test.tsx +43 -0
  59. package/src/components/custom/Workout/SessionRail.tsx +50 -13
  60. package/src/components/custom/Workout/SetRow.test.tsx +6 -6
  61. package/src/components/custom/Workout/SetRow.tsx +9 -2
  62. package/src/components/custom/Workout/VelocityStrip.compact.stories.tsx +191 -0
  63. package/src/components/custom/Workout/VelocityStrip.dual.stories.tsx +186 -0
  64. package/src/components/custom/Workout/VelocityStrip.expanded.stories.tsx +296 -0
  65. package/src/components/custom/Workout/VelocityStrip.hero.stories.tsx +259 -0
  66. package/src/components/custom/Workout/VelocityStrip.stories.tsx +217 -250
  67. package/src/components/custom/Workout/VelocityStrip.test.tsx +196 -79
  68. package/src/components/custom/Workout/VelocityStrip.tsx +953 -609
  69. package/src/components/custom/Workout/index.ts +3 -0
  70. package/src/components/custom/Workout/velocity-story-kit.tsx +438 -0
  71. package/src/components/custom/charts/SetBarChart.test.tsx +275 -0
  72. package/src/components/custom/charts/SetBarChart.tsx +651 -0
  73. package/src/components/custom/charts/live-rep-growth.ts +91 -0
  74. package/src/components/custom/index.ts +1 -0
  75. package/src/test/raw-color-patterns.test.ts +71 -0
  76. package/src/theme/ColorPalettes.stories.tsx +871 -0
  77. package/src/theme/ColorPrimitives.stories.tsx +300 -0
  78. package/src/theme/bar-paper.ts +59 -0
  79. package/src/theme/color-stories.coverage.test.ts +130 -0
  80. package/src/theme/color-story-kit.tsx +170 -0
  81. package/src/components/custom/Workout/VelocityStrip.modalities.stories.tsx +0 -254
  82. package/src/components/custom/Workout/VelocityStrip.responsive.stories.tsx +0 -145
  83. package/src/theme/ColorSystem.stories.tsx +0 -508
  84. package/src/theme/Colors.stories.tsx +0 -323
@@ -1,508 +0,0 @@
1
- import type { ReactNode } from 'react'
2
- import type { Meta, StoryObj } from '@storybook/react-vite'
3
- import { View, Text } from 'react-native'
4
- import {
5
- primitiveColors,
6
- primitiveRamps,
7
- categoricalPalette,
8
- getCategoricalColor,
9
- CATEGORICAL_CVD_SAFE_MAX,
10
- divergingScale,
11
- sequentialEffort,
12
- bestTextColor,
13
- } from './tokens/primitives'
14
- import { getSemanticColors } from './tokens/semantic'
15
-
16
- const t = getSemanticColors('dark')
17
-
18
- const meta: Meta = {
19
- title: 'Foundations/Color/System',
20
- tags: ['autodocs'],
21
- }
22
- export default meta
23
-
24
- // ---- color math (self-contained; drives the accessibility panel) ----
25
- const hexToRgb = (hex: string): [number, number, number] => {
26
- const h = hex.replace('#', '')
27
- return [0, 2, 4].map((i) => parseInt(h.slice(i, i + 2), 16) / 255) as [number, number, number]
28
- }
29
- const toHex = (v: number) =>
30
- Math.round(Math.min(1, Math.max(0, v)) * 255)
31
- .toString(16)
32
- .padStart(2, '0')
33
- const lin = (c: number) => (c >= 0.04045 ? ((c + 0.055) / 1.055) ** 2.4 : c / 12.92)
34
- const gamma = (c: number) =>
35
- c >= 0.0031308 ? 1.055 * Math.max(0, c) ** (1 / 2.4) - 0.055 : 12.92 * c
36
- const relLum = (hex: string) => {
37
- const [r, g, b] = hexToRgb(hex).map(lin)
38
- return 0.2126 * r + 0.7152 * g + 0.0722 * b
39
- }
40
- const contrast = (a: string, b: string) => {
41
- const [l1, l2] = [relLum(a), relLum(b)].sort((x, y) => y - x)
42
- return (l1 + 0.05) / (l2 + 0.05)
43
- }
44
- const bestText = (hex: string) =>
45
- contrast(hex, '#000000') >= contrast(hex, '#FFFFFF') ? '#000000' : '#FFFFFF'
46
- // Machado-2009 dichromacy simulation (severity 1.0)
47
- const CVD = {
48
- deuteranopia: [
49
- 0.367322, 0.860646, -0.227968, 0.280085, 0.672501, 0.047413, -0.01182, 0.04294, 0.968881,
50
- ],
51
- protanopia: [
52
- 0.152286, 1.052583, -0.204868, 0.114503, 0.786281, 0.099216, -0.003882, -0.048116, 1.051998,
53
- ],
54
- } as const
55
- const simulate = (M: readonly number[], hex: string) => {
56
- const [r, g, b] = hexToRgb(hex).map(lin)
57
- const out = [
58
- M[0] * r + M[1] * g + M[2] * b,
59
- M[3] * r + M[4] * g + M[5] * b,
60
- M[6] * r + M[7] * g + M[8] * b,
61
- ].map(gamma)
62
- return '#' + out.map(toHex).join('')
63
- }
64
-
65
- const STEPS = [50, 100, 200, 300, 400, 500, 600, 700, 800, 900, 950] as const
66
-
67
- function SectionTitle({ children }: { children: string }) {
68
- return <Text className="text-lg font-bold text-text-primary mb-3 mt-2">{children}</Text>
69
- }
70
-
71
- function Ramp({ name, ramp }: { name: string; ramp: Record<number, string> }) {
72
- return (
73
- <View style={{ flexDirection: 'row', alignItems: 'center', gap: 12, marginBottom: 6 }}>
74
- <Text className="text-text-secondary text-xs" style={{ width: 150 }}>
75
- {name}
76
- </Text>
77
- <View
78
- style={{
79
- flexDirection: 'row',
80
- flex: 1,
81
- borderRadius: 6,
82
- overflow: 'hidden',
83
- borderWidth: 1,
84
- borderColor: '#2C2C2C',
85
- }}
86
- >
87
- {STEPS.map((s) => (
88
- <View key={s} style={{ flex: 1, height: 30, backgroundColor: ramp[s] }} />
89
- ))}
90
- </View>
91
- </View>
92
- )
93
- }
94
-
95
- function Chip({ hex, label, sub }: { hex: string; label?: string; sub?: string }) {
96
- return (
97
- <View style={{ alignItems: 'center' }}>
98
- <View
99
- style={{
100
- width: 58,
101
- height: 44,
102
- borderRadius: 8,
103
- backgroundColor: hex,
104
- borderWidth: 1,
105
- borderColor: '#2C2C2C',
106
- alignItems: 'center',
107
- justifyContent: 'center',
108
- }}
109
- >
110
- {label ? (
111
- <Text style={{ color: bestText(hex), fontSize: 11, fontWeight: '700' }}>{label}</Text>
112
- ) : null}
113
- </View>
114
- {sub ? (
115
- <Text className="text-text-secondary" style={{ fontSize: 8, marginTop: 2 }}>
116
- {sub}
117
- </Text>
118
- ) : null}
119
- </View>
120
- )
121
- }
122
-
123
- const RAMP_NAMES: Array<[keyof typeof primitiveRamps, string]> = [
124
- ['red', 'Red'],
125
- ['orange', 'Orange'],
126
- ['amber', 'Amber'],
127
- ['green', 'Green'],
128
- ['cyan', 'Cyan'],
129
- ['blue', 'Blue'],
130
- ['magenta', 'Magenta'],
131
- ]
132
-
133
- const SEM = {
134
- success: primitiveRamps.green[300],
135
- warning: primitiveRamps.amber[300],
136
- error: primitiveRamps.red[600],
137
- info: primitiveRamps.blue[500],
138
- deload: primitiveRamps.magenta[600],
139
- brand: primitiveRamps.orange[400],
140
- brandDark: primitiveRamps.orange[600],
141
- brandSecondary: primitiveRamps.cyan[600],
142
- spinner: primitiveRamps.cyan[600],
143
- neutral: primitiveColors.neutral[500],
144
- } as const
145
-
146
- // BodyMap training-status diverging scale (from the divergingScale token).
147
- const D3_STATUS = divergingScale
148
-
149
- export const Overview: StoryObj = {
150
- render: () => (
151
- <View style={{ padding: 24, gap: 6 }}>
152
- <Text className="text-2xl font-bold text-text-primary mb-1">Color System</Text>
153
- <Text className="text-text-secondary mb-4">
154
- Seven OKLCH tonal ramps and an ordered, colorblind-safe categorical palette. Ramps are the
155
- single source of truth; the categorical references ramp steps.
156
- </Text>
157
-
158
- <SectionTitle>Tonal ramps</SectionTitle>
159
- {RAMP_NAMES.map(([key, name]) => (
160
- <Ramp key={key} name={name} ramp={primitiveRamps[key]} />
161
- ))}
162
-
163
- <SectionTitle>Semantic tokens</SectionTitle>
164
- <View style={{ flexDirection: 'row', flexWrap: 'wrap', gap: 18 }}>
165
- {Object.entries(SEM).map(([label, hex]) => (
166
- <View key={label} style={{ alignItems: 'center' }}>
167
- <Chip hex={hex} />
168
- <Text className="text-text-secondary" style={{ fontSize: 9, marginTop: 3 }}>
169
- {label}
170
- </Text>
171
- </View>
172
- ))}
173
- </View>
174
-
175
- <SectionTitle>Categorical — ordered, nested, CVD-safe to 6</SectionTitle>
176
- {(['default', 'dark'] as const).map((variant) => (
177
- <View key={variant} style={{ marginBottom: 10 }}>
178
- <Text
179
- className="text-text-secondary text-xs mb-1"
180
- style={{ textTransform: 'capitalize' }}
181
- >
182
- {variant}
183
- </Text>
184
- <View style={{ flexDirection: 'row', gap: 6 }}>
185
- {categoricalPalette[variant].map((hex, i) => (
186
- <Chip
187
- key={i}
188
- hex={hex}
189
- label={`${i + 1}`}
190
- sub={i < CATEGORICAL_CVD_SAFE_MAX ? undefined : 'ext'}
191
- />
192
- ))}
193
- </View>
194
- </View>
195
- ))}
196
-
197
- <SectionTitle>Data-viz scales</SectionTitle>
198
- <Text className="text-text-secondary text-xs mb-2">
199
- Diverging (BodyMap training-status) — light-centered so it reads in lightness and stays
200
- colorblind-stable; direction is the blue↔red axis. Labels use per-cell best-contrast text.
201
- </Text>
202
- <View style={{ flexDirection: 'row', gap: 6, marginBottom: 12 }}>
203
- {['under', 'maint', 'optimal', 'approach', 'over'].map((l, i) => (
204
- <View
205
- key={l}
206
- style={{
207
- flex: 1,
208
- height: 40,
209
- borderRadius: 6,
210
- backgroundColor: divergingScale[i],
211
- alignItems: 'center',
212
- justifyContent: 'center',
213
- }}
214
- >
215
- <Text
216
- style={{ color: bestTextColor(divergingScale[i]), fontSize: 10, fontWeight: '700' }}
217
- >
218
- {l}
219
- </Text>
220
- </View>
221
- ))}
222
- </View>
223
- <Text className="text-text-secondary text-xs mb-2">
224
- Sequential effort (low → high) — an ordinal green→yellow→orange→red scale that IntensityBar
225
- uses in full and VelocityStrip/RPE sub-samples.
226
- </Text>
227
- <View style={{ flexDirection: 'row', gap: 4, marginBottom: 4 }}>
228
- {sequentialEffort.map((hex, i) => (
229
- <View
230
- key={i}
231
- style={{
232
- flex: 1,
233
- height: 34,
234
- borderRadius: 5,
235
- backgroundColor: hex,
236
- alignItems: 'center',
237
- justifyContent: 'center',
238
- }}
239
- >
240
- <Text style={{ color: bestTextColor(hex), fontSize: 10, fontWeight: '700' }}>
241
- {i + 1}
242
- </Text>
243
- </View>
244
- ))}
245
- </View>
246
-
247
- <SectionTitle>Component examples — correct color roles</SectionTitle>
248
- <View style={{ flexDirection: 'row', gap: 20, flexWrap: 'wrap', alignItems: 'flex-start' }}>
249
- {/* BodyMap muscle heatmap — training-status diverging (D3) */}
250
- <Demo label="BodyMap · training status">
251
- <View style={{ flexDirection: 'row', flexWrap: 'wrap', width: 132, gap: 4 }}>
252
- {[
253
- ['Chest', 4],
254
- ['Quads', 2],
255
- ['Back', 3],
256
- ['Delts', 0],
257
- ['Biceps', 4],
258
- ['Calves', 1],
259
- ].map(([m, idx]) => (
260
- <View
261
- key={m as string}
262
- style={{
263
- width: 40,
264
- backgroundColor: D3_STATUS[idx as number],
265
- borderRadius: 5,
266
- paddingVertical: 6,
267
- }}
268
- >
269
- <Text
270
- style={{
271
- color: bestTextColor(D3_STATUS[idx as number]),
272
- fontSize: 8,
273
- fontWeight: '700',
274
- textAlign: 'center',
275
- }}
276
- >
277
- {m as string}
278
- </Text>
279
- </View>
280
- ))}
281
- </View>
282
- </Demo>
283
- {/* StatusDot — semantic */}
284
- <Demo label="StatusDot · semantic">
285
- <View style={{ gap: 6 }}>
286
- {[
287
- ['ok', SEM.success],
288
- ['warn', SEM.warning],
289
- ['err', SEM.error],
290
- ['neutral', SEM.neutral],
291
- ].map(([l, c]) => (
292
- <View
293
- key={l as string}
294
- style={{ flexDirection: 'row', alignItems: 'center', gap: 6 }}
295
- >
296
- <View
297
- style={{ width: 12, height: 12, borderRadius: 6, backgroundColor: c as string }}
298
- />
299
- <Text className="text-text-secondary" style={{ fontSize: 11 }}>
300
- {l as string}
301
- </Text>
302
- </View>
303
- ))}
304
- </View>
305
- </Demo>
306
- {/* WorkoutPill — status */}
307
- <Demo label="WorkoutPill">
308
- <View style={{ flexDirection: 'row', gap: 6, flexWrap: 'wrap', maxWidth: 220 }}>
309
- {[
310
- ['Done', SEM.success],
311
- ['Now', SEM.brand],
312
- ['Next', SEM.neutral],
313
- ['Deload', SEM.deload],
314
- ].map(([t, c]) => (
315
- <View
316
- key={t as string}
317
- style={{
318
- borderRadius: 99,
319
- borderWidth: 1,
320
- borderColor: c as string,
321
- paddingHorizontal: 10,
322
- paddingVertical: 3,
323
- }}
324
- >
325
- <Text style={{ color: c as string, fontSize: 11, fontWeight: '700' }}>
326
- {t as string}
327
- </Text>
328
- </View>
329
- ))}
330
- </View>
331
- </Demo>
332
- {/* StrengthTrend legend — series → semantic */}
333
- <Demo label="StrengthTrend · series">
334
- <View style={{ flexDirection: 'row', gap: 12, flexWrap: 'wrap' }}>
335
- {[
336
- ['1RM', SEM.brand],
337
- ['vel', SEM.success],
338
- ['miss', SEM.error],
339
- ['RPE', SEM.warning],
340
- ].map(([l, c]) => (
341
- <View
342
- key={l as string}
343
- style={{ flexDirection: 'row', alignItems: 'center', gap: 4 }}
344
- >
345
- <View
346
- style={{ width: 14, height: 3, backgroundColor: c as string, borderRadius: 2 }}
347
- />
348
- <Text className="text-text-secondary" style={{ fontSize: 10 }}>
349
- {l as string}
350
- </Text>
351
- </View>
352
- ))}
353
- </View>
354
- </Demo>
355
- {/* Treemap — ordered categorical */}
356
- <Demo label="Treemap · categorical">
357
- <View style={{ flexDirection: 'row', flexWrap: 'wrap', width: 140, gap: 2 }}>
358
- {[46, 30, 30, 24, 24, 24, 20].map((h, i) => (
359
- <View
360
- key={i}
361
- style={{
362
- height: h,
363
- flexGrow: 1,
364
- flexBasis: h > 40 ? 60 : 40,
365
- backgroundColor: getCategoricalColor(i),
366
- borderRadius: 3,
367
- }}
368
- />
369
- ))}
370
- </View>
371
- </Demo>
372
- {/* Spinner */}
373
- <Demo label="Spinner">
374
- <View
375
- style={{
376
- width: 24,
377
- height: 24,
378
- borderRadius: 12,
379
- borderWidth: 3,
380
- borderColor: '#2C2C2C',
381
- borderTopColor: SEM.spinner,
382
- }}
383
- />
384
- </Demo>
385
- {/* IntensityBar — full effort scale */}
386
- <Demo label="IntensityBar · effort">
387
- <View style={{ flexDirection: 'row', gap: 2, width: 132 }}>
388
- {sequentialEffort.map((hex, i) => (
389
- <View
390
- key={i}
391
- style={{ flex: 1, height: 20, backgroundColor: hex, borderRadius: 2 }}
392
- />
393
- ))}
394
- </View>
395
- </Demo>
396
- {/* VelocityStrip / RPE — effort sub-sample */}
397
- <Demo label="VelocityStrip · RPE">
398
- <View style={{ flexDirection: 'row', gap: 3 }}>
399
- {[
400
- sequentialEffort[0],
401
- sequentialEffort[2],
402
- sequentialEffort[3],
403
- sequentialEffort[5],
404
- ].map((hex, i) => (
405
- <View
406
- key={i}
407
- style={{
408
- width: 30,
409
- height: 26,
410
- borderRadius: 4,
411
- backgroundColor: hex,
412
- alignItems: 'center',
413
- justifyContent: 'center',
414
- }}
415
- >
416
- <Text style={{ color: bestTextColor(hex), fontSize: 9, fontWeight: '700' }}>
417
- {['S', 'M', 'F', 'X'][i]}
418
- </Text>
419
- </View>
420
- ))}
421
- </View>
422
- </Demo>
423
- </View>
424
- </View>
425
- ),
426
- }
427
-
428
- function Demo({ label, children }: { label: string; children: ReactNode }) {
429
- return (
430
- <View style={{ padding: 10, backgroundColor: t['surface-raised'], borderRadius: 8 }}>
431
- <Text className="text-text-secondary" style={{ fontSize: 9, marginBottom: 6 }}>
432
- {label}
433
- </Text>
434
- {children}
435
- </View>
436
- )
437
- }
438
-
439
- export const Accessibility: StoryObj = {
440
- render: () => (
441
- <View style={{ padding: 24 }}>
442
- <Text className="text-2xl font-bold text-text-primary mb-1">Categorical Accessibility</Text>
443
- <Text className="text-text-secondary mb-5">
444
- In-place text contrast and colorblind simulation for the categorical series. Converging
445
- strips across two swatches signal a collision for that viewer; all pairs hold ΔE ≥ 8 through
446
- the first {CATEGORICAL_CVD_SAFE_MAX}.
447
- </Text>
448
-
449
- {(['default', 'dark'] as const).map((variant) => {
450
- const colors = categoricalPalette[variant]
451
- const textColor = variant === 'dark' ? '#FFFFFF' : '#000000'
452
- return (
453
- <View key={variant} style={{ marginBottom: 22 }}>
454
- <Text
455
- className="text-lg font-bold text-text-primary mb-2"
456
- style={{ textTransform: 'capitalize' }}
457
- >
458
- {variant}
459
- </Text>
460
- {/* swatches with in-place text + contrast ratio */}
461
- <View style={{ flexDirection: 'row', gap: 6, marginBottom: 6 }}>
462
- {colors.map((hex, i) => (
463
- <View key={i} style={{ alignItems: 'center' }}>
464
- <View
465
- style={{
466
- width: 56,
467
- height: 40,
468
- borderRadius: 6,
469
- backgroundColor: hex,
470
- alignItems: 'center',
471
- justifyContent: 'center',
472
- }}
473
- >
474
- <Text style={{ color: textColor, fontSize: 11, fontWeight: '700' }}>Aa</Text>
475
- </View>
476
- <Text className="text-text-secondary" style={{ fontSize: 8, marginTop: 2 }}>
477
- {contrast(hex, textColor).toFixed(1)}:1
478
- </Text>
479
- </View>
480
- ))}
481
- </View>
482
- {/* CVD simulation strips */}
483
- {(['deuteranopia', 'protanopia'] as const).map((mode) => (
484
- <View key={mode} style={{ flexDirection: 'row', alignItems: 'center', marginTop: 3 }}>
485
- <Text className="text-text-secondary" style={{ fontSize: 9, width: 96 }}>
486
- {mode}
487
- </Text>
488
- <View style={{ flexDirection: 'row', gap: 6 }}>
489
- {colors.map((hex, i) => (
490
- <View
491
- key={i}
492
- style={{
493
- width: 56,
494
- height: 14,
495
- borderRadius: 3,
496
- backgroundColor: simulate(CVD[mode], hex),
497
- }}
498
- />
499
- ))}
500
- </View>
501
- </View>
502
- ))}
503
- </View>
504
- )
505
- })}
506
- </View>
507
- ),
508
- }