radix-native 0.0.1

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 (81) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +51 -0
  3. package/dist/index.cjs +4323 -0
  4. package/dist/index.d.cts +861 -0
  5. package/dist/index.d.mts +861 -0
  6. package/dist/index.d.ts +861 -0
  7. package/dist/index.mjs +4285 -0
  8. package/package.json +51 -0
  9. package/src/components/actions/Button.tsx +337 -0
  10. package/src/components/actions/Checkbox.tsx +216 -0
  11. package/src/components/actions/CheckboxCards.tsx +257 -0
  12. package/src/components/actions/CheckboxGroup.tsx +249 -0
  13. package/src/components/actions/index.ts +4 -0
  14. package/src/components/layout/Box.tsx +108 -0
  15. package/src/components/layout/Flex.tsx +149 -0
  16. package/src/components/layout/Grid.tsx +224 -0
  17. package/src/components/layout/index.ts +9 -0
  18. package/src/components/playground/ThemeControls.tsx +456 -0
  19. package/src/components/playground/index.ts +1 -0
  20. package/src/components/typography/Blockquote.tsx +137 -0
  21. package/src/components/typography/Code.tsx +164 -0
  22. package/src/components/typography/Em.tsx +68 -0
  23. package/src/components/typography/Heading.tsx +136 -0
  24. package/src/components/typography/Kbd.tsx +162 -0
  25. package/src/components/typography/Link.tsx +173 -0
  26. package/src/components/typography/Quote.tsx +71 -0
  27. package/src/components/typography/Strong.tsx +70 -0
  28. package/src/components/typography/Text.tsx +140 -0
  29. package/src/components/typography/index.ts +9 -0
  30. package/src/hooks/useResolveColor.ts +24 -0
  31. package/src/hooks/useThemeContext.ts +11 -0
  32. package/src/index.ts +63 -0
  33. package/src/theme/Theme.tsx +12 -0
  34. package/src/theme/ThemeContext.ts +4 -0
  35. package/src/theme/ThemeImpl.tsx +54 -0
  36. package/src/theme/ThemeRoot.tsx +65 -0
  37. package/src/theme/createTheme.tsx +17 -0
  38. package/src/theme/resolveGrayColor.ts +38 -0
  39. package/src/theme/theme.types.ts +95 -0
  40. package/src/tokens/colors/amber.ts +28 -0
  41. package/src/tokens/colors/blue.ts +28 -0
  42. package/src/tokens/colors/bronze.ts +28 -0
  43. package/src/tokens/colors/brown.ts +28 -0
  44. package/src/tokens/colors/crimson.ts +28 -0
  45. package/src/tokens/colors/cyan.ts +28 -0
  46. package/src/tokens/colors/gold.ts +28 -0
  47. package/src/tokens/colors/grass.ts +28 -0
  48. package/src/tokens/colors/gray.ts +28 -0
  49. package/src/tokens/colors/green.ts +28 -0
  50. package/src/tokens/colors/index.ts +36 -0
  51. package/src/tokens/colors/indigo.ts +28 -0
  52. package/src/tokens/colors/iris.ts +28 -0
  53. package/src/tokens/colors/jade.ts +28 -0
  54. package/src/tokens/colors/lime.ts +28 -0
  55. package/src/tokens/colors/mauve.ts +28 -0
  56. package/src/tokens/colors/mint.ts +28 -0
  57. package/src/tokens/colors/olive.ts +28 -0
  58. package/src/tokens/colors/orange.ts +28 -0
  59. package/src/tokens/colors/pink.ts +28 -0
  60. package/src/tokens/colors/plum.ts +28 -0
  61. package/src/tokens/colors/purple.ts +28 -0
  62. package/src/tokens/colors/red.ts +28 -0
  63. package/src/tokens/colors/ruby.ts +28 -0
  64. package/src/tokens/colors/sage.ts +28 -0
  65. package/src/tokens/colors/sand.ts +28 -0
  66. package/src/tokens/colors/sky.ts +28 -0
  67. package/src/tokens/colors/slate.ts +28 -0
  68. package/src/tokens/colors/teal.ts +28 -0
  69. package/src/tokens/colors/tomato.ts +28 -0
  70. package/src/tokens/colors/types.ts +69 -0
  71. package/src/tokens/colors/violet.ts +28 -0
  72. package/src/tokens/colors/yellow.ts +28 -0
  73. package/src/tokens/index.ts +5 -0
  74. package/src/tokens/radius.ts +56 -0
  75. package/src/tokens/scaling.ts +10 -0
  76. package/src/tokens/spacing.ts +21 -0
  77. package/src/tokens/typography.ts +60 -0
  78. package/src/utils/applyScaling.ts +6 -0
  79. package/src/utils/classicEffect.ts +46 -0
  80. package/src/utils/resolveColor.ts +69 -0
  81. package/src/utils/resolveSpace.ts +13 -0
