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,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'