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.
- package/LICENSE +21 -0
- package/README.md +51 -0
- package/dist/index.cjs +4323 -0
- package/dist/index.d.cts +861 -0
- package/dist/index.d.mts +861 -0
- package/dist/index.d.ts +861 -0
- package/dist/index.mjs +4285 -0
- package/package.json +51 -0
- package/src/components/actions/Button.tsx +337 -0
- package/src/components/actions/Checkbox.tsx +216 -0
- package/src/components/actions/CheckboxCards.tsx +257 -0
- package/src/components/actions/CheckboxGroup.tsx +249 -0
- package/src/components/actions/index.ts +4 -0
- package/src/components/layout/Box.tsx +108 -0
- package/src/components/layout/Flex.tsx +149 -0
- package/src/components/layout/Grid.tsx +224 -0
- package/src/components/layout/index.ts +9 -0
- package/src/components/playground/ThemeControls.tsx +456 -0
- package/src/components/playground/index.ts +1 -0
- package/src/components/typography/Blockquote.tsx +137 -0
- package/src/components/typography/Code.tsx +164 -0
- package/src/components/typography/Em.tsx +68 -0
- package/src/components/typography/Heading.tsx +136 -0
- package/src/components/typography/Kbd.tsx +162 -0
- package/src/components/typography/Link.tsx +173 -0
- package/src/components/typography/Quote.tsx +71 -0
- package/src/components/typography/Strong.tsx +70 -0
- package/src/components/typography/Text.tsx +140 -0
- package/src/components/typography/index.ts +9 -0
- package/src/hooks/useResolveColor.ts +24 -0
- package/src/hooks/useThemeContext.ts +11 -0
- package/src/index.ts +63 -0
- package/src/theme/Theme.tsx +12 -0
- package/src/theme/ThemeContext.ts +4 -0
- package/src/theme/ThemeImpl.tsx +54 -0
- package/src/theme/ThemeRoot.tsx +65 -0
- package/src/theme/createTheme.tsx +17 -0
- package/src/theme/resolveGrayColor.ts +38 -0
- package/src/theme/theme.types.ts +95 -0
- package/src/tokens/colors/amber.ts +28 -0
- package/src/tokens/colors/blue.ts +28 -0
- package/src/tokens/colors/bronze.ts +28 -0
- package/src/tokens/colors/brown.ts +28 -0
- package/src/tokens/colors/crimson.ts +28 -0
- package/src/tokens/colors/cyan.ts +28 -0
- package/src/tokens/colors/gold.ts +28 -0
- package/src/tokens/colors/grass.ts +28 -0
- package/src/tokens/colors/gray.ts +28 -0
- package/src/tokens/colors/green.ts +28 -0
- package/src/tokens/colors/index.ts +36 -0
- package/src/tokens/colors/indigo.ts +28 -0
- package/src/tokens/colors/iris.ts +28 -0
- package/src/tokens/colors/jade.ts +28 -0
- package/src/tokens/colors/lime.ts +28 -0
- package/src/tokens/colors/mauve.ts +28 -0
- package/src/tokens/colors/mint.ts +28 -0
- package/src/tokens/colors/olive.ts +28 -0
- package/src/tokens/colors/orange.ts +28 -0
- package/src/tokens/colors/pink.ts +28 -0
- package/src/tokens/colors/plum.ts +28 -0
- package/src/tokens/colors/purple.ts +28 -0
- package/src/tokens/colors/red.ts +28 -0
- package/src/tokens/colors/ruby.ts +28 -0
- package/src/tokens/colors/sage.ts +28 -0
- package/src/tokens/colors/sand.ts +28 -0
- package/src/tokens/colors/sky.ts +28 -0
- package/src/tokens/colors/slate.ts +28 -0
- package/src/tokens/colors/teal.ts +28 -0
- package/src/tokens/colors/tomato.ts +28 -0
- package/src/tokens/colors/types.ts +69 -0
- package/src/tokens/colors/violet.ts +28 -0
- package/src/tokens/colors/yellow.ts +28 -0
- package/src/tokens/index.ts +5 -0
- package/src/tokens/radius.ts +56 -0
- package/src/tokens/scaling.ts +10 -0
- package/src/tokens/spacing.ts +21 -0
- package/src/tokens/typography.ts +60 -0
- package/src/utils/applyScaling.ts +6 -0
- package/src/utils/classicEffect.ts +46 -0
- package/src/utils/resolveColor.ts +69 -0
- 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,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'
|