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,173 @@
1
+ import React from 'react'
2
+ import { Text as RNText, Linking } from 'react-native'
3
+ import type { TextProps as RNTextProps, StyleProp, 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, lineHeight, letterSpacingEm } from '../../tokens/typography'
8
+ import { scalingMap } from '../../tokens/scaling'
9
+ import type { FontSizeToken } from '../../tokens/typography'
10
+ import type { MarginToken } from '../../tokens/spacing'
11
+ import type { AccentColor } from '../../tokens/colors/types'
12
+ import type { TextWeight, TextWrap } from './Text'
13
+
14
+ // ─── Types ────────────────────────────────────────────────────────────────────
15
+
16
+ /**
17
+ * Controls underline visibility.
18
+ * 'auto' → no underline by default (Radix shows on hover; RN has no hover).
19
+ * highContrast forces underline visible (matches Radix .rt-high-contrast).
20
+ * 'always' → underline always visible
21
+ * 'hover' → no underline (hover never fires in RN)
22
+ * 'none' → no underline
23
+ */
24
+ export type LinkUnderline = 'auto' | 'always' | 'hover' | 'none'
25
+
26
+ export interface LinkProps extends Omit<RNTextProps, 'style'> {
27
+ /** Text size token (1–9). Default: inherits — set to 3 (16px) if no parent. */
28
+ size?: FontSizeToken
29
+ weight?: TextWeight
30
+ /** Truncates text with an ellipsis when it overflows. */
31
+ truncate?: boolean
32
+ /** Controls text wrapping. 'pretty'/'balance' are not supported in React Native, no-op. */
33
+ wrap?: TextWrap
34
+ /**
35
+ * Underline visibility. Default: 'auto'.
36
+ * 'auto' → no underline, unless highContrast is set.
37
+ * 'hover' → no underline (hover does not exist on mobile).
38
+ */
39
+ underline?: LinkUnderline
40
+ /** Accent color for the link. Default: theme accent (accent-11). */
41
+ color?: AccentColor
42
+ /** Switches color to step 12 and forces underline visible in 'auto' mode. */
43
+ highContrast?: boolean
44
+ /**
45
+ * RN-only: URL opened via Linking.openURL when pressed.
46
+ * If both `href` and `onPress` are provided, `onPress` takes precedence.
47
+ */
48
+ href?: string
49
+ // ─── Margin props ──────────────────────────────────────────────────────────
50
+ m?: MarginToken
51
+ mx?: MarginToken
52
+ my?: MarginToken
53
+ mt?: MarginToken
54
+ mr?: MarginToken
55
+ mb?: MarginToken
56
+ ml?: MarginToken
57
+ style?: StyleProp<TextStyle>
58
+ }
59
+
60
+ // ─── Constants ────────────────────────────────────────────────────────────────
61
+
62
+ const FONT_WEIGHT: Record<TextWeight, NonNullable<TextStyle['fontWeight']>> = {
63
+ light: '300',
64
+ regular: '400',
65
+ medium: '500',
66
+ bold: '700',
67
+ }
68
+
69
+ // ─── Component ────────────────────────────────────────────────────────────────
70
+
71
+ export function Link({
72
+ size = 3,
73
+ weight,
74
+ truncate,
75
+ wrap,
76
+ underline = 'auto',
77
+ color,
78
+ highContrast,
79
+ href,
80
+ onPress,
81
+ m, mx, my, mt, mr, mb, ml,
82
+ style,
83
+ ...rest
84
+ }: LinkProps) {
85
+ const { scaling, fonts } = useThemeContext()
86
+ const rc = useResolveColor()
87
+
88
+ const sp = (token: MarginToken | undefined): number | undefined =>
89
+ token !== undefined ? resolveSpace(token, scaling) : undefined
90
+
91
+ // ─── Typography ─────────────────────────────────────────────────────────────
92
+ const scalingFactor = scalingMap[scaling]
93
+ const resolvedSize = Math.round(fontSize[size] * scalingFactor)
94
+ const resolvedLineHeight = Math.round(lineHeight[size] * scalingFactor)
95
+ const resolvedLetterSpacing = letterSpacingEm[size] * resolvedSize
96
+
97
+ // ─── Color ──────────────────────────────────────────────────────────────────
98
+ // Radix: accent links use a11 (vibrant foreground), highContrast → step 12.
99
+ // Gray links always use gray-12 (distinguishes via underline/weight, not color).
100
+ const prefix = color ?? 'accent'
101
+ const isGray = color === 'gray'
102
+ const linkColor = rc(
103
+ isGray || highContrast ? `${prefix}-12` : `${prefix}-a11`,
104
+ )
105
+
106
+ // ─── Font family ─────────────────────────────────────────────────────────────
107
+ const effectiveWeight: TextWeight = weight ?? 'regular'
108
+ const fontFamily = fonts[effectiveWeight] ?? fonts.regular
109
+
110
+ // ─── Underline ──────────────────────────────────────────────────────────────
111
+ // Radix CSS rules for .rt-underline-auto:
112
+ // - hover (web only) → underline
113
+ // - .rt-high-contrast → underline always, color: accent-a6
114
+ // - inside colored parent (data-accent-color) → underline always
115
+ // In RN there is no hover, so 'auto' only shows underline for highContrast.
116
+ // 'always' → underline, 'hover'/'none' → no underline.
117
+ const showUnderline =
118
+ underline === 'always' || (underline === 'auto' && !!highContrast)
119
+ const textDecorationLine: TextStyle['textDecorationLine'] =
120
+ showUnderline ? 'underline' : 'none'
121
+ // Radix: accent-a5 for normal underline, accent-a6 for highContrast
122
+ const textDecorationColor = showUnderline
123
+ ? rc(highContrast ? `${prefix}-a6` : `${prefix}-a5`)
124
+ : undefined
125
+
126
+ // ─── Wrapping / truncation ──────────────────────────────────────────────────
127
+ const numberOfLines = truncate ? 1 : wrap === 'nowrap' ? 1 : undefined
128
+ const ellipsizeMode = truncate ? 'tail' : wrap === 'nowrap' ? 'clip' : undefined
129
+
130
+ // ─── Press handler ──────────────────────────────────────────────────────────
131
+ const handlePress = React.useCallback(
132
+ (e: Parameters<NonNullable<RNTextProps['onPress']>>[0]) => {
133
+ if (onPress) {
134
+ onPress(e)
135
+ } else if (href) {
136
+ void Linking.openURL(href)
137
+ }
138
+ },
139
+ [onPress, href],
140
+ )
141
+
142
+ // ─── Style ──────────────────────────────────────────────────────────────────
143
+ const linkStyle: TextStyle = {
144
+ fontSize: resolvedSize,
145
+ lineHeight: resolvedLineHeight,
146
+ letterSpacing: resolvedLetterSpacing,
147
+ color: linkColor,
148
+ fontWeight: weight ? FONT_WEIGHT[weight] : undefined,
149
+ fontFamily,
150
+ textDecorationLine,
151
+ textDecorationColor,
152
+ // RN defaults flexShrink to 0; CSS defaults to 1. Without this, text
153
+ // inside a Flex row overflows instead of wrapping to the available width.
154
+ flexShrink: 1,
155
+ // Margins
156
+ marginTop: sp(mt ?? my ?? m),
157
+ marginBottom: sp(mb ?? my ?? m),
158
+ marginLeft: sp(ml ?? mx ?? m),
159
+ marginRight: sp(mr ?? mx ?? m),
160
+ }
161
+
162
+ return (
163
+ <RNText
164
+ accessibilityRole="link"
165
+ numberOfLines={numberOfLines}
166
+ ellipsizeMode={ellipsizeMode}
167
+ onPress={handlePress}
168
+ style={[linkStyle, style]}
169
+ {...rest}
170
+ />
171
+ )
172
+ }
173
+ Link.displayName = 'Link'
@@ -0,0 +1,71 @@
1
+ import React from 'react'
2
+ import { Text as RNText } from 'react-native'
3
+ import type { TextProps as RNTextProps, StyleProp, TextStyle } from 'react-native'
4
+ import { resolveSpace } from '../../utils/resolveSpace'
5
+ import { useThemeContext } from '../../hooks/useThemeContext'
6
+ import type { MarginToken } from '../../tokens/spacing'
7
+ import type { TextWrap } from './Text'
8
+
9
+ // ─── Types ────────────────────────────────────────────────────────────────────
10
+
11
+ export interface QuoteProps extends Omit<RNTextProps, 'style'> {
12
+ /** Truncates text with an ellipsis when it overflows. */
13
+ truncate?: boolean
14
+ /** Controls text wrapping. 'pretty'/'balance' are not supported in React Native, no-op. */
15
+ wrap?: TextWrap
16
+ // ─── Margin props ──────────────────────────────────────────────────────────
17
+ m?: MarginToken
18
+ mx?: MarginToken
19
+ my?: MarginToken
20
+ mt?: MarginToken
21
+ mr?: MarginToken
22
+ mb?: MarginToken
23
+ ml?: MarginToken
24
+ style?: StyleProp<TextStyle>
25
+ }
26
+
27
+ // ─── Component ────────────────────────────────────────────────────────────────
28
+
29
+ /**
30
+ * Inline quoted text. Wraps children with typographic curly quotes (" ").
31
+ * Can be nested inside `<Text>` for inline use.
32
+ */
33
+ export function Quote({
34
+ truncate,
35
+ wrap,
36
+ m, mx, my, mt, mr, mb, ml,
37
+ style,
38
+ children,
39
+ ...rest
40
+ }: QuoteProps) {
41
+ const { scaling } = useThemeContext()
42
+
43
+ const sp = (token: MarginToken | undefined): number | undefined =>
44
+ token !== undefined ? resolveSpace(token, scaling) : undefined
45
+
46
+ const numberOfLines = truncate ? 1 : wrap === 'nowrap' ? 1 : undefined
47
+ const ellipsizeMode = truncate ? 'tail' : wrap === 'nowrap' ? 'clip' : undefined
48
+
49
+ const quoteStyle: TextStyle = {
50
+ fontStyle: 'italic',
51
+ // RN defaults flexShrink to 0; CSS defaults to 1. Without this, text
52
+ // inside a Flex row overflows instead of wrapping to the available width.
53
+ flexShrink: 1,
54
+ marginTop: sp(mt ?? my ?? m),
55
+ marginBottom: sp(mb ?? my ?? m),
56
+ marginLeft: sp(ml ?? mx ?? m),
57
+ marginRight: sp(mr ?? mx ?? m),
58
+ }
59
+
60
+ return (
61
+ <RNText
62
+ numberOfLines={numberOfLines}
63
+ ellipsizeMode={ellipsizeMode}
64
+ style={[quoteStyle, style]}
65
+ {...rest}
66
+ >
67
+ {'\u201c'}{children}{'\u201d'}
68
+ </RNText>
69
+ )
70
+ }
71
+ Quote.displayName = 'Quote'
@@ -0,0 +1,70 @@
1
+ import React from 'react'
2
+ import { Text as RNText } from 'react-native'
3
+ import type { TextProps as RNTextProps, StyleProp, TextStyle } from 'react-native'
4
+ import { resolveSpace } from '../../utils/resolveSpace'
5
+ import { useThemeContext } from '../../hooks/useThemeContext'
6
+ import type { MarginToken } from '../../tokens/spacing'
7
+ import type { TextWrap } from './Text'
8
+
9
+ // ─── Types ────────────────────────────────────────────────────────────────────
10
+
11
+ export interface StrongProps extends Omit<RNTextProps, 'style'> {
12
+ /** Truncates text with an ellipsis when it overflows. */
13
+ truncate?: boolean
14
+ /** Controls text wrapping. 'pretty'/'balance' are not supported in React Native, no-op. */
15
+ wrap?: TextWrap
16
+ // ─── Margin props ──────────────────────────────────────────────────────────
17
+ m?: MarginToken
18
+ mx?: MarginToken
19
+ my?: MarginToken
20
+ mt?: MarginToken
21
+ mr?: MarginToken
22
+ mb?: MarginToken
23
+ ml?: MarginToken
24
+ style?: StyleProp<TextStyle>
25
+ }
26
+
27
+ // ─── Component ────────────────────────────────────────────────────────────────
28
+
29
+ /**
30
+ * Inline bold text. Can be nested inside `<Text>` for inline use.
31
+ * Can be nested inside `<Text>` for inline use.
32
+ * Uses `fonts.bold` when provided, otherwise `fontWeight: '700'`.
33
+ */
34
+ export function Strong({
35
+ truncate,
36
+ wrap,
37
+ m, mx, my, mt, mr, mb, ml,
38
+ style,
39
+ ...rest
40
+ }: StrongProps) {
41
+ const { scaling, fonts } = useThemeContext()
42
+
43
+ const sp = (token: MarginToken | undefined): number | undefined =>
44
+ token !== undefined ? resolveSpace(token, scaling) : undefined
45
+
46
+ const numberOfLines = truncate ? 1 : wrap === 'nowrap' ? 1 : undefined
47
+ const ellipsizeMode = truncate ? 'tail' : wrap === 'nowrap' ? 'clip' : undefined
48
+
49
+ const strongStyle: TextStyle = {
50
+ fontWeight: '700',
51
+ fontFamily: fonts.bold ?? fonts.regular,
52
+ // RN defaults flexShrink to 0; CSS defaults to 1. Without this, text
53
+ // inside a Flex row overflows instead of wrapping to the available width.
54
+ flexShrink: 1,
55
+ marginTop: sp(mt ?? my ?? m),
56
+ marginBottom: sp(mb ?? my ?? m),
57
+ marginLeft: sp(ml ?? mx ?? m),
58
+ marginRight: sp(mr ?? mx ?? m),
59
+ }
60
+
61
+ return (
62
+ <RNText
63
+ numberOfLines={numberOfLines}
64
+ ellipsizeMode={ellipsizeMode}
65
+ style={[strongStyle, style]}
66
+ {...rest}
67
+ />
68
+ )
69
+ }
70
+ Strong.displayName = 'Strong'
@@ -0,0 +1,140 @@
1
+ import React from 'react'
2
+ import { Text as RNText } from 'react-native'
3
+ import type { TextProps as RNTextProps, StyleProp, 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, lineHeight, letterSpacingEm } from '../../tokens/typography'
8
+ import { scalingMap } from '../../tokens/scaling'
9
+ import type { FontSizeToken } from '../../tokens/typography'
10
+ import type { SpaceToken, MarginToken } from '../../tokens/spacing'
11
+ import type { AccentColor } from '../../tokens/colors/types'
12
+
13
+ // ─── Types ────────────────────────────────────────────────────────────────────
14
+
15
+ export type TextSize = FontSizeToken
16
+ export type TextWeight = 'light' | 'regular' | 'medium' | 'bold'
17
+ export type TextAlign = 'left' | 'center' | 'right'
18
+ /** 'pretty' and 'balance' are not supported in React Native and have no effect. */
19
+ export type TextWrap = 'wrap' | 'nowrap' | 'pretty' | 'balance'
20
+
21
+ export interface TextProps extends Omit<RNTextProps, 'style'> {
22
+ /** Text size token (1–9). Default: 3 (16px). */
23
+ size?: TextSize
24
+ /** Font weight. Each weight maps to its own fontFamily when configured in ThemeFonts. */
25
+ weight?: TextWeight
26
+ /** Text alignment. */
27
+ align?: TextAlign
28
+ /** Truncates text with an ellipsis when it overflows its container. */
29
+ truncate?: boolean
30
+ /**
31
+ * Controls text wrapping.
32
+ * 'nowrap' → single line with clip (no ellipsis).
33
+ * 'pretty' / 'balance' → not supported in React Native, no-op.
34
+ */
35
+ wrap?: TextWrap
36
+ /**
37
+ * Text color from the theme palette.
38
+ * When set: uses alpha step a11 (accessible foreground).
39
+ * When not set: uses gray-12 (standard body text color).
40
+ */
41
+ color?: AccentColor
42
+ /**
43
+ * Increases color contrast when `color` is set: switches from a11 → solid 12.
44
+ * Has no effect when `color` is not set (gray-12 is already maximum contrast).
45
+ */
46
+ highContrast?: boolean
47
+ // ─── Margin props ──────────────────────────────────────────────────────────
48
+ m?: MarginToken
49
+ mx?: MarginToken
50
+ my?: MarginToken
51
+ mt?: MarginToken
52
+ mr?: MarginToken
53
+ mb?: MarginToken
54
+ ml?: MarginToken
55
+ style?: StyleProp<TextStyle>
56
+ }
57
+
58
+ // ─── Constants ────────────────────────────────────────────────────────────────
59
+
60
+ const FONT_WEIGHT: Record<TextWeight, NonNullable<TextStyle['fontWeight']>> = {
61
+ light: '300',
62
+ regular: '400',
63
+ medium: '500',
64
+ bold: '700',
65
+ }
66
+
67
+ // ─── Component ────────────────────────────────────────────────────────────────
68
+
69
+ export function Text({
70
+ size = 3,
71
+ weight,
72
+ align,
73
+ truncate,
74
+ wrap,
75
+ color,
76
+ highContrast,
77
+ m, mx, my, mt, mr, mb, ml,
78
+ style,
79
+ ...rest
80
+ }: TextProps) {
81
+ const { scaling, fonts } = useThemeContext()
82
+ const rc = useResolveColor()
83
+
84
+ const sp = (token: MarginToken | undefined): number | undefined =>
85
+ token !== undefined ? resolveSpace(token, scaling) : undefined
86
+
87
+ // ─── Typography ─────────────────────────────────────────────────────────────
88
+ const scalingFactor = scalingMap[scaling]
89
+ const resolvedSize = Math.round(fontSize[size] * scalingFactor)
90
+ const resolvedLineHeight = Math.round(lineHeight[size] * scalingFactor)
91
+ const resolvedLetterSpacing = letterSpacingEm[size] * resolvedSize
92
+
93
+ // ─── Color ──────────────────────────────────────────────────────────────────
94
+ // no color prop → gray-12 (standard body text)
95
+ // color prop → {color}-a11 (alpha step 11 — accessible foreground)
96
+ // color + highContrast → {color}-12 (solid step 12 — maximum contrast)
97
+ const textColor = color
98
+ ? rc(highContrast ? `${color}-12` : `${color}-a11`)
99
+ : rc('gray-12')
100
+
101
+ // ─── Font family ─────────────────────────────────────────────────────────────
102
+ // Each weight maps to its own fontFamily — in RN, fontWeight alone doesn't
103
+ // load a different font file; the fontFamily must be registered per weight.
104
+ const effectiveWeight: TextWeight = weight ?? 'regular'
105
+ const fontFamily = fonts[effectiveWeight] ?? fonts.regular
106
+
107
+ // ─── Wrapping / truncation ──────────────────────────────────────────────────
108
+ // truncate takes precedence over wrap
109
+ const numberOfLines = truncate ? 1 : wrap === 'nowrap' ? 1 : undefined
110
+ const ellipsizeMode = truncate ? 'tail' : wrap === 'nowrap' ? 'clip' : undefined
111
+
112
+ // ─── Style ──────────────────────────────────────────────────────────────────
113
+ const textStyle: TextStyle = {
114
+ fontSize: resolvedSize,
115
+ lineHeight: resolvedLineHeight,
116
+ letterSpacing: resolvedLetterSpacing,
117
+ color: textColor,
118
+ textAlign: align,
119
+ fontWeight: weight ? FONT_WEIGHT[weight] : undefined,
120
+ fontFamily,
121
+ // RN defaults flexShrink to 0; CSS defaults to 1. Without this, text
122
+ // inside a Flex row overflows instead of wrapping to the available width.
123
+ flexShrink: 1,
124
+ // Margins
125
+ marginTop: sp(mt ?? my ?? m),
126
+ marginBottom: sp(mb ?? my ?? m),
127
+ marginLeft: sp(ml ?? mx ?? m),
128
+ marginRight: sp(mr ?? mx ?? m),
129
+ }
130
+
131
+ return (
132
+ <RNText
133
+ numberOfLines={numberOfLines}
134
+ ellipsizeMode={ellipsizeMode}
135
+ style={[textStyle, style]}
136
+ {...rest}
137
+ />
138
+ )
139
+ }
140
+ Text.displayName = 'Text'
@@ -0,0 +1,9 @@
1
+ export { Text, type TextProps, type TextSize, type TextWeight, type TextAlign, type TextWrap } from './Text'
2
+ export { Heading, type HeadingProps, type HeadingSize } from './Heading'
3
+ export { Link, type LinkProps, type LinkUnderline } from './Link'
4
+ export { Blockquote, type BlockquoteProps } from './Blockquote'
5
+ export { Em, type EmProps } from './Em'
6
+ export { Strong, type StrongProps } from './Strong'
7
+ export { Code, type CodeProps, type CodeVariant } from './Code'
8
+ export { Kbd, type KbdProps, type KbdVariant } from './Kbd'
9
+ export { Quote, type QuoteProps } from './Quote'
@@ -0,0 +1,24 @@
1
+ import React from 'react'
2
+ import { useThemeContext } from './useThemeContext'
3
+ import { resolveColor } from '../utils/resolveColor'
4
+ import type { ThemeColor } from '../theme/theme.types'
5
+
6
+ /**
7
+ * Returns a resolver function bound to the current theme context.
8
+ * Use this to resolve ThemeColor tokens to hex strings inside components.
9
+ *
10
+ * @example
11
+ * const rc = useResolveColor()
12
+ * const bg = rc('gray-1')
13
+ * const accent = rc('accent-9')
14
+ * const blue = rc('blue-5')
15
+ */
16
+ export function useResolveColor(): (color: ThemeColor) => string {
17
+ const { appearance, accentColor, resolvedGrayColor, colorOverrides } = useThemeContext()
18
+
19
+ return React.useCallback(
20
+ (color: ThemeColor) =>
21
+ resolveColor(color, appearance, accentColor, resolvedGrayColor, colorOverrides),
22
+ [appearance, accentColor, resolvedGrayColor, colorOverrides],
23
+ )
24
+ }
@@ -0,0 +1,11 @@
1
+ import React from 'react'
2
+ import { ThemeContext } from '../theme/ThemeContext'
3
+ import type { ThemeContextValue } from '../theme/theme.types'
4
+
5
+ export function useThemeContext(): ThemeContextValue {
6
+ const context = React.useContext(ThemeContext)
7
+ if (context === undefined) {
8
+ throw new Error('`useThemeContext` must be used within a `Theme`')
9
+ }
10
+ return context
11
+ }
package/src/index.ts ADDED
@@ -0,0 +1,63 @@
1
+ // radix-native — public entry point
2
+
3
+ // Theme
4
+ export { Theme } from './theme/Theme'
5
+ export { createTheme } from './theme/createTheme'
6
+ export { useThemeContext } from './hooks/useThemeContext'
7
+ export { useResolveColor } from './hooks/useResolveColor'
8
+ export type {
9
+ ThemeProps,
10
+ ThemeContextValue,
11
+ ThemeAppearance,
12
+ ThemeColor,
13
+ ThemeFonts,
14
+ ColorScaleOverride,
15
+ ColorOverrides,
16
+ AccentColor,
17
+ GrayColor,
18
+ RadiusToken,
19
+ ScalingMode,
20
+ } from './theme/theme.types'
21
+
22
+ // Utils
23
+ export { applyScaling } from './utils/applyScaling'
24
+ export { resolveSpace } from './utils/resolveSpace'
25
+ export { resolveColor } from './utils/resolveColor'
26
+ export { getClassicEffect } from './utils/classicEffect'
27
+
28
+ // Components
29
+ export { Box, type BoxProps } from './components/layout'
30
+ export {
31
+ Flex, type FlexProps, type FlexDirection, type FlexAlign, type FlexJustify, type FlexWrap,
32
+ } from './components/layout'
33
+ export {
34
+ Grid, type GridProps, type GridAlign, type GridJustify,
35
+ } from './components/layout'
36
+ export {
37
+ Text, type TextProps, type TextSize, type TextWeight, type TextAlign, type TextWrap,
38
+ Heading, type HeadingProps, type HeadingSize,
39
+ Link, type LinkProps, type LinkUnderline,
40
+ Blockquote, type BlockquoteProps,
41
+ Em, type EmProps,
42
+ Strong, type StrongProps,
43
+ Code, type CodeProps, type CodeVariant,
44
+ Kbd, type KbdProps, type KbdVariant,
45
+ Quote, type QuoteProps,
46
+ } from './components/typography'
47
+
48
+ // Actions
49
+ export {
50
+ Button, type ButtonProps, type ButtonSize, type ButtonVariant,
51
+ Checkbox, type CheckboxProps, type CheckboxSize, type CheckboxVariant, type CheckedState,
52
+ CheckboxGroup, type CheckboxGroupProps, type CheckboxGroupItemProps,
53
+ CheckboxCards, type CheckboxCardsProps, type CheckboxCardsItemProps, type CheckboxCardsVariant,
54
+ } from './components/actions'
55
+
56
+ // Playground
57
+ export { ThemeControls, type ThemeControlsProps } from './components/playground'
58
+
59
+ // Tokens
60
+ export { space, type SpaceToken, type MarginToken } from './tokens/spacing'
61
+ export { fontSize, lineHeight, headingLineHeight, letterSpacingEm, type FontSizeToken } from './tokens/typography'
62
+ export { getRadius, getFullRadius, type RadiusLevel } from './tokens/radius'
63
+ export { scalingMap } from './tokens/scaling'
@@ -0,0 +1,12 @@
1
+ import React from 'react'
2
+ import { ThemeContext } from './ThemeContext'
3
+ import { ThemeRoot } from './ThemeRoot'
4
+ import { ThemeImpl } from './ThemeImpl'
5
+ import type { ThemeProps } from './theme.types'
6
+
7
+ export function Theme(props: ThemeProps) {
8
+ const context = React.useContext(ThemeContext)
9
+ const isRoot = context === undefined
10
+ return isRoot ? <ThemeRoot {...props} /> : <ThemeImpl {...props} />
11
+ }
12
+ Theme.displayName = 'Theme'
@@ -0,0 +1,4 @@
1
+ import React from 'react'
2
+ import type { ThemeContextValue } from './theme.types'
3
+
4
+ export const ThemeContext = React.createContext<ThemeContextValue | undefined>(undefined)
@@ -0,0 +1,54 @@
1
+ import React from 'react'
2
+ import { ThemeContext } from './ThemeContext'
3
+ import { resolveGrayColor } from './resolveGrayColor'
4
+ import type { ThemeProps, ThemeContextValue } from './theme.types'
5
+
6
+ export function ThemeImpl({
7
+ appearance: appearanceProp,
8
+ accentColor: accentColorProp,
9
+ grayColor: grayColorProp,
10
+ radius: radiusProp,
11
+ scaling: scalingProp,
12
+ fonts: fontsProp,
13
+ colorOverrides: colorOverridesProp,
14
+ children,
15
+ }: ThemeProps) {
16
+ // Always defined here — ThemeImpl is only rendered when context exists
17
+ const parent = React.useContext(ThemeContext)!
18
+
19
+ const appearance =
20
+ appearanceProp !== undefined && appearanceProp !== 'inherit'
21
+ ? appearanceProp
22
+ : parent.appearance
23
+
24
+ const accentColor = accentColorProp ?? parent.accentColor
25
+ // Inherit resolvedGrayColor from parent (same as Radix) so 'auto' doesn't
26
+ // re-resolve against the nested accentColor unless explicitly set
27
+ const grayColor = grayColorProp ?? parent.resolvedGrayColor
28
+ const radius = radiusProp ?? parent.radius
29
+ const scaling = scalingProp ?? parent.scaling
30
+ const resolvedGrayColor = grayColor === 'auto' ? resolveGrayColor(accentColor) : grayColor
31
+
32
+ const value = React.useMemo<ThemeContextValue>(
33
+ () => ({
34
+ appearance,
35
+ accentColor,
36
+ grayColor,
37
+ resolvedGrayColor,
38
+ radius,
39
+ scaling,
40
+ fonts: fontsProp ?? parent.fonts,
41
+ colorOverrides: colorOverridesProp ?? parent.colorOverrides,
42
+ // Handlers always bubble up to ThemeRoot
43
+ onAppearanceChange: parent.onAppearanceChange,
44
+ onAccentColorChange: parent.onAccentColorChange,
45
+ onGrayColorChange: parent.onGrayColorChange,
46
+ onRadiusChange: parent.onRadiusChange,
47
+ onScalingChange: parent.onScalingChange,
48
+ }),
49
+ [appearance, accentColor, grayColor, resolvedGrayColor, radius, scaling, fontsProp, colorOverridesProp, parent],
50
+ )
51
+
52
+ return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>
53
+ }
54
+ ThemeImpl.displayName = 'ThemeImpl'