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,257 @@
|
|
|
1
|
+
import React, { createContext, useCallback, useContext, useMemo } from 'react'
|
|
2
|
+
import { Pressable, View } from 'react-native'
|
|
3
|
+
import type { StyleProp, ViewStyle } from 'react-native'
|
|
4
|
+
import { useThemeContext } from '../../hooks/useThemeContext'
|
|
5
|
+
import { useResolveColor } from '../../hooks/useResolveColor'
|
|
6
|
+
import { resolveSpace } from '../../utils/resolveSpace'
|
|
7
|
+
import { scalingMap } from '../../tokens/scaling'
|
|
8
|
+
import { getRadius } from '../../tokens/radius'
|
|
9
|
+
import type { MarginToken, SpaceToken } from '../../tokens/spacing'
|
|
10
|
+
import type { AccentColor } from '../../tokens/colors/types'
|
|
11
|
+
import { Checkbox, type CheckboxSize } from './Checkbox'
|
|
12
|
+
|
|
13
|
+
// ─── Types ──────────────────────────────────────────────────────────────────────
|
|
14
|
+
|
|
15
|
+
export type CheckboxCardsVariant = 'surface' | 'classic'
|
|
16
|
+
|
|
17
|
+
// ─── Context ──────────────────────────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
interface CheckboxCardsContextValue {
|
|
20
|
+
size: CheckboxSize
|
|
21
|
+
variant: CheckboxCardsVariant
|
|
22
|
+
color?: AccentColor
|
|
23
|
+
highContrast?: boolean
|
|
24
|
+
disabled: boolean
|
|
25
|
+
value: string[]
|
|
26
|
+
onItemToggle: (itemValue: string) => void
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
const CheckboxCardsContext = createContext<CheckboxCardsContextValue | null>(null)
|
|
30
|
+
|
|
31
|
+
// ─── Root Types ─────────────────────────────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
export interface CheckboxCardsProps {
|
|
34
|
+
/** Card size (1–3). Default: 2. */
|
|
35
|
+
size?: CheckboxSize
|
|
36
|
+
/** Visual variant. Default: 'surface'. */
|
|
37
|
+
variant?: CheckboxCardsVariant
|
|
38
|
+
/** Accent color. Default: theme accent. */
|
|
39
|
+
color?: AccentColor
|
|
40
|
+
/** Increases color contrast with the background. */
|
|
41
|
+
highContrast?: boolean
|
|
42
|
+
/** Number of columns. Default: 1. */
|
|
43
|
+
columns?: number
|
|
44
|
+
/** Gap between cards as space token. Default: 4. */
|
|
45
|
+
gap?: SpaceToken
|
|
46
|
+
/** Controlled selected values. */
|
|
47
|
+
value?: string[]
|
|
48
|
+
/** Uncontrolled default selected values. */
|
|
49
|
+
defaultValue?: string[]
|
|
50
|
+
/** Called when selected values change. */
|
|
51
|
+
onValueChange?: (value: string[]) => void
|
|
52
|
+
/** Disables all cards. */
|
|
53
|
+
disabled?: boolean
|
|
54
|
+
/** Card items. */
|
|
55
|
+
children?: React.ReactNode
|
|
56
|
+
// ─── Margin props ──────────────────────────────────────────────────────────
|
|
57
|
+
m?: MarginToken
|
|
58
|
+
mx?: MarginToken
|
|
59
|
+
my?: MarginToken
|
|
60
|
+
mt?: MarginToken
|
|
61
|
+
mr?: MarginToken
|
|
62
|
+
mb?: MarginToken
|
|
63
|
+
ml?: MarginToken
|
|
64
|
+
style?: StyleProp<ViewStyle>
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// ─── Item Types ─────────────────────────────────────────────────────────────────
|
|
68
|
+
|
|
69
|
+
export interface CheckboxCardsItemProps {
|
|
70
|
+
/** Unique value identifying this card. */
|
|
71
|
+
value: string
|
|
72
|
+
/** Disables this individual card. */
|
|
73
|
+
disabled?: boolean
|
|
74
|
+
/** Card content. */
|
|
75
|
+
children?: React.ReactNode
|
|
76
|
+
style?: StyleProp<ViewStyle>
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ─── Size tokens (matching Radix CSS vars) ──────────────────────────────────────
|
|
80
|
+
// Radix uses: padding-left = space-3/4/5, padding-top/bottom = space-3/1.2, space-4*0.875, space-5/1.2
|
|
81
|
+
// checkbox-size = space-4*0.875, space-4, space-4*1.25
|
|
82
|
+
// padding-right = padding-left * 2 + checkbox-size
|
|
83
|
+
|
|
84
|
+
const SIZE_CONFIG: Record<CheckboxSize, {
|
|
85
|
+
paddingLeft: number
|
|
86
|
+
paddingY: number
|
|
87
|
+
checkboxSize: number
|
|
88
|
+
radiusLevel: 3 | 4
|
|
89
|
+
}> = {
|
|
90
|
+
1: { paddingLeft: 12, paddingY: 10, checkboxSize: 14, radiusLevel: 3 },
|
|
91
|
+
2: { paddingLeft: 16, paddingY: 14, checkboxSize: 16, radiusLevel: 3 },
|
|
92
|
+
3: { paddingLeft: 24, paddingY: 20, checkboxSize: 20, radiusLevel: 4 },
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// ─── Root Component ─────────────────────────────────────────────────────────────
|
|
96
|
+
|
|
97
|
+
function CheckboxCardsRoot({
|
|
98
|
+
size = 2,
|
|
99
|
+
variant = 'surface',
|
|
100
|
+
color,
|
|
101
|
+
highContrast,
|
|
102
|
+
columns = 1,
|
|
103
|
+
gap: gapProp = 4,
|
|
104
|
+
value: valueProp,
|
|
105
|
+
defaultValue = [],
|
|
106
|
+
onValueChange,
|
|
107
|
+
disabled = false,
|
|
108
|
+
children,
|
|
109
|
+
m, mx, my, mt, mr, mb, ml,
|
|
110
|
+
style,
|
|
111
|
+
}: CheckboxCardsProps) {
|
|
112
|
+
const { scaling } = useThemeContext()
|
|
113
|
+
|
|
114
|
+
const [internal, setInternal] = React.useState<string[]>(defaultValue)
|
|
115
|
+
const isControlled = valueProp !== undefined
|
|
116
|
+
const value = isControlled ? valueProp : internal
|
|
117
|
+
|
|
118
|
+
const onItemToggle = useCallback((itemValue: string) => {
|
|
119
|
+
const next = value.includes(itemValue)
|
|
120
|
+
? value.filter(v => v !== itemValue)
|
|
121
|
+
: [...value, itemValue]
|
|
122
|
+
if (!isControlled) setInternal(next)
|
|
123
|
+
onValueChange?.(next)
|
|
124
|
+
}, [value, isControlled, onValueChange])
|
|
125
|
+
|
|
126
|
+
const ctx = useMemo<CheckboxCardsContextValue>(() => ({
|
|
127
|
+
size, variant, color, highContrast, disabled, value, onItemToggle,
|
|
128
|
+
}), [size, variant, color, highContrast, disabled, value, onItemToggle])
|
|
129
|
+
|
|
130
|
+
const sp = (token: MarginToken | undefined): number | undefined =>
|
|
131
|
+
token !== undefined ? resolveSpace(token, scaling) : undefined
|
|
132
|
+
|
|
133
|
+
const resolvedGap = resolveSpace(gapProp, scaling)
|
|
134
|
+
|
|
135
|
+
return (
|
|
136
|
+
<CheckboxCardsContext.Provider value={ctx}>
|
|
137
|
+
<View
|
|
138
|
+
style={[
|
|
139
|
+
{
|
|
140
|
+
flexDirection: 'row',
|
|
141
|
+
flexWrap: 'wrap',
|
|
142
|
+
gap: resolvedGap,
|
|
143
|
+
marginTop: sp(mt ?? my ?? m),
|
|
144
|
+
marginBottom: sp(mb ?? my ?? m),
|
|
145
|
+
marginLeft: sp(ml ?? mx ?? m),
|
|
146
|
+
marginRight: sp(mr ?? mx ?? m),
|
|
147
|
+
},
|
|
148
|
+
style,
|
|
149
|
+
]}
|
|
150
|
+
>
|
|
151
|
+
{React.Children.map(children, child => (
|
|
152
|
+
<View style={{
|
|
153
|
+
flexBasis: columns > 1 ? `${100 / columns}%` as unknown as number : undefined,
|
|
154
|
+
flexGrow: 1,
|
|
155
|
+
flexShrink: 1,
|
|
156
|
+
minWidth: 0,
|
|
157
|
+
}}>
|
|
158
|
+
{child}
|
|
159
|
+
</View>
|
|
160
|
+
))}
|
|
161
|
+
</View>
|
|
162
|
+
</CheckboxCardsContext.Provider>
|
|
163
|
+
)
|
|
164
|
+
}
|
|
165
|
+
CheckboxCardsRoot.displayName = 'CheckboxCards.Root'
|
|
166
|
+
|
|
167
|
+
// ─── Item Component ─────────────────────────────────────────────────────────────
|
|
168
|
+
|
|
169
|
+
function CheckboxCardsItem({
|
|
170
|
+
value: itemValue,
|
|
171
|
+
disabled: disabledProp,
|
|
172
|
+
children,
|
|
173
|
+
style,
|
|
174
|
+
}: CheckboxCardsItemProps) {
|
|
175
|
+
const ctx = useContext(CheckboxCardsContext)
|
|
176
|
+
if (!ctx) throw new Error('CheckboxCards.Item must be used within CheckboxCards.Root')
|
|
177
|
+
|
|
178
|
+
const { size, variant, color, highContrast, disabled: groupDisabled, value, onItemToggle } = ctx
|
|
179
|
+
const { scaling, radius } = useThemeContext()
|
|
180
|
+
const rc = useResolveColor()
|
|
181
|
+
|
|
182
|
+
const isDisabled = disabledProp ?? groupDisabled
|
|
183
|
+
const isChecked = value.includes(itemValue)
|
|
184
|
+
|
|
185
|
+
const handlePress = useCallback(() => {
|
|
186
|
+
if (!isDisabled) onItemToggle(itemValue)
|
|
187
|
+
}, [isDisabled, onItemToggle, itemValue])
|
|
188
|
+
|
|
189
|
+
const scalingFactor = scalingMap[scaling]
|
|
190
|
+
const cfg = SIZE_CONFIG[size]
|
|
191
|
+
const paddingLeft = Math.round(cfg.paddingLeft * scalingFactor)
|
|
192
|
+
const paddingY = Math.round(cfg.paddingY * scalingFactor)
|
|
193
|
+
const checkboxSize = Math.round(cfg.checkboxSize * scalingFactor)
|
|
194
|
+
// Radix CSS: padding-right = padding-left * 2 + checkbox-size
|
|
195
|
+
const paddingRight = paddingLeft * 2 + checkboxSize
|
|
196
|
+
const borderRadius = getRadius(radius, cfg.radiusLevel)
|
|
197
|
+
|
|
198
|
+
type C = Parameters<typeof rc>[0]
|
|
199
|
+
|
|
200
|
+
// Card border & background — does NOT change on checked state (matches Radix)
|
|
201
|
+
// Surface: box-shadow: 0 0 0 1px gray-a5 (simulated with borderWidth)
|
|
202
|
+
// Classic: similar border + outer shadow
|
|
203
|
+
const borderColor = rc('gray-a5' as C)
|
|
204
|
+
const bgColor = rc('gray-surface' as C)
|
|
205
|
+
|
|
206
|
+
const cardStyle: ViewStyle = {
|
|
207
|
+
position: 'relative',
|
|
208
|
+
overflow: 'hidden',
|
|
209
|
+
paddingLeft,
|
|
210
|
+
paddingRight,
|
|
211
|
+
paddingTop: paddingY,
|
|
212
|
+
paddingBottom: paddingY,
|
|
213
|
+
borderRadius,
|
|
214
|
+
borderWidth: 1,
|
|
215
|
+
borderColor,
|
|
216
|
+
backgroundColor: bgColor,
|
|
217
|
+
opacity: isDisabled ? 0.5 : undefined,
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return (
|
|
221
|
+
<Pressable
|
|
222
|
+
onPress={handlePress}
|
|
223
|
+
disabled={isDisabled}
|
|
224
|
+
accessibilityRole="checkbox"
|
|
225
|
+
accessibilityState={{ checked: isChecked, disabled: isDisabled }}
|
|
226
|
+
style={[cardStyle, style]}
|
|
227
|
+
>
|
|
228
|
+
{children}
|
|
229
|
+
{/* Checkbox absolutely positioned on the right, matching Radix CSS */}
|
|
230
|
+
<View style={{
|
|
231
|
+
position: 'absolute',
|
|
232
|
+
right: paddingLeft,
|
|
233
|
+
top: 0,
|
|
234
|
+
bottom: 0,
|
|
235
|
+
justifyContent: 'center',
|
|
236
|
+
}}>
|
|
237
|
+
<Checkbox
|
|
238
|
+
size={size}
|
|
239
|
+
variant="surface"
|
|
240
|
+
color={color}
|
|
241
|
+
highContrast={highContrast}
|
|
242
|
+
checked={isChecked}
|
|
243
|
+
onCheckedChange={() => onItemToggle(itemValue)}
|
|
244
|
+
disabled={isDisabled}
|
|
245
|
+
/>
|
|
246
|
+
</View>
|
|
247
|
+
</Pressable>
|
|
248
|
+
)
|
|
249
|
+
}
|
|
250
|
+
CheckboxCardsItem.displayName = 'CheckboxCards.Item'
|
|
251
|
+
|
|
252
|
+
// ─── Compound export ────────────────────────────────────────────────────────────
|
|
253
|
+
|
|
254
|
+
export const CheckboxCards = Object.assign(CheckboxCardsRoot, {
|
|
255
|
+
Root: CheckboxCardsRoot,
|
|
256
|
+
Item: CheckboxCardsItem,
|
|
257
|
+
})
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import React, { createContext, useCallback, useContext, useMemo } from 'react'
|
|
2
|
+
import { Pressable, View } from 'react-native'
|
|
3
|
+
import { Text as RNText } from 'react-native'
|
|
4
|
+
import type { StyleProp, ViewStyle, TextStyle } from 'react-native'
|
|
5
|
+
import { useThemeContext } from '../../hooks/useThemeContext'
|
|
6
|
+
import { useResolveColor } from '../../hooks/useResolveColor'
|
|
7
|
+
import { resolveSpace } from '../../utils/resolveSpace'
|
|
8
|
+
import { fontSize, lineHeight, letterSpacingEm } from '../../tokens/typography'
|
|
9
|
+
import { scalingMap } from '../../tokens/scaling'
|
|
10
|
+
import type { MarginToken } from '../../tokens/spacing'
|
|
11
|
+
import type { AccentColor } from '../../tokens/colors/types'
|
|
12
|
+
import { Checkbox, type CheckboxSize, type CheckboxVariant } from './Checkbox'
|
|
13
|
+
|
|
14
|
+
// ─── Context ──────────────────────────────────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
interface CheckboxGroupContextValue {
|
|
17
|
+
size: CheckboxSize
|
|
18
|
+
variant: CheckboxVariant
|
|
19
|
+
color?: AccentColor
|
|
20
|
+
highContrast?: boolean
|
|
21
|
+
disabled: boolean
|
|
22
|
+
value: string[]
|
|
23
|
+
onItemToggle: (itemValue: string) => void
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const CheckboxGroupContext = createContext<CheckboxGroupContextValue | null>(null)
|
|
27
|
+
|
|
28
|
+
// ─── Root Types ─────────────────────────────────────────────────────────────────
|
|
29
|
+
|
|
30
|
+
export interface CheckboxGroupProps {
|
|
31
|
+
/** Checkbox size (1–3). Default: 2. */
|
|
32
|
+
size?: CheckboxSize
|
|
33
|
+
/** Visual variant. Default: 'surface'. */
|
|
34
|
+
variant?: CheckboxVariant
|
|
35
|
+
/** Accent color. Default: theme accent. */
|
|
36
|
+
color?: AccentColor
|
|
37
|
+
/** Increases color contrast with the background. */
|
|
38
|
+
highContrast?: boolean
|
|
39
|
+
/** Controlled selected values. */
|
|
40
|
+
value?: string[]
|
|
41
|
+
/** Uncontrolled default selected values. */
|
|
42
|
+
defaultValue?: string[]
|
|
43
|
+
/** Called when selected values change. */
|
|
44
|
+
onValueChange?: (value: string[]) => void
|
|
45
|
+
/** Disables all checkboxes in the group. */
|
|
46
|
+
disabled?: boolean
|
|
47
|
+
/** Group children (CheckboxGroup.Item). */
|
|
48
|
+
children?: React.ReactNode
|
|
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<ViewStyle>
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ─── Item Types ─────────────────────────────────────────────────────────────────
|
|
61
|
+
|
|
62
|
+
export interface CheckboxGroupItemProps {
|
|
63
|
+
/** Unique value identifying this item. */
|
|
64
|
+
value: string
|
|
65
|
+
/** Disables this individual item. */
|
|
66
|
+
disabled?: boolean
|
|
67
|
+
/** Label text or custom content. */
|
|
68
|
+
children?: React.ReactNode
|
|
69
|
+
// ─── Margin props ──────────────────────────────────────────────────────────
|
|
70
|
+
m?: MarginToken
|
|
71
|
+
mx?: MarginToken
|
|
72
|
+
my?: MarginToken
|
|
73
|
+
mt?: MarginToken
|
|
74
|
+
mr?: MarginToken
|
|
75
|
+
mb?: MarginToken
|
|
76
|
+
ml?: MarginToken
|
|
77
|
+
style?: StyleProp<ViewStyle>
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ─── Font size mapping per checkbox size ────────────────────────────────────────
|
|
81
|
+
|
|
82
|
+
const LABEL_FONT_SIZE: Record<CheckboxSize, 1 | 2 | 3> = { 1: 1, 2: 2, 3: 3 }
|
|
83
|
+
const LABEL_GAP: Record<CheckboxSize, number> = { 1: 4, 2: 6, 3: 8 }
|
|
84
|
+
|
|
85
|
+
// ─── Root Component ─────────────────────────────────────────────────────────────
|
|
86
|
+
|
|
87
|
+
function CheckboxGroupRoot({
|
|
88
|
+
size = 2,
|
|
89
|
+
variant = 'surface',
|
|
90
|
+
color,
|
|
91
|
+
highContrast,
|
|
92
|
+
value: valueProp,
|
|
93
|
+
defaultValue = [],
|
|
94
|
+
onValueChange,
|
|
95
|
+
disabled = false,
|
|
96
|
+
children,
|
|
97
|
+
m, mx, my, mt, mr, mb, ml,
|
|
98
|
+
style,
|
|
99
|
+
}: CheckboxGroupProps) {
|
|
100
|
+
const { scaling } = useThemeContext()
|
|
101
|
+
|
|
102
|
+
const [internal, setInternal] = React.useState<string[]>(defaultValue)
|
|
103
|
+
const isControlled = valueProp !== undefined
|
|
104
|
+
const value = isControlled ? valueProp : internal
|
|
105
|
+
|
|
106
|
+
const onItemToggle = useCallback((itemValue: string) => {
|
|
107
|
+
const next = value.includes(itemValue)
|
|
108
|
+
? value.filter(v => v !== itemValue)
|
|
109
|
+
: [...value, itemValue]
|
|
110
|
+
if (!isControlled) setInternal(next)
|
|
111
|
+
onValueChange?.(next)
|
|
112
|
+
}, [value, isControlled, onValueChange])
|
|
113
|
+
|
|
114
|
+
const ctx = useMemo<CheckboxGroupContextValue>(() => ({
|
|
115
|
+
size, variant, color, highContrast, disabled, value, onItemToggle,
|
|
116
|
+
}), [size, variant, color, highContrast, disabled, value, onItemToggle])
|
|
117
|
+
|
|
118
|
+
const sp = (token: MarginToken | undefined): number | undefined =>
|
|
119
|
+
token !== undefined ? resolveSpace(token, scaling) : undefined
|
|
120
|
+
|
|
121
|
+
return (
|
|
122
|
+
<CheckboxGroupContext.Provider value={ctx}>
|
|
123
|
+
<View
|
|
124
|
+
accessibilityRole="none"
|
|
125
|
+
style={[
|
|
126
|
+
{ flexDirection: 'column', gap: resolveSpace(2, scaling) },
|
|
127
|
+
{
|
|
128
|
+
marginTop: sp(mt ?? my ?? m),
|
|
129
|
+
marginBottom: sp(mb ?? my ?? m),
|
|
130
|
+
marginLeft: sp(ml ?? mx ?? m),
|
|
131
|
+
marginRight: sp(mr ?? mx ?? m),
|
|
132
|
+
},
|
|
133
|
+
style,
|
|
134
|
+
]}
|
|
135
|
+
>
|
|
136
|
+
{children}
|
|
137
|
+
</View>
|
|
138
|
+
</CheckboxGroupContext.Provider>
|
|
139
|
+
)
|
|
140
|
+
}
|
|
141
|
+
CheckboxGroupRoot.displayName = 'CheckboxGroup.Root'
|
|
142
|
+
|
|
143
|
+
// ─── Item Component ─────────────────────────────────────────────────────────────
|
|
144
|
+
|
|
145
|
+
function CheckboxGroupItem({
|
|
146
|
+
value: itemValue,
|
|
147
|
+
disabled: disabledProp,
|
|
148
|
+
children,
|
|
149
|
+
m, mx, my, mt, mr, mb, ml,
|
|
150
|
+
style,
|
|
151
|
+
}: CheckboxGroupItemProps) {
|
|
152
|
+
const ctx = useContext(CheckboxGroupContext)
|
|
153
|
+
if (!ctx) throw new Error('CheckboxGroup.Item must be used within CheckboxGroup.Root')
|
|
154
|
+
|
|
155
|
+
const { size, variant, color, highContrast, disabled: groupDisabled, value, onItemToggle } = ctx
|
|
156
|
+
const { scaling, fonts } = useThemeContext()
|
|
157
|
+
const rc = useResolveColor()
|
|
158
|
+
|
|
159
|
+
const isDisabled = disabledProp ?? groupDisabled
|
|
160
|
+
const isChecked = value.includes(itemValue)
|
|
161
|
+
|
|
162
|
+
const handlePress = useCallback(() => {
|
|
163
|
+
if (!isDisabled) onItemToggle(itemValue)
|
|
164
|
+
}, [isDisabled, onItemToggle, itemValue])
|
|
165
|
+
|
|
166
|
+
const sp = (token: MarginToken | undefined): number | undefined =>
|
|
167
|
+
token !== undefined ? resolveSpace(token, scaling) : undefined
|
|
168
|
+
|
|
169
|
+
const scalingFactor = scalingMap[scaling]
|
|
170
|
+
const fontIdx = LABEL_FONT_SIZE[size]
|
|
171
|
+
const resolvedFontSize = Math.round(fontSize[fontIdx] * scalingFactor)
|
|
172
|
+
const resolvedLineHeight = Math.round(lineHeight[fontIdx] * scalingFactor)
|
|
173
|
+
const resolvedLetterSpacing = letterSpacingEm[fontIdx] * resolvedFontSize
|
|
174
|
+
const gap = Math.round(LABEL_GAP[size] * scalingFactor)
|
|
175
|
+
|
|
176
|
+
type C = Parameters<typeof rc>[0]
|
|
177
|
+
const textColor = rc('gray-12' as C)
|
|
178
|
+
const fontFamily = fonts.regular
|
|
179
|
+
|
|
180
|
+
const hasLabel = children != null
|
|
181
|
+
|
|
182
|
+
if (!hasLabel) {
|
|
183
|
+
return (
|
|
184
|
+
<Checkbox
|
|
185
|
+
size={size}
|
|
186
|
+
variant={variant}
|
|
187
|
+
color={color}
|
|
188
|
+
highContrast={highContrast}
|
|
189
|
+
checked={isChecked}
|
|
190
|
+
onCheckedChange={() => onItemToggle(itemValue)}
|
|
191
|
+
disabled={isDisabled}
|
|
192
|
+
m={m} mx={mx} my={my} mt={mt} mr={mr} mb={mb} ml={ml}
|
|
193
|
+
style={style}
|
|
194
|
+
/>
|
|
195
|
+
)
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const labelStyle: TextStyle = {
|
|
199
|
+
fontSize: resolvedFontSize,
|
|
200
|
+
lineHeight: resolvedLineHeight,
|
|
201
|
+
letterSpacing: resolvedLetterSpacing,
|
|
202
|
+
color: isDisabled ? rc('gray-a8' as C) : textColor,
|
|
203
|
+
fontFamily,
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return (
|
|
207
|
+
<Pressable
|
|
208
|
+
onPress={handlePress}
|
|
209
|
+
disabled={isDisabled}
|
|
210
|
+
accessibilityRole="checkbox"
|
|
211
|
+
accessibilityState={{ checked: isChecked, disabled: isDisabled }}
|
|
212
|
+
style={[
|
|
213
|
+
{
|
|
214
|
+
flexDirection: 'row',
|
|
215
|
+
alignItems: 'center',
|
|
216
|
+
gap,
|
|
217
|
+
marginTop: sp(mt ?? my ?? m),
|
|
218
|
+
marginBottom: sp(mb ?? my ?? m),
|
|
219
|
+
marginLeft: sp(ml ?? mx ?? m),
|
|
220
|
+
marginRight: sp(mr ?? mx ?? m),
|
|
221
|
+
},
|
|
222
|
+
style,
|
|
223
|
+
]}
|
|
224
|
+
>
|
|
225
|
+
<Checkbox
|
|
226
|
+
size={size}
|
|
227
|
+
variant={variant}
|
|
228
|
+
color={color}
|
|
229
|
+
highContrast={highContrast}
|
|
230
|
+
checked={isChecked}
|
|
231
|
+
onCheckedChange={() => onItemToggle(itemValue)}
|
|
232
|
+
disabled={isDisabled}
|
|
233
|
+
/>
|
|
234
|
+
{typeof children === 'string' || typeof children === 'number' ? (
|
|
235
|
+
<RNText style={labelStyle}>{children}</RNText>
|
|
236
|
+
) : (
|
|
237
|
+
children
|
|
238
|
+
)}
|
|
239
|
+
</Pressable>
|
|
240
|
+
)
|
|
241
|
+
}
|
|
242
|
+
CheckboxGroupItem.displayName = 'CheckboxGroup.Item'
|
|
243
|
+
|
|
244
|
+
// ─── Compound export ────────────────────────────────────────────────────────────
|
|
245
|
+
|
|
246
|
+
export const CheckboxGroup = Object.assign(CheckboxGroupRoot, {
|
|
247
|
+
Root: CheckboxGroupRoot,
|
|
248
|
+
Item: CheckboxGroupItem,
|
|
249
|
+
})
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
export { Button, type ButtonProps, type ButtonSize, type ButtonVariant } from './Button'
|
|
2
|
+
export { Checkbox, type CheckboxProps, type CheckboxSize, type CheckboxVariant, type CheckedState } from './Checkbox'
|
|
3
|
+
export { CheckboxGroup, type CheckboxGroupProps, type CheckboxGroupItemProps } from './CheckboxGroup'
|
|
4
|
+
export { CheckboxCards, type CheckboxCardsProps, type CheckboxCardsItemProps, type CheckboxCardsVariant } from './CheckboxCards'
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import React from 'react'
|
|
2
|
+
import { View } from 'react-native'
|
|
3
|
+
import type { ViewProps, ViewStyle } from 'react-native'
|
|
4
|
+
import { useThemeContext } from '../../hooks/useThemeContext'
|
|
5
|
+
import { useResolveColor } from '../../hooks/useResolveColor'
|
|
6
|
+
import { resolveSpace } from '../../utils/resolveSpace'
|
|
7
|
+
import { getRadius, getFullRadius } from '../../tokens/radius'
|
|
8
|
+
import type { SpaceToken, MarginToken } from '../../tokens/spacing'
|
|
9
|
+
import type { ThemeColor, RadiusToken } from '../../theme/theme.types'
|
|
10
|
+
|
|
11
|
+
export interface BoxProps extends ViewProps {
|
|
12
|
+
// ─── Padding ──────────────────────────────────────────────────────
|
|
13
|
+
p?: SpaceToken
|
|
14
|
+
px?: SpaceToken
|
|
15
|
+
py?: SpaceToken
|
|
16
|
+
pt?: SpaceToken
|
|
17
|
+
pr?: SpaceToken
|
|
18
|
+
pb?: SpaceToken
|
|
19
|
+
pl?: SpaceToken
|
|
20
|
+
// ─── Margin ───────────────────────────────────────────────────────
|
|
21
|
+
m?: MarginToken
|
|
22
|
+
mx?: MarginToken
|
|
23
|
+
my?: MarginToken
|
|
24
|
+
mt?: MarginToken
|
|
25
|
+
mr?: MarginToken
|
|
26
|
+
mb?: MarginToken
|
|
27
|
+
ml?: MarginToken
|
|
28
|
+
// ─── Size ─────────────────────────────────────────────────────────
|
|
29
|
+
width?: number | string
|
|
30
|
+
minWidth?: number | string
|
|
31
|
+
maxWidth?: number | string
|
|
32
|
+
height?: number | string
|
|
33
|
+
minHeight?: number | string
|
|
34
|
+
maxHeight?: number | string
|
|
35
|
+
// ─── Position ─────────────────────────────────────────────────────
|
|
36
|
+
position?: 'relative' | 'absolute'
|
|
37
|
+
top?: number | string
|
|
38
|
+
right?: number | string
|
|
39
|
+
bottom?: number | string
|
|
40
|
+
left?: number | string
|
|
41
|
+
// ─── Layout ───────────────────────────────────────────────────────
|
|
42
|
+
overflow?: 'hidden' | 'visible' | 'scroll'
|
|
43
|
+
flexBasis?: number | string
|
|
44
|
+
flexShrink?: number
|
|
45
|
+
flexGrow?: number
|
|
46
|
+
// ─── RN-only theme props ───────────────────────────────────────────
|
|
47
|
+
/** Background color from the theme token system */
|
|
48
|
+
bg?: ThemeColor
|
|
49
|
+
/** Border radius using the theme radius token (level 4 — card-sized) */
|
|
50
|
+
radius?: RadiusToken
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Fundamental layout primitive backed by a View. */
|
|
54
|
+
export function Box({
|
|
55
|
+
p, px, py, pt, pr, pb, pl,
|
|
56
|
+
m, mx, my, mt, mr, mb, ml,
|
|
57
|
+
width, minWidth, maxWidth,
|
|
58
|
+
height, minHeight, maxHeight,
|
|
59
|
+
position,
|
|
60
|
+
top, right, bottom, left,
|
|
61
|
+
overflow,
|
|
62
|
+
flexBasis, flexShrink, flexGrow,
|
|
63
|
+
bg,
|
|
64
|
+
radius,
|
|
65
|
+
style,
|
|
66
|
+
...rest
|
|
67
|
+
}: BoxProps) {
|
|
68
|
+
const { scaling } = useThemeContext()
|
|
69
|
+
const rc = useResolveColor()
|
|
70
|
+
|
|
71
|
+
const sp = (token: MarginToken | SpaceToken | undefined): number | undefined =>
|
|
72
|
+
token !== undefined ? resolveSpace(token, scaling) : undefined
|
|
73
|
+
|
|
74
|
+
const boxStyle: ViewStyle = {
|
|
75
|
+
// Padding — specific > axis > all
|
|
76
|
+
paddingTop: sp(pt ?? py ?? p),
|
|
77
|
+
paddingBottom: sp(pb ?? py ?? p),
|
|
78
|
+
paddingLeft: sp(pl ?? px ?? p),
|
|
79
|
+
paddingRight: sp(pr ?? px ?? p),
|
|
80
|
+
// Margin — specific > axis > all
|
|
81
|
+
marginTop: sp(mt ?? my ?? m),
|
|
82
|
+
marginBottom: sp(mb ?? my ?? m),
|
|
83
|
+
marginLeft: sp(ml ?? mx ?? m),
|
|
84
|
+
marginRight: sp(mr ?? mx ?? m),
|
|
85
|
+
// Size
|
|
86
|
+
width: width as ViewStyle['width'],
|
|
87
|
+
minWidth: minWidth as ViewStyle['minWidth'],
|
|
88
|
+
maxWidth: maxWidth as ViewStyle['maxWidth'],
|
|
89
|
+
height: height as ViewStyle['height'],
|
|
90
|
+
minHeight: minHeight as ViewStyle['minHeight'],
|
|
91
|
+
maxHeight: maxHeight as ViewStyle['maxHeight'],
|
|
92
|
+
// Position
|
|
93
|
+
position, top: top as ViewStyle['top'], right: right as ViewStyle['right'],
|
|
94
|
+
bottom: bottom as ViewStyle['bottom'], left: left as ViewStyle['left'],
|
|
95
|
+
// Layout
|
|
96
|
+
overflow: overflow as ViewStyle['overflow'],
|
|
97
|
+
flexBasis: flexBasis as ViewStyle['flexBasis'],
|
|
98
|
+
flexShrink, flexGrow,
|
|
99
|
+
// Theme
|
|
100
|
+
backgroundColor: bg ? rc(bg) : undefined,
|
|
101
|
+
borderRadius: radius
|
|
102
|
+
? (radius === 'full' ? getFullRadius(radius) : getRadius(radius, 4))
|
|
103
|
+
: undefined,
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return <View style={[boxStyle, style]} {...rest} />
|
|
107
|
+
}
|
|
108
|
+
Box.displayName = 'Box'
|