@@ -0,0 +1,456 @@
1
+ import React, { useState } from 'react'
2
+ import { Animated, Pressable, ScrollView, StyleSheet, View, useWindowDimensions } from 'react-native'
3
+ import type { ViewStyle } from 'react-native'
4
+ import { useThemeContext } from '../../hooks/useThemeContext'
5
+ import { useResolveColor } from '../../hooks/useResolveColor'
6
+ import { Text } from '../typography/Text'
7
+ import { Heading } from '../typography/Heading'
8
+ import { getRadius } from '../../tokens/radius'
9
+ import type { AccentColor, GrayColor } from '../../tokens/colors/types'
10
+ import type { RadiusToken } from '../../tokens/radius'
11
+ import type { ScalingMode } from '../../tokens/scaling'
12
+ import type { ThemeColor } from '../../theme/theme.types'
13
+
14
+ // ─── Constants ────────────────────────────────────────────────────────────────
15
+
16
+ const ACCENT_COLORS: AccentColor[] = [
17
+ 'gray', 'gold', 'bronze', 'brown', 'yellow', 'amber', 'orange',
18
+ 'tomato', 'red', 'ruby', 'crimson', 'pink', 'plum', 'purple', 'violet',
19
+ 'iris', 'indigo', 'blue', 'cyan', 'teal', 'jade', 'green', 'grass',
20
+ 'lime', 'mint', 'sky',
21
+ ]
22
+
23
+ const GRAY_OPTIONS: (GrayColor | 'auto')[] = ['auto', 'gray', 'mauve', 'slate', 'sage', 'olive', 'sand']
24
+ const RADIUS_OPTIONS: RadiusToken[] = ['none', 'small', 'medium', 'large', 'full']
25
+ const SCALING_OPTIONS: ScalingMode[] = ['90%', '95%', '100%', '105%', '110%']
26
+
27
+ const PANEL_WIDTH = 280
28
+
29
+ // ─── ThemeControls ────────────────────────────────────────────────────────────
30
+
31
+ export interface ThemeControlsProps {
32
+ /** Show the "Copy Theme" button at the bottom */
33
+ showCopyTheme?: boolean
34
+ /** Callback when "Copy Theme" is pressed — receives the config string */
35
+ onCopyTheme?: (config: string) => void
36
+ /** Start with the panel open */
37
+ defaultOpen?: boolean
38
+ }
39
+
40
+ export function ThemeControls({ showCopyTheme, onCopyTheme, defaultOpen = false }: ThemeControlsProps = {}) {
41
+ const [open, setOpen] = useState(defaultOpen)
42
+ const slideAnim = React.useRef(new Animated.Value(defaultOpen ? 0 : PANEL_WIDTH + 20)).current
43
+ const { height: windowHeight } = useWindowDimensions()
44
+
45
+ const {
46
+ appearance,
47
+ accentColor,
48
+ grayColor,
49
+ resolvedGrayColor,
50
+ radius,
51
+ scaling,
52
+ onAppearanceChange,
53
+ onAccentColorChange,
54
+ onGrayColorChange,
55
+ onRadiusChange,
56
+ onScalingChange,
57
+ } = useThemeContext()
58
+
59
+ const rc = useResolveColor()
60
+
61
+ const toggle = () => {
62
+ const toValue = open ? PANEL_WIDTH + 20 : 0
63
+ Animated.spring(slideAnim, {
64
+ toValue,
65
+ useNativeDriver: true,
66
+ damping: 20,
67
+ stiffness: 200,
68
+ }).start()
69
+ setOpen(!open)
70
+ }
71
+
72
+ const handleCopyTheme = () => {
73
+ const config = `<Theme accentColor="${accentColor}" grayColor="${grayColor}" appearance="${appearance}" radius="${radius}" scaling="${scaling}">`
74
+ onCopyTheme?.(config)
75
+ }
76
+
77
+ return (
78
+ <View style={styles.container} pointerEvents="box-none">
79
+ {/* "T" toggle button — always visible */}
80
+ <Pressable
81
+ onPress={toggle}
82
+ accessibilityRole="button"
83
+ accessibilityLabel="Toggle theme panel"
84
+ style={[
85
+ styles.toggleButton,
86
+ {
87
+ backgroundColor: rc('gray-2'),
88
+ borderColor: rc('gray-6'),
89
+ },
90
+ ]}
91
+ >
92
+ <Text size={2} weight="bold" style={{ color: rc('gray-12') }}>T</Text>
93
+ </Pressable>
94
+
95
+ {/* Sliding panel */}
96
+ <Animated.View
97
+ style={[
98
+ styles.panelWrapper,
99
+ { maxHeight: windowHeight - 120, transform: [{ translateX: slideAnim }] },
100
+ ]}
101
+ >
102
+ <ScrollView
103
+ style={[styles.panel, { backgroundColor: rc('gray-2'), borderColor: rc('gray-6') }]}
104
+ contentContainerStyle={styles.panelContent}
105
+ showsVerticalScrollIndicator={false}
106
+ >
107
+ {/* Header */}
108
+ <View style={styles.headerRow}>
109
+ <Heading size={5} style={{ color: rc('gray-12') }}>Theme</Heading>
110
+ <Pressable
111
+ onPress={toggle}
112
+ accessibilityRole="button"
113
+ accessibilityLabel="Close theme panel"
114
+ style={[styles.closeButton, { borderColor: rc('gray-6') }]}
115
+ >
116
+ <Text size={2} weight="bold" style={{ color: rc('gray-12') }}>T</Text>
117
+ </Pressable>
118
+ </View>
119
+
120
+ {/* ─── Accent color ─────────────────────────────────────── */}
121
+ <SectionLabel rc={rc}>Accent color</SectionLabel>
122
+ <View style={styles.dotGrid}>
123
+ {ACCENT_COLORS.map((c) => (
124
+ <ColorDot
125
+ key={c}
126
+ color={`${c}-9` as ThemeColor}
127
+ selected={accentColor === c}
128
+ onPress={() => onAccentColorChange(c)}
129
+ rc={rc}
130
+ label={c}
131
+ />
132
+ ))}
133
+ </View>
134
+
135
+ {/* ─── Gray color ───────────────────────────────────────── */}
136
+ <SectionLabel rc={rc}>Gray color</SectionLabel>
137
+ <View style={styles.dotGrid}>
138
+ {GRAY_OPTIONS.map((g) => {
139
+ const dotColor = g === 'auto'
140
+ ? `${resolvedGrayColor}-9` as ThemeColor
141
+ : `${g}-9` as ThemeColor
142
+ return (
143
+ <ColorDot
144
+ key={g}
145
+ color={dotColor}
146
+ selected={grayColor === g}
147
+ onPress={() => onGrayColorChange(g)}
148
+ rc={rc}
149
+ label={g}
150
+ />
151
+ )
152
+ })}
153
+ </View>
154
+
155
+ {/* ─── Appearance ───────────────────────────────────────── */}
156
+ <SectionLabel rc={rc}>Appearance</SectionLabel>
157
+ <View style={styles.segmentedRow}>
158
+ <SegmentedButton
159
+ label="Light"
160
+ icon={'\u2600\uFE0E'}
161
+ selected={appearance === 'light'}
162
+ onPress={() => onAppearanceChange('light')}
163
+ rc={rc}
164
+ />
165
+ <SegmentedButton
166
+ label="Dark"
167
+ icon={'\u263E'}
168
+ selected={appearance === 'dark'}
169
+ onPress={() => onAppearanceChange('dark')}
170
+ rc={rc}
171
+ />
172
+ </View>
173
+
174
+ {/* ─── Radius ───────────────────────────────────────────── */}
175
+ <SectionLabel rc={rc}>Radius</SectionLabel>
176
+ <View style={styles.radiusRow}>
177
+ {RADIUS_OPTIONS.map((r) => (
178
+ <RadiusPreview
179
+ key={r}
180
+ token={r}
181
+ selected={radius === r}
182
+ onPress={() => onRadiusChange(r)}
183
+ rc={rc}
184
+ />
185
+ ))}
186
+ </View>
187
+
188
+ {/* ─── Scaling ──────────────────────────────────────────── */}
189
+ <SectionLabel rc={rc}>Scaling</SectionLabel>
190
+ <View style={styles.chipRow}>
191
+ {SCALING_OPTIONS.map((s) => (
192
+ <Chip
193
+ key={s}
194
+ label={s}
195
+ selected={scaling === s}
196
+ onPress={() => onScalingChange(s)}
197
+ rc={rc}
198
+ />
199
+ ))}
200
+ </View>
201
+
202
+ {/* ─── Copy Theme ───────────────────────────────────────── */}
203
+ {showCopyTheme && (
204
+ <Pressable
205
+ style={[styles.copyButton, { backgroundColor: rc('accent-9') }]}
206
+ onPress={handleCopyTheme}
207
+ >
208
+ <Text size={2} weight="bold" style={{ color: rc('accent-contrast') }}>
209
+ Copy Theme
210
+ </Text>
211
+ </Pressable>
212
+ )}
213
+ </ScrollView>
214
+ </Animated.View>
215
+ </View>
216
+ )
217
+ }
218
+ ThemeControls.displayName = 'ThemeControls'
219
+
220
+ // ─── Internal components ──────────────────────────────────────────────────────
221
+
222
+ type RC = (color: ThemeColor) => string
223
+
224
+ function SectionLabel({ children, rc }: { children: string; rc: RC }) {
225
+ return (
226
+ <Text size={2} weight="medium" style={{ color: rc('accent-11'), marginTop: 4 }}>
227
+ {children}
228
+ </Text>
229
+ )
230
+ }
231
+
232
+ function ColorDot({
233
+ color, selected, onPress, rc, label,
234
+ }: { color: ThemeColor; selected: boolean; onPress: () => void; rc: RC; label: string }) {
235
+ const bg = rc(color)
236
+ return (
237
+ <Pressable
238
+ onPress={onPress}
239
+ accessibilityLabel={label}
240
+ accessibilityRole="button"
241
+ style={[
242
+ styles.dotOuter,
243
+ selected && { borderColor: rc('gray-12') },
244
+ ]}
245
+ >
246
+ <View style={[styles.dotInner, { backgroundColor: bg }]} />
247
+ </Pressable>
248
+ )
249
+ }
250
+
251
+ function SegmentedButton({
252
+ label, icon, selected, onPress, rc,
253
+ }: { label: string; icon: string; selected: boolean; onPress: () => void; rc: RC }) {
254
+ return (
255
+ <Pressable
256
+ onPress={onPress}
257
+ accessibilityRole="button"
258
+ style={[
259
+ styles.segmentedButton,
260
+ {
261
+ borderColor: selected ? rc('gray-12') : rc('gray-6'),
262
+ backgroundColor: selected ? rc('gray-3') : 'transparent',
263
+ },
264
+ ]}
265
+ >
266
+ <Text size={2} style={{ color: rc('gray-12') }}>{icon}</Text>
267
+ <Text size={2} weight="medium" style={{ color: rc('gray-12') }}>{label}</Text>
268
+ </Pressable>
269
+ )
270
+ }
271
+
272
+ function RadiusPreview({
273
+ token, selected, onPress, rc,
274
+ }: { token: RadiusToken; selected: boolean; onPress: () => void; rc: RC }) {
275
+ const previewRadius = token === 'full'
276
+ ? 26
277
+ : getRadius(token, 5)
278
+
279
+ const label = token.charAt(0).toUpperCase() + token.slice(1)
280
+ const borderAccent = rc('accent-a8')
281
+
282
+ return (
283
+ <Pressable onPress={onPress} accessibilityRole="button" style={styles.radiusItem}>
284
+ <View
285
+ style={[
286
+ styles.radiusCard,
287
+ {
288
+ borderColor: selected ? rc('gray-12') : rc('gray-6'),
289
+ backgroundColor: selected ? rc('gray-3') : 'transparent',
290
+ },
291
+ ]}
292
+ >
293
+ <View
294
+ style={[
295
+ styles.radiusPreviewShape,
296
+ {
297
+ backgroundColor: rc('accent-4'),
298
+ borderTopLeftRadius: previewRadius,
299
+ borderTopWidth: 2,
300
+ borderLeftWidth: 2,
301
+ borderTopColor: borderAccent,
302
+ borderLeftColor: borderAccent,
303
+ },
304
+ ]}
305
+ />
306
+ </View>
307
+ <Text size={1} style={{ color: rc('gray-11'), textAlign: 'center' }}>{label}</Text>
308
+ </Pressable>
309
+ )
310
+ }
311
+
312
+ function Chip({
313
+ label, selected, onPress, rc,
314
+ }: { label: string; selected: boolean; onPress: () => void; rc: RC }) {
315
+ return (
316
+ <Pressable
317
+ onPress={onPress}
318
+ accessibilityRole="button"
319
+ style={[
320
+ styles.chip,
321
+ {
322
+ borderColor: selected ? rc('gray-12') : rc('gray-6'),
323
+ backgroundColor: selected ? rc('gray-3') : 'transparent',
324
+ },
325
+ ]}
326
+ >
327
+ <Text
328
+ size={2}
329
+ weight={selected ? 'bold' : 'regular'}
330
+ style={{ color: rc('gray-12') }}
331
+ >
332
+ {label}
333
+ </Text>
334
+ </Pressable>
335
+ )
336
+ }
337
+
338
+ // ─── Styles ───────────────────────────────────────────────────────────────────
339
+
340
+ const styles = StyleSheet.create({
341
+ container: {
342
+ position: 'absolute',
343
+ top: 54,
344
+ right: 12,
345
+ zIndex: 1000,
346
+ alignItems: 'flex-end',
347
+ },
348
+ toggleButton: {
349
+ width: 36,
350
+ height: 36,
351
+ borderRadius: 8,
352
+ borderWidth: 1,
353
+ alignItems: 'center',
354
+ justifyContent: 'center',
355
+ },
356
+ panelWrapper: {
357
+ position: 'absolute',
358
+ top: 0,
359
+ right: 0,
360
+ width: PANEL_WIDTH,
361
+ marginTop: 44,
362
+ },
363
+ panel: {
364
+ borderRadius: 12,
365
+ borderWidth: 1,
366
+ },
367
+ panelContent: {
368
+ padding: 20,
369
+ rowGap: 10,
370
+ },
371
+ headerRow: {
372
+ flexDirection: 'row',
373
+ alignItems: 'center',
374
+ justifyContent: 'space-between',
375
+ },
376
+ closeButton: {
377
+ width: 32,
378
+ height: 32,
379
+ borderRadius: 6,
380
+ borderWidth: 1,
381
+ alignItems: 'center',
382
+ justifyContent: 'center',
383
+ },
384
+ dotGrid: {
385
+ flexDirection: 'row',
386
+ flexWrap: 'wrap',
387
+ gap: 4,
388
+ },
389
+ dotOuter: {
390
+ width: 28,
391
+ height: 28,
392
+ borderRadius: 14,
393
+ borderWidth: 2,
394
+ borderColor: 'transparent',
395
+ alignItems: 'center',
396
+ justifyContent: 'center',
397
+ },
398
+ dotInner: {
399
+ width: 20,
400
+ height: 20,
401
+ borderRadius: 10,
402
+ },
403
+ segmentedRow: {
404
+ flexDirection: 'row',
405
+ gap: 8,
406
+ },
407
+ segmentedButton: {
408
+ flex: 1,
409
+ flexDirection: 'row',
410
+ alignItems: 'center',
411
+ justifyContent: 'center',
412
+ gap: 6,
413
+ paddingVertical: 10,
414
+ borderRadius: 8,
415
+ borderWidth: 1,
416
+ },
417
+ chipRow: {
418
+ flexDirection: 'row',
419
+ flexWrap: 'wrap',
420
+ gap: 6,
421
+ },
422
+ chip: {
423
+ paddingHorizontal: 12,
424
+ paddingVertical: 6,
425
+ borderRadius: 6,
426
+ borderWidth: 1,
427
+ },
428
+ radiusRow: {
429
+ flexDirection: 'row',
430
+ gap: 6,
431
+ },
432
+ radiusItem: {
433
+ flex: 1,
434
+ alignItems: 'center',
435
+ gap: 6,
436
+ },
437
+ radiusCard: {
438
+ width: '100%',
439
+ aspectRatio: 1,
440
+ borderRadius: 8,
441
+ borderWidth: 1,
442
+ alignItems: 'center',
443
+ justifyContent: 'center',
444
+ },
445
+ radiusPreviewShape: {
446
+ width: 32,
447
+ height: 32,
448
+ },
449
+ copyButton: {
450
+ marginTop: 6,
451
+ paddingVertical: 10,
452
+ borderRadius: 8,
453
+ alignItems: 'center',
454
+ justifyContent: 'center',
455
+ },
456
+ })
@@ -0,0 +1 @@
1
+ export { ThemeControls, type ThemeControlsProps } from './ThemeControls'
@@ -0,0 +1,137 @@
1
+ import React from 'react'
2
+ import { View, Text as RNText } from 'react-native'
3
+ import type { StyleProp, ViewStyle, TextStyle } from 'react-native'
4
+ import { useThemeContext } from '../../hooks/useThemeContext'
5
+ import { useResolveColor } from '../../hooks/useResolveColor'
6
+ import { resolveSpace } from '../../utils/resolveSpace'
7
+ import { fontSize as fontSizeMap, lineHeight, letterSpacingEm } from '../../tokens/typography'
8
+ import { scalingMap } from '../../tokens/scaling'
9
+ import { space } from '../../tokens/spacing'
10
+ import type { FontSizeToken } from '../../tokens/typography'
11
+ import type { MarginToken } from '../../tokens/spacing'
12
+ import type { AccentColor } from '../../tokens/colors/types'
13
+ import type { TextWeight, TextWrap } from './Text'
14
+
15
+ // ─── Types ────────────────────────────────────────────────────────────────────
16
+
17
+ export interface BlockquoteProps {
18
+ /** Text size token (1–9). Default: 3 (16px). */
19
+ size?: FontSizeToken
20
+ /** Font weight. Default: 'regular'. */
21
+ weight?: TextWeight
22
+ /** Text color. When not set: uses gray-12. */
23
+ color?: AccentColor
24
+ /** Increases color contrast when `color` is set (a11 → 12). */
25
+ highContrast?: boolean
26
+ /** Truncates text with an ellipsis when it overflows. */
27
+ truncate?: boolean
28
+ /** Controls text wrapping. 'pretty'/'balance' are not supported in React Native, no-op. */
29
+ wrap?: TextWrap
30
+ // ─── Margin props ──────────────────────────────────────────────────────────
31
+ m?: MarginToken
32
+ mx?: MarginToken
33
+ my?: MarginToken
34
+ mt?: MarginToken
35
+ mr?: MarginToken
36
+ mb?: MarginToken
37
+ ml?: MarginToken
38
+ style?: StyleProp<ViewStyle>
39
+ children?: React.ReactNode
40
+ }
41
+
42
+ // ─── Constants ────────────────────────────────────────────────────────────────
43
+
44
+ const FONT_WEIGHT: Record<TextWeight, NonNullable<TextStyle['fontWeight']>> = {
45
+ light: '300',
46
+ regular: '400',
47
+ medium: '500',
48
+ bold: '700',
49
+ }
50
+
51
+ // ─── Component ────────────────────────────────────────────────────────────────
52
+
53
+ export function Blockquote({
54
+ size = 3,
55
+ weight = 'regular',
56
+ color,
57
+ highContrast,
58
+ truncate,
59
+ wrap,
60
+ m, mx, my, mt, mr, mb, ml,
61
+ style,
62
+ children,
63
+ }: BlockquoteProps) {
64
+ const { scaling, fonts } = useThemeContext()
65
+ const rc = useResolveColor()
66
+
67
+ const sp = (token: MarginToken | undefined): number | undefined =>
68
+ token !== undefined ? resolveSpace(token, scaling) : undefined
69
+
70
+ // ─── Typography ─────────────────────────────────────────────────────────────
71
+ const scalingFactor = scalingMap[scaling]
72
+ const resolvedSize = Math.round(fontSizeMap[size] * scalingFactor)
73
+ const resolvedLineHeight = Math.round(lineHeight[size] * scalingFactor)
74
+ const resolvedLetterSpacing = letterSpacingEm[size] * resolvedSize
75
+
76
+ // ─── Color ──────────────────────────────────────────────────────────────────
77
+ // Radix scopes --accent-a6 per-component via data-accent-color.
78
+ // In RN we resolve the border color from the explicit color prop (or accent fallback).
79
+ const borderColor = rc(color ? `${color}-a6` : 'accent-a6')
80
+ const textColor = color
81
+ ? rc(highContrast ? `${color}-12` : `${color}-a11`)
82
+ : rc('gray-12')
83
+
84
+ // ─── Font family ─────────────────────────────────────────────────────────────
85
+ const fontFamily = fonts[weight] ?? fonts.regular
86
+
87
+ // ─── Wrapping / truncation ──────────────────────────────────────────────────
88
+ const numberOfLines = truncate ? 1 : wrap === 'nowrap' ? 1 : undefined
89
+ const ellipsizeMode = truncate ? 'tail' : wrap === 'nowrap' ? 'clip' : undefined
90
+
91
+ // ─── Border & padding (match Radix CSS: dynamic based on font size) ────────
92
+ // Space tokens are scaled in Radix: --space-N: calc(Npx * var(--scaling))
93
+ const s1 = space[1] * scalingFactor
94
+ const s3 = space[3] * scalingFactor
95
+ const s5 = space[5] * scalingFactor
96
+ // Radix: border-left: max(var(--space-1), 0.25em) solid var(--accent-a6)
97
+ const borderWidth = Math.max(s1, resolvedSize * 0.25)
98
+ // Radix: padding-left: min(var(--space-5), max(var(--space-3), 0.5em))
99
+ const paddingLeft = Math.min(s5, Math.max(s3, resolvedSize * 0.5))
100
+
101
+ // ─── Styles ─────────────────────────────────────────────────────────────────
102
+ const containerStyle: ViewStyle = {
103
+ borderLeftWidth: borderWidth,
104
+ borderLeftColor: borderColor,
105
+ paddingLeft,
106
+ // RN defaults flexShrink to 0; CSS defaults to 1. Without this, text
107
+ // inside a Flex row overflows instead of wrapping to the available width.
108
+ flexShrink: 1,
109
+ // Margins
110
+ marginTop: sp(mt ?? my ?? m),
111
+ marginBottom: sp(mb ?? my ?? m),
112
+ marginLeft: sp(ml ?? mx ?? m),
113
+ marginRight: sp(mr ?? mx ?? m),
114
+ }
115
+
116
+ const textStyle: TextStyle = {
117
+ fontSize: resolvedSize,
118
+ lineHeight: resolvedLineHeight,
119
+ letterSpacing: resolvedLetterSpacing,
120
+ fontWeight: FONT_WEIGHT[weight],
121
+ fontFamily,
122
+ color: textColor,
123
+ }
124
+
125
+ return (
126
+ <View style={[containerStyle, style]}>
127
+ <RNText
128
+ numberOfLines={numberOfLines}
129
+ ellipsizeMode={ellipsizeMode}
130
+ style={textStyle}
131
+ >
132
+ {children}
133
+ </RNText>
134
+ </View>
135
+ )
136
+ }
137
+ Blockquote.displayName = 'Blockquote'