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,164 @@
|
|
|
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, letterSpacingEm } from '../../tokens/typography'
|
|
8
|
+
import { scalingMap } from '../../tokens/scaling'
|
|
9
|
+
import { getRadius } from '../../tokens/radius'
|
|
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 type CodeVariant = 'solid' | 'soft' | 'outline' | 'ghost'
|
|
18
|
+
|
|
19
|
+
export interface CodeProps extends Omit<RNTextProps, 'style'> {
|
|
20
|
+
/** Text size token (1–9). Default: inherits context — set to 3 (16px) if no parent. */
|
|
21
|
+
size?: FontSizeToken
|
|
22
|
+
/**
|
|
23
|
+
* Visual style.
|
|
24
|
+
* 'soft' → light accent background, accent-a11 text (default)
|
|
25
|
+
* 'solid' → filled accent-9 background, contrast text
|
|
26
|
+
* 'outline' → transparent background with accent border
|
|
27
|
+
* 'ghost' → no background, no border
|
|
28
|
+
*/
|
|
29
|
+
variant?: CodeVariant
|
|
30
|
+
weight?: TextWeight
|
|
31
|
+
/** Accent color. Default: theme accent. */
|
|
32
|
+
color?: AccentColor
|
|
33
|
+
/** Increases color contrast (a11 → 12 for text in soft/outline/ghost). */
|
|
34
|
+
highContrast?: boolean
|
|
35
|
+
/** Truncates text with an ellipsis when it overflows. */
|
|
36
|
+
truncate?: boolean
|
|
37
|
+
/** Controls text wrapping. 'pretty'/'balance' are not supported in React Native, no-op. */
|
|
38
|
+
wrap?: TextWrap
|
|
39
|
+
// ─── Margin props ──────────────────────────────────────────────────────────
|
|
40
|
+
m?: MarginToken
|
|
41
|
+
mx?: MarginToken
|
|
42
|
+
my?: MarginToken
|
|
43
|
+
mt?: MarginToken
|
|
44
|
+
mr?: MarginToken
|
|
45
|
+
mb?: MarginToken
|
|
46
|
+
ml?: MarginToken
|
|
47
|
+
style?: StyleProp<TextStyle>
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// ─── Constants ────────────────────────────────────────────────────────────────
|
|
51
|
+
|
|
52
|
+
const FONT_WEIGHT: Record<TextWeight, NonNullable<TextStyle['fontWeight']>> = {
|
|
53
|
+
light: '300',
|
|
54
|
+
regular: '400',
|
|
55
|
+
medium: '500',
|
|
56
|
+
bold: '700',
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// ─── Component ────────────────────────────────────────────────────────────────
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Inline code. Can be nested inside `<Text>` for inline use.
|
|
63
|
+
* Can be nested inside `<Text>` for inline use.
|
|
64
|
+
*
|
|
65
|
+
* Note: In RN, `backgroundColor` on inline Text fills the tight text bounds
|
|
66
|
+
* without padding/borderRadius support when nested. For block-level code with
|
|
67
|
+
* full visual fidelity, wrap in a `<Box>` with explicit styling.
|
|
68
|
+
*/
|
|
69
|
+
export function Code({
|
|
70
|
+
size = 3,
|
|
71
|
+
variant = 'soft',
|
|
72
|
+
weight,
|
|
73
|
+
color,
|
|
74
|
+
highContrast,
|
|
75
|
+
truncate,
|
|
76
|
+
wrap,
|
|
77
|
+
m, mx, my, mt, mr, mb, ml,
|
|
78
|
+
style,
|
|
79
|
+
...rest
|
|
80
|
+
}: CodeProps) {
|
|
81
|
+
const { scaling, fonts, radius } = 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
|
+
// Rendered at 85% of the base size to visually match inline code conventions.
|
|
90
|
+
const resolvedSize = Math.round(fontSize[size] * scalingFactor * 0.85)
|
|
91
|
+
const resolvedLetterSpacing = letterSpacingEm[size] * resolvedSize
|
|
92
|
+
|
|
93
|
+
// ─── Color helpers ───────────────────────────────────────────────────────────
|
|
94
|
+
// When `color` prop is set, use that color's steps; otherwise use 'accent-*'
|
|
95
|
+
const prefix = color ?? 'accent'
|
|
96
|
+
|
|
97
|
+
const textColor = (() => {
|
|
98
|
+
switch (variant) {
|
|
99
|
+
case 'solid':
|
|
100
|
+
return rc(`${prefix}-contrast` as Parameters<typeof rc>[0])
|
|
101
|
+
case 'soft':
|
|
102
|
+
case 'outline':
|
|
103
|
+
case 'ghost':
|
|
104
|
+
default:
|
|
105
|
+
return rc((highContrast ? `${prefix}-12` : `${prefix}-a11`) as Parameters<typeof rc>[0])
|
|
106
|
+
}
|
|
107
|
+
})()
|
|
108
|
+
|
|
109
|
+
const backgroundColor = (() => {
|
|
110
|
+
switch (variant) {
|
|
111
|
+
case 'solid': return rc(`${prefix}-9` as Parameters<typeof rc>[0])
|
|
112
|
+
case 'soft': return rc(`${prefix}-a3` as Parameters<typeof rc>[0])
|
|
113
|
+
default: return undefined
|
|
114
|
+
}
|
|
115
|
+
})()
|
|
116
|
+
|
|
117
|
+
const borderColor = variant === 'outline'
|
|
118
|
+
? rc(`${prefix}-a7` as Parameters<typeof rc>[0])
|
|
119
|
+
: undefined
|
|
120
|
+
|
|
121
|
+
// ─── Font family ─────────────────────────────────────────────────────────────
|
|
122
|
+
const effectiveWeight: TextWeight = weight ?? 'regular'
|
|
123
|
+
const fontFamily = fonts.code ?? fonts[effectiveWeight] ?? fonts.regular
|
|
124
|
+
|
|
125
|
+
// ─── Wrapping / truncation ──────────────────────────────────────────────────
|
|
126
|
+
const numberOfLines = truncate ? 1 : wrap === 'nowrap' ? 1 : undefined
|
|
127
|
+
const ellipsizeMode = truncate ? 'tail' : wrap === 'nowrap' ? 'clip' : undefined
|
|
128
|
+
|
|
129
|
+
// ─── Padding — proportional to font size (mirrors Radix's 0.1em / 0.35em) ───
|
|
130
|
+
const paddingVertical = Math.round(resolvedSize * 0.1)
|
|
131
|
+
const paddingHorizontal = Math.round(resolvedSize * 0.35)
|
|
132
|
+
|
|
133
|
+
// ─── Style ──────────────────────────────────────────────────────────────────
|
|
134
|
+
const codeStyle: TextStyle = {
|
|
135
|
+
fontSize: resolvedSize,
|
|
136
|
+
letterSpacing: resolvedLetterSpacing,
|
|
137
|
+
fontWeight: weight ? FONT_WEIGHT[weight] : undefined,
|
|
138
|
+
fontFamily,
|
|
139
|
+
color: textColor,
|
|
140
|
+
backgroundColor,
|
|
141
|
+
borderRadius: getRadius(radius, 1),
|
|
142
|
+
borderWidth: borderColor ? 1 : undefined,
|
|
143
|
+
borderColor,
|
|
144
|
+
paddingVertical,
|
|
145
|
+
paddingHorizontal,
|
|
146
|
+
// Shrink to content width when used standalone (no-op when nested inline in Text).
|
|
147
|
+
alignSelf: variant !== 'ghost' ? 'flex-start' : undefined,
|
|
148
|
+
// Margins
|
|
149
|
+
marginTop: sp(mt ?? my ?? m),
|
|
150
|
+
marginBottom: sp(mb ?? my ?? m),
|
|
151
|
+
marginLeft: sp(ml ?? mx ?? m),
|
|
152
|
+
marginRight: sp(mr ?? mx ?? m),
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return (
|
|
156
|
+
<RNText
|
|
157
|
+
numberOfLines={numberOfLines}
|
|
158
|
+
ellipsizeMode={ellipsizeMode}
|
|
159
|
+
style={[codeStyle, style]}
|
|
160
|
+
{...rest}
|
|
161
|
+
/>
|
|
162
|
+
)
|
|
163
|
+
}
|
|
164
|
+
Code.displayName = 'Code'
|
|
@@ -0,0 +1,68 @@
|
|
|
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 EmProps 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 italic text. Can be nested inside `<Text>` for inline use.
|
|
31
|
+
* Can be nested inside `<Text>` for inline use.
|
|
32
|
+
*/
|
|
33
|
+
export function Em({
|
|
34
|
+
truncate,
|
|
35
|
+
wrap,
|
|
36
|
+
m, mx, my, mt, mr, mb, ml,
|
|
37
|
+
style,
|
|
38
|
+
...rest
|
|
39
|
+
}: EmProps) {
|
|
40
|
+
const { scaling } = useThemeContext()
|
|
41
|
+
|
|
42
|
+
const sp = (token: MarginToken | undefined): number | undefined =>
|
|
43
|
+
token !== undefined ? resolveSpace(token, scaling) : undefined
|
|
44
|
+
|
|
45
|
+
const numberOfLines = truncate ? 1 : wrap === 'nowrap' ? 1 : undefined
|
|
46
|
+
const ellipsizeMode = truncate ? 'tail' : wrap === 'nowrap' ? 'clip' : undefined
|
|
47
|
+
|
|
48
|
+
const emStyle: TextStyle = {
|
|
49
|
+
fontStyle: 'italic',
|
|
50
|
+
// RN defaults flexShrink to 0; CSS defaults to 1. Without this, text
|
|
51
|
+
// inside a Flex row overflows instead of wrapping to the available width.
|
|
52
|
+
flexShrink: 1,
|
|
53
|
+
marginTop: sp(mt ?? my ?? m),
|
|
54
|
+
marginBottom: sp(mb ?? my ?? m),
|
|
55
|
+
marginLeft: sp(ml ?? mx ?? m),
|
|
56
|
+
marginRight: sp(mr ?? mx ?? m),
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
return (
|
|
60
|
+
<RNText
|
|
61
|
+
numberOfLines={numberOfLines}
|
|
62
|
+
ellipsizeMode={ellipsizeMode}
|
|
63
|
+
style={[emStyle, style]}
|
|
64
|
+
{...rest}
|
|
65
|
+
/>
|
|
66
|
+
)
|
|
67
|
+
}
|
|
68
|
+
Em.displayName = 'Em'
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { Text as RNText } from 'react-native'
|
|
3
|
+
import type { 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, headingLineHeight, 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, TextAlign, TextWrap } from './Text'
|
|
13
|
+
|
|
14
|
+
// ─── Types ────────────────────────────────────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
export type HeadingSize = FontSizeToken
|
|
17
|
+
|
|
18
|
+
export interface HeadingProps {
|
|
19
|
+
/** Heading size token (1–9). Default: 6 (24px). */
|
|
20
|
+
size?: HeadingSize
|
|
21
|
+
/** Font weight. Default: 'bold'. */
|
|
22
|
+
weight?: TextWeight
|
|
23
|
+
/** Text alignment. */
|
|
24
|
+
align?: TextAlign
|
|
25
|
+
/** Truncates text with an ellipsis when it overflows its container. */
|
|
26
|
+
truncate?: boolean
|
|
27
|
+
/**
|
|
28
|
+
* Controls text wrapping.
|
|
29
|
+
* 'nowrap' → single line with clip. 'pretty'/'balance' → not supported in React Native, no-op.
|
|
30
|
+
*/
|
|
31
|
+
wrap?: TextWrap
|
|
32
|
+
/**
|
|
33
|
+
* Text color from the theme palette.
|
|
34
|
+
* No color → gray-12; color → {color}-a11; color + highContrast → {color}-12.
|
|
35
|
+
*/
|
|
36
|
+
color?: AccentColor
|
|
37
|
+
/** Increases color contrast when `color` is set (a11 → solid 12). */
|
|
38
|
+
highContrast?: boolean
|
|
39
|
+
// ─── Margin props ──────────────────────────────────────────────────────────
|
|
40
|
+
m?: MarginToken
|
|
41
|
+
mx?: MarginToken
|
|
42
|
+
my?: MarginToken
|
|
43
|
+
mt?: MarginToken
|
|
44
|
+
mr?: MarginToken
|
|
45
|
+
mb?: MarginToken
|
|
46
|
+
ml?: MarginToken
|
|
47
|
+
style?: StyleProp<TextStyle>
|
|
48
|
+
children?: React.ReactNode
|
|
49
|
+
accessibilityLabel?: string
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// ─── Constants ────────────────────────────────────────────────────────────────
|
|
53
|
+
|
|
54
|
+
const FONT_WEIGHT: Record<TextWeight, NonNullable<TextStyle['fontWeight']>> = {
|
|
55
|
+
light: '300',
|
|
56
|
+
regular: '400',
|
|
57
|
+
medium: '500',
|
|
58
|
+
bold: '700',
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ─── Component ────────────────────────────────────────────────────────────────
|
|
62
|
+
|
|
63
|
+
export function Heading({
|
|
64
|
+
size = 6,
|
|
65
|
+
weight = 'bold',
|
|
66
|
+
align,
|
|
67
|
+
truncate,
|
|
68
|
+
wrap,
|
|
69
|
+
color,
|
|
70
|
+
highContrast,
|
|
71
|
+
m, mx, my, mt, mr, mb, ml,
|
|
72
|
+
style,
|
|
73
|
+
children,
|
|
74
|
+
...rest
|
|
75
|
+
}: HeadingProps) {
|
|
76
|
+
const { scaling, fonts } = useThemeContext()
|
|
77
|
+
const rc = useResolveColor()
|
|
78
|
+
|
|
79
|
+
const sp = (token: MarginToken | undefined): number | undefined =>
|
|
80
|
+
token !== undefined ? resolveSpace(token, scaling) : undefined
|
|
81
|
+
|
|
82
|
+
// ─── Typography ─────────────────────────────────────────────────────────────
|
|
83
|
+
// Heading uses the same font size scale as Text but has its own tighter
|
|
84
|
+
// line heights. Letter spacing values are identical to Text.
|
|
85
|
+
const scalingFactor = scalingMap[scaling]
|
|
86
|
+
const resolvedSize = Math.round(fontSize[size] * scalingFactor)
|
|
87
|
+
const resolvedLineHeight = Math.round(headingLineHeight[size] * scalingFactor)
|
|
88
|
+
const resolvedLetterSpacing = letterSpacingEm[size] * resolvedSize
|
|
89
|
+
|
|
90
|
+
// ─── Color ──────────────────────────────────────────────────────────────────
|
|
91
|
+
// Same logic as Text.
|
|
92
|
+
const textColor = color
|
|
93
|
+
? rc(highContrast ? `${color}-12` : `${color}-a11`)
|
|
94
|
+
: rc('gray-12')
|
|
95
|
+
|
|
96
|
+
// ─── Font family ─────────────────────────────────────────────────────────────
|
|
97
|
+
// Heading prefers fonts.heading, then the specific weight's font, then bold, then regular.
|
|
98
|
+
const effectiveWeight: TextWeight = weight ?? 'bold'
|
|
99
|
+
const fontFamily = fonts.heading ?? fonts[effectiveWeight] ?? fonts.bold ?? fonts.regular
|
|
100
|
+
|
|
101
|
+
// ─── Wrapping / truncation ──────────────────────────────────────────────────
|
|
102
|
+
const numberOfLines = truncate ? 1 : wrap === 'nowrap' ? 1 : undefined
|
|
103
|
+
const ellipsizeMode = truncate ? 'tail' : wrap === 'nowrap' ? 'clip' : undefined
|
|
104
|
+
|
|
105
|
+
// ─── Style ──────────────────────────────────────────────────────────────────
|
|
106
|
+
const headingStyle: TextStyle = {
|
|
107
|
+
fontSize: resolvedSize,
|
|
108
|
+
lineHeight: resolvedLineHeight,
|
|
109
|
+
letterSpacing: resolvedLetterSpacing,
|
|
110
|
+
color: textColor,
|
|
111
|
+
textAlign: align,
|
|
112
|
+
fontWeight: FONT_WEIGHT[weight],
|
|
113
|
+
fontFamily,
|
|
114
|
+
// RN defaults flexShrink to 0; CSS defaults to 1. Without this, text
|
|
115
|
+
// inside a Flex row overflows instead of wrapping to the available width.
|
|
116
|
+
flexShrink: 1,
|
|
117
|
+
// Margins
|
|
118
|
+
marginTop: sp(mt ?? my ?? m),
|
|
119
|
+
marginBottom: sp(mb ?? my ?? m),
|
|
120
|
+
marginLeft: sp(ml ?? mx ?? m),
|
|
121
|
+
marginRight: sp(mr ?? mx ?? m),
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
return (
|
|
125
|
+
<RNText
|
|
126
|
+
accessibilityRole="header"
|
|
127
|
+
numberOfLines={numberOfLines}
|
|
128
|
+
ellipsizeMode={ellipsizeMode}
|
|
129
|
+
style={[headingStyle, style]}
|
|
130
|
+
{...rest}
|
|
131
|
+
>
|
|
132
|
+
{children}
|
|
133
|
+
</RNText>
|
|
134
|
+
)
|
|
135
|
+
}
|
|
136
|
+
Heading.displayName = 'Heading'
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { Platform, 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, letterSpacingEm } from '../../tokens/typography'
|
|
8
|
+
import { scalingMap } from '../../tokens/scaling'
|
|
9
|
+
import { getRadius } from '../../tokens/radius'
|
|
10
|
+
import type { FontSizeToken } from '../../tokens/typography'
|
|
11
|
+
import type { MarginToken } from '../../tokens/spacing'
|
|
12
|
+
|
|
13
|
+
// ─── Types ────────────────────────────────────────────────────────────────────
|
|
14
|
+
|
|
15
|
+
export type KbdVariant = 'classic' | 'soft'
|
|
16
|
+
|
|
17
|
+
export interface KbdProps {
|
|
18
|
+
/**
|
|
19
|
+
* Text size token (1–9).
|
|
20
|
+
*
|
|
21
|
+
* In Radix web, Kbd uses `0.75em` to inherit from the parent.
|
|
22
|
+
* In RN there are no relative units, so `size` sets the *parent* text size
|
|
23
|
+
* and the Kbd font is derived as `fontSize[size] * 0.8` (matching Radix).
|
|
24
|
+
*/
|
|
25
|
+
size?: FontSizeToken
|
|
26
|
+
/** Visual variant. Default: `'classic'`. */
|
|
27
|
+
variant?: KbdVariant
|
|
28
|
+
// ─── Margin props (Radix layout) ──────────────────────────────────────────
|
|
29
|
+
m?: MarginToken
|
|
30
|
+
mx?: MarginToken
|
|
31
|
+
my?: MarginToken
|
|
32
|
+
mt?: MarginToken
|
|
33
|
+
mr?: MarginToken
|
|
34
|
+
mb?: MarginToken
|
|
35
|
+
ml?: MarginToken
|
|
36
|
+
style?: StyleProp<ViewStyle>
|
|
37
|
+
children?: React.ReactNode
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// ─── Component ────────────────────────────────────────────────────────────────
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Keyboard key indicator — styled to look like a physical key.
|
|
44
|
+
*
|
|
45
|
+
* Follows the Radix Themes Kbd API:
|
|
46
|
+
* - `size` 1-9 (default 2)
|
|
47
|
+
* - `variant` 'classic' (raised key with shadow) | 'soft' (flat tinted bg)
|
|
48
|
+
* - Margin props: m, mx, my, mt, mr, mb, ml
|
|
49
|
+
*
|
|
50
|
+
* When nested inline inside `<Text>`, RN places the View's bottom at the
|
|
51
|
+
* text baseline. We use `transform: translateY` to shift it into alignment
|
|
52
|
+
* (marginBottom is ignored for inline Views in Text).
|
|
53
|
+
*/
|
|
54
|
+
export function Kbd({
|
|
55
|
+
size = 2,
|
|
56
|
+
variant = 'classic',
|
|
57
|
+
m, mx, my, mt, mr, mb, ml,
|
|
58
|
+
style,
|
|
59
|
+
children,
|
|
60
|
+
}: KbdProps) {
|
|
61
|
+
const { scaling, fonts, radius } = useThemeContext()
|
|
62
|
+
const rc = useResolveColor()
|
|
63
|
+
|
|
64
|
+
const sp = (token: MarginToken | undefined): number | undefined =>
|
|
65
|
+
token !== undefined ? resolveSpace(token, scaling) : undefined
|
|
66
|
+
|
|
67
|
+
const sf = scalingMap[scaling]
|
|
68
|
+
|
|
69
|
+
// Radix: font-size = fontSize[size] * 0.8
|
|
70
|
+
const kbdFontSize = Math.round(fontSize[size] * sf * 0.8)
|
|
71
|
+
// Radix: border-radius = radiusFactor * 0.35em (relative to kbdFontSize)
|
|
72
|
+
const borderR = Math.max(2, Math.round(getRadius(radius, 2) * 0.35 * (kbdFontSize / 14)))
|
|
73
|
+
// Radix: min-width 1.75em, line-height 1.7em (em = kbdFontSize)
|
|
74
|
+
const minW = Math.round(kbdFontSize * 1.75)
|
|
75
|
+
const kbdHeight = Math.round(kbdFontSize * 1.7)
|
|
76
|
+
// Radix: padding 0.5em horizontal, 0.05em bottom
|
|
77
|
+
const ph = Math.round(kbdFontSize * 0.5)
|
|
78
|
+
const pb = Math.round(kbdFontSize * 0.05)
|
|
79
|
+
|
|
80
|
+
// Inline alignment: shift down so Kbd centers with surrounding text.
|
|
81
|
+
// In RN, inline View bottom = text baseline. Kbd sticks above by
|
|
82
|
+
// (kbdHeight - ascender). We shift it down by that amount.
|
|
83
|
+
const parentFontSize = Math.round(fontSize[size] * sf)
|
|
84
|
+
const ascender = Math.round(parentFontSize * 0.82)
|
|
85
|
+
const translateY = Math.round((kbdHeight - ascender) * 0.5)
|
|
86
|
+
|
|
87
|
+
const isClassic = variant === 'classic'
|
|
88
|
+
|
|
89
|
+
const containerStyle: ViewStyle = {
|
|
90
|
+
alignSelf: 'flex-start',
|
|
91
|
+
flexDirection: 'row',
|
|
92
|
+
alignItems: 'center',
|
|
93
|
+
justifyContent: 'center',
|
|
94
|
+
height: kbdHeight,
|
|
95
|
+
minWidth: minW,
|
|
96
|
+
paddingHorizontal: ph,
|
|
97
|
+
paddingBottom: pb,
|
|
98
|
+
borderRadius: borderR,
|
|
99
|
+
backgroundColor: isClassic ? rc('gray-1') : rc('gray-a3'),
|
|
100
|
+
// Classic uses border to approximate box-shadow outline + bottom shadow
|
|
101
|
+
...(isClassic
|
|
102
|
+
? {
|
|
103
|
+
borderWidth: 1,
|
|
104
|
+
borderColor: rc('gray-a5'),
|
|
105
|
+
borderBottomWidth: 2,
|
|
106
|
+
borderBottomColor: rc('gray-a7'),
|
|
107
|
+
}
|
|
108
|
+
: {}),
|
|
109
|
+
transform: [{ translateY }],
|
|
110
|
+
// iOS shadow approximates the outer drop shadow from Radix box-shadow
|
|
111
|
+
...(isClassic
|
|
112
|
+
? Platform.select({
|
|
113
|
+
ios: {
|
|
114
|
+
shadowColor: rc('gray-12'),
|
|
115
|
+
shadowOffset: { width: 0, height: 1 },
|
|
116
|
+
shadowOpacity: 0.12,
|
|
117
|
+
shadowRadius: 1,
|
|
118
|
+
},
|
|
119
|
+
android: {
|
|
120
|
+
elevation: 1,
|
|
121
|
+
},
|
|
122
|
+
})
|
|
123
|
+
: {}),
|
|
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
|
+
const textStyle: TextStyle = {
|
|
132
|
+
fontSize: kbdFontSize,
|
|
133
|
+
// Radix uses the default (body) font, NOT monospace
|
|
134
|
+
fontFamily: fonts.regular,
|
|
135
|
+
fontWeight: 'normal',
|
|
136
|
+
color: rc('gray-12'),
|
|
137
|
+
letterSpacing: letterSpacingEm[size] * kbdFontSize,
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Classic variant: thin highlight line at the top to simulate
|
|
141
|
+
// the inset box-shadow highlight from Radix
|
|
142
|
+
const highlightStyle: ViewStyle | null = isClassic
|
|
143
|
+
? {
|
|
144
|
+
position: 'absolute',
|
|
145
|
+
top: 0,
|
|
146
|
+
left: 1,
|
|
147
|
+
right: 1,
|
|
148
|
+
height: 1,
|
|
149
|
+
backgroundColor: rc('gray-a3'),
|
|
150
|
+
borderTopLeftRadius: Math.max(0, borderR - 1),
|
|
151
|
+
borderTopRightRadius: Math.max(0, borderR - 1),
|
|
152
|
+
}
|
|
153
|
+
: null
|
|
154
|
+
|
|
155
|
+
return (
|
|
156
|
+
<View style={[containerStyle, style]}>
|
|
157
|
+
{highlightStyle && <View style={highlightStyle} />}
|
|
158
|
+
<RNText style={textStyle}>{children}</RNText>
|
|
159
|
+
</View>
|
|
160
|
+
)
|
|
161
|
+
}
|
|
162
|
+
Kbd.displayName = 'Kbd'
|