@nexub/react-native 0.0.0-stage → 2.2.0
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/README.md +113 -2
- package/lib/module/components/actions.js +222 -0
- package/lib/module/components/actions.js.map +1 -0
- package/lib/module/components/containers.js +300 -0
- package/lib/module/components/containers.js.map +1 -0
- package/lib/module/components/display.js +299 -0
- package/lib/module/components/display.js.map +1 -0
- package/lib/module/components/feedback.js +484 -0
- package/lib/module/components/feedback.js.map +1 -0
- package/lib/module/components/inputs.js +792 -0
- package/lib/module/components/inputs.js.map +1 -0
- package/lib/module/components/mobile.js +818 -0
- package/lib/module/components/mobile.js.map +1 -0
- package/lib/module/components/overlays.js +421 -0
- package/lib/module/components/overlays.js.map +1 -0
- package/lib/module/components/shared.js +57 -0
- package/lib/module/components/shared.js.map +1 -0
- package/lib/module/hooks/useControllableState.js +23 -0
- package/lib/module/hooks/useControllableState.js.map +1 -0
- package/lib/module/index.js +31 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/module/patterns/app.js +709 -0
- package/lib/module/patterns/app.js.map +1 -0
- package/lib/module/patterns/auth.js +224 -0
- package/lib/module/patterns/auth.js.map +1 -0
- package/lib/module/patterns/home.js +201 -0
- package/lib/module/patterns/home.js.map +1 -0
- package/lib/module/patterns/index.js +8 -0
- package/lib/module/patterns/index.js.map +1 -0
- package/lib/module/patterns/money.js +419 -0
- package/lib/module/patterns/money.js.map +1 -0
- package/lib/module/primitives/Icon.js +17 -0
- package/lib/module/primitives/Icon.js.map +1 -0
- package/lib/module/primitives/Layout.js +79 -0
- package/lib/module/primitives/Layout.js.map +1 -0
- package/lib/module/primitives/Pressable.js +79 -0
- package/lib/module/primitives/Pressable.js.map +1 -0
- package/lib/module/primitives/ScreenContainer.js +58 -0
- package/lib/module/primitives/ScreenContainer.js.map +1 -0
- package/lib/module/primitives/Surface.js +57 -0
- package/lib/module/primitives/Surface.js.map +1 -0
- package/lib/module/primitives/Text.js +70 -0
- package/lib/module/primitives/Text.js.map +1 -0
- package/lib/module/theme/ThemeProvider.js +127 -0
- package/lib/module/theme/ThemeProvider.js.map +1 -0
- package/lib/module/theme/fonts.js +56 -0
- package/lib/module/theme/fonts.js.map +1 -0
- package/lib/module/theme/haptics.js +56 -0
- package/lib/module/theme/haptics.js.map +1 -0
- package/lib/module/theme/motion.js +89 -0
- package/lib/module/theme/motion.js.map +1 -0
- package/lib/module/theme/navigation.js +99 -0
- package/lib/module/theme/navigation.js.map +1 -0
- package/lib/typescript/components/actions.d.ts +53 -0
- package/lib/typescript/components/actions.d.ts.map +1 -0
- package/lib/typescript/components/containers.d.ts +59 -0
- package/lib/typescript/components/containers.d.ts.map +1 -0
- package/lib/typescript/components/display.d.ts +68 -0
- package/lib/typescript/components/display.d.ts.map +1 -0
- package/lib/typescript/components/feedback.d.ts +96 -0
- package/lib/typescript/components/feedback.d.ts.map +1 -0
- package/lib/typescript/components/inputs.d.ts +141 -0
- package/lib/typescript/components/inputs.d.ts.map +1 -0
- package/lib/typescript/components/mobile.d.ts +173 -0
- package/lib/typescript/components/mobile.d.ts.map +1 -0
- package/lib/typescript/components/overlays.d.ts +74 -0
- package/lib/typescript/components/overlays.d.ts.map +1 -0
- package/lib/typescript/components/shared.d.ts +20 -0
- package/lib/typescript/components/shared.d.ts.map +1 -0
- package/lib/typescript/hooks/useControllableState.d.ts +6 -0
- package/lib/typescript/hooks/useControllableState.d.ts.map +1 -0
- package/lib/typescript/index.d.ts +39 -0
- package/lib/typescript/index.d.ts.map +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/patterns/app.d.ts +139 -0
- package/lib/typescript/patterns/app.d.ts.map +1 -0
- package/lib/typescript/patterns/auth.d.ts +54 -0
- package/lib/typescript/patterns/auth.d.ts.map +1 -0
- package/lib/typescript/patterns/home.d.ts +50 -0
- package/lib/typescript/patterns/home.d.ts.map +1 -0
- package/lib/typescript/patterns/index.d.ts +9 -0
- package/lib/typescript/patterns/index.d.ts.map +1 -0
- package/lib/typescript/patterns/money.d.ts +106 -0
- package/lib/typescript/patterns/money.d.ts.map +1 -0
- package/lib/typescript/primitives/Icon.d.ts +12 -0
- package/lib/typescript/primitives/Icon.d.ts.map +1 -0
- package/lib/typescript/primitives/Layout.d.ts +26 -0
- package/lib/typescript/primitives/Layout.d.ts.map +1 -0
- package/lib/typescript/primitives/Pressable.d.ts +21 -0
- package/lib/typescript/primitives/Pressable.d.ts.map +1 -0
- package/lib/typescript/primitives/ScreenContainer.d.ts +25 -0
- package/lib/typescript/primitives/ScreenContainer.d.ts.map +1 -0
- package/lib/typescript/primitives/Surface.d.ts +23 -0
- package/lib/typescript/primitives/Surface.d.ts.map +1 -0
- package/lib/typescript/primitives/Text.d.ts +21 -0
- package/lib/typescript/primitives/Text.d.ts.map +1 -0
- package/lib/typescript/theme/ThemeProvider.d.ts +69 -0
- package/lib/typescript/theme/ThemeProvider.d.ts.map +1 -0
- package/lib/typescript/theme/fonts.d.ts +25 -0
- package/lib/typescript/theme/fonts.d.ts.map +1 -0
- package/lib/typescript/theme/haptics.d.ts +3 -0
- package/lib/typescript/theme/haptics.d.ts.map +1 -0
- package/lib/typescript/theme/motion.d.ts +79 -0
- package/lib/typescript/theme/motion.d.ts.map +1 -0
- package/lib/typescript/theme/navigation.d.ts +158 -0
- package/lib/typescript/theme/navigation.d.ts.map +1 -0
- package/package.json +138 -3
- package/src/components/actions.tsx +259 -0
- package/src/components/containers.tsx +352 -0
- package/src/components/display.tsx +345 -0
- package/src/components/feedback.tsx +541 -0
- package/src/components/inputs.tsx +899 -0
- package/src/components/mobile.tsx +883 -0
- package/src/components/overlays.tsx +488 -0
- package/src/components/shared.ts +54 -0
- package/src/hooks/useControllableState.ts +27 -0
- package/src/index.ts +177 -0
- package/src/patterns/app.tsx +715 -0
- package/src/patterns/auth.tsx +266 -0
- package/src/patterns/home.tsx +211 -0
- package/src/patterns/index.ts +59 -0
- package/src/patterns/money.tsx +458 -0
- package/src/primitives/Icon.tsx +15 -0
- package/src/primitives/Layout.tsx +71 -0
- package/src/primitives/Pressable.tsx +97 -0
- package/src/primitives/ScreenContainer.tsx +65 -0
- package/src/primitives/Surface.tsx +75 -0
- package/src/primitives/Text.tsx +79 -0
- package/src/theme/ThemeProvider.tsx +210 -0
- package/src/theme/fonts.ts +52 -0
- package/src/theme/haptics.ts +57 -0
- package/src/theme/motion.ts +105 -0
- package/src/theme/navigation.ts +68 -0
package/package.json
CHANGED
|
@@ -1,6 +1,141 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@nexub/react-native",
|
|
3
|
-
"version": "
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "2.2.0",
|
|
4
|
+
"description": "React Native components for Nexub Design System — native chrome, Nexub content. Same tokens, brands and prop names as @nexub/react, for Expo and bare React Native apps.",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"homepage": "https://github.com/Nexub-AI/nexub-ds#readme",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/Nexub-AI/nexub-ds.git",
|
|
10
|
+
"directory": "packages/react-native"
|
|
11
|
+
},
|
|
12
|
+
"keywords": [
|
|
13
|
+
"react-native",
|
|
14
|
+
"expo",
|
|
15
|
+
"components",
|
|
16
|
+
"design-system",
|
|
17
|
+
"nexub"
|
|
18
|
+
],
|
|
19
|
+
"publishConfig": {
|
|
20
|
+
"access": "public"
|
|
21
|
+
},
|
|
22
|
+
"files": [
|
|
23
|
+
"src",
|
|
24
|
+
"lib",
|
|
25
|
+
"!src/__tests__"
|
|
26
|
+
],
|
|
27
|
+
"sideEffects": false,
|
|
28
|
+
"main": "./lib/module/index.js",
|
|
29
|
+
"types": "./lib/typescript/index.d.ts",
|
|
30
|
+
"exports": {
|
|
31
|
+
".": {
|
|
32
|
+
"source": "./src/index.ts",
|
|
33
|
+
"types": "./lib/typescript/index.d.ts",
|
|
34
|
+
"default": "./lib/module/index.js"
|
|
35
|
+
},
|
|
36
|
+
"./patterns": {
|
|
37
|
+
"source": "./src/patterns/index.ts",
|
|
38
|
+
"types": "./lib/typescript/patterns/index.d.ts",
|
|
39
|
+
"default": "./lib/module/patterns/index.js"
|
|
40
|
+
},
|
|
41
|
+
"./package.json": "./package.json"
|
|
42
|
+
},
|
|
43
|
+
"react-native-builder-bob": {
|
|
44
|
+
"source": "src",
|
|
45
|
+
"output": "lib",
|
|
46
|
+
"targets": [
|
|
47
|
+
[
|
|
48
|
+
"module",
|
|
49
|
+
{
|
|
50
|
+
"esm": true
|
|
51
|
+
}
|
|
52
|
+
],
|
|
53
|
+
[
|
|
54
|
+
"typescript",
|
|
55
|
+
{
|
|
56
|
+
"project": "tsconfig.build.json"
|
|
57
|
+
}
|
|
58
|
+
]
|
|
59
|
+
]
|
|
60
|
+
},
|
|
61
|
+
"dependencies": {
|
|
62
|
+
"@expo-google-fonts/geist-mono": "^0.4.3",
|
|
63
|
+
"@expo-google-fonts/inter": "^0.4.2",
|
|
64
|
+
"@expo-google-fonts/plus-jakarta-sans": "^0.4.2",
|
|
65
|
+
"lucide-react-native": "^1.64.0",
|
|
66
|
+
"@nexub/core": "2.2.0",
|
|
67
|
+
"@nexub/icons": "2.2.0",
|
|
68
|
+
"@nexub/tokens": "2.2.0"
|
|
69
|
+
},
|
|
70
|
+
"peerDependencies": {
|
|
71
|
+
"expo-glass-effect": ">=57.0.0",
|
|
72
|
+
"expo-haptics": ">=57.0.0",
|
|
73
|
+
"react": ">=19.1.0",
|
|
74
|
+
"react-native": ">=0.86.0",
|
|
75
|
+
"react-native-reanimated": ">=4.5.0",
|
|
76
|
+
"react-native-safe-area-context": ">=5.6.0",
|
|
77
|
+
"react-native-svg": ">=15.0.0",
|
|
78
|
+
"@react-native-community/datetimepicker": ">=8.4.0",
|
|
79
|
+
"expo-screen-capture": ">=57.0.0"
|
|
80
|
+
},
|
|
81
|
+
"peerDependenciesMeta": {
|
|
82
|
+
"expo-glass-effect": {
|
|
83
|
+
"optional": true
|
|
84
|
+
},
|
|
85
|
+
"expo-haptics": {
|
|
86
|
+
"optional": true
|
|
87
|
+
},
|
|
88
|
+
"@react-native-community/datetimepicker": {
|
|
89
|
+
"optional": true
|
|
90
|
+
},
|
|
91
|
+
"expo-screen-capture": {
|
|
92
|
+
"optional": true
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
"devDependencies": {
|
|
96
|
+
"@testing-library/react-native": "^14.1.0",
|
|
97
|
+
"@types/jest": "^30.0.0",
|
|
98
|
+
"@types/react": "~19.3.0",
|
|
99
|
+
"expo": "~58.0.21",
|
|
100
|
+
"expo-glass-effect": "~58.0.5",
|
|
101
|
+
"expo-haptics": "~58.0.7",
|
|
102
|
+
"jest": "^30.2.0",
|
|
103
|
+
"jest-expo": "~58.0.13",
|
|
104
|
+
"react": "19.3.0",
|
|
105
|
+
"react-native": "0.88.1",
|
|
106
|
+
"react-native-builder-bob": "^0.44.1",
|
|
107
|
+
"react-native-reanimated": "4.7.1",
|
|
108
|
+
"react-native-safe-area-context": "~5.10.1",
|
|
109
|
+
"react-native-svg": "15.16.0",
|
|
110
|
+
"react-native-worklets": "0.13.0",
|
|
111
|
+
"typescript": "^5.9.3",
|
|
112
|
+
"@react-native-community/datetimepicker": "9.2.1",
|
|
113
|
+
"expo-screen-capture": "~58.0.6"
|
|
114
|
+
},
|
|
115
|
+
"jest": {
|
|
116
|
+
"preset": "jest-expo",
|
|
117
|
+
"roots": [
|
|
118
|
+
"<rootDir>/src"
|
|
119
|
+
],
|
|
120
|
+
"setupFiles": [
|
|
121
|
+
"<rootDir>/jest.setup.js"
|
|
122
|
+
],
|
|
123
|
+
"transformIgnorePatterns": [
|
|
124
|
+
"node_modules/(?!(\\.pnpm|((jest-)?react-native|@react-native(-community)?)|expo(nent)?|@expo(nent)?/.*|@expo-google-fonts/.*|react-navigation|@react-navigation/.*|react-native-svg|lucide-react-native|@nexub/.*|@material/.*))"
|
|
125
|
+
],
|
|
126
|
+
"transform": {
|
|
127
|
+
"^.+\\.mjs$": "babel-jest"
|
|
128
|
+
},
|
|
129
|
+
"moduleNameMapper": {
|
|
130
|
+
"^react$": "<rootDir>/node_modules/react",
|
|
131
|
+
"^react/(.*)$": "<rootDir>/node_modules/react/$1"
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
"scripts": {
|
|
135
|
+
"build": "bob build",
|
|
136
|
+
"clean": "rm -rf lib",
|
|
137
|
+
"typecheck": "tsc --noEmit",
|
|
138
|
+
"lint": "eslint src",
|
|
139
|
+
"test": "jest"
|
|
140
|
+
}
|
|
6
141
|
}
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { ActivityIndicator, View, type StyleProp, type ViewStyle } from 'react-native';
|
|
3
|
+
import type { ControlSize, ToneValue } from '@nexub/core';
|
|
4
|
+
import type { NativeIconProps } from '@nexub/icons/native';
|
|
5
|
+
import { Pressable, type PressableProps } from '../primitives/Pressable';
|
|
6
|
+
import { Surface } from '../primitives/Surface';
|
|
7
|
+
import { Text } from '../primitives/Text';
|
|
8
|
+
import { useTheme, type RoleName } from '../theme/ThemeProvider';
|
|
9
|
+
import type { HapticToken } from '../theme/haptics';
|
|
10
|
+
import { TONE_ROLES, controlHeight, disabledOpacity, hitSlopFor } from './shared';
|
|
11
|
+
|
|
12
|
+
export type ButtonVariant = 'filled' | 'tonal' | 'outlined' | 'text' | 'glass';
|
|
13
|
+
|
|
14
|
+
// [container role | undefined, content role]
|
|
15
|
+
const VARIANT: Record<ButtonVariant, [RoleName | undefined, RoleName]> = {
|
|
16
|
+
filled: ['primary', 'on-primary'],
|
|
17
|
+
tonal: ['secondary-container', 'on-secondary-container'],
|
|
18
|
+
outlined: [undefined, 'on-surface'],
|
|
19
|
+
text: [undefined, 'primary'],
|
|
20
|
+
glass: [undefined, 'on-surface'],
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const LABEL: Record<ControlSize, 'label-md' | 'label-lg' | 'title-md'> = {
|
|
24
|
+
xs: 'label-md',
|
|
25
|
+
sm: 'label-md',
|
|
26
|
+
md: 'label-lg',
|
|
27
|
+
lg: 'label-lg',
|
|
28
|
+
xl: 'title-md',
|
|
29
|
+
};
|
|
30
|
+
const PAD: Record<ControlSize, number> = { xs: 10, sm: 14, md: 20, lg: 24, xl: 28 };
|
|
31
|
+
const ICON: Record<ControlSize, NativeIconProps['size']> = {
|
|
32
|
+
xs: 'xs',
|
|
33
|
+
sm: 'sm',
|
|
34
|
+
md: 'sm',
|
|
35
|
+
lg: 'md',
|
|
36
|
+
xl: 'md',
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
export interface ButtonProps extends Omit<PressableProps, 'children'> {
|
|
40
|
+
children: React.ReactNode;
|
|
41
|
+
/** @default 'filled' */
|
|
42
|
+
variant?: ButtonVariant;
|
|
43
|
+
/** Solid semantic or product colour (overrides the variant colour). */
|
|
44
|
+
tone?: ToneValue;
|
|
45
|
+
/** The control ladder. Primary actions on mobile use 'lg' (48). @default 'md' */
|
|
46
|
+
size?: ControlSize;
|
|
47
|
+
/** Shows a spinner, blocks presses and announces `loadingLabel`. */
|
|
48
|
+
loading?: boolean;
|
|
49
|
+
loadingLabel?: string;
|
|
50
|
+
fullWidth?: boolean;
|
|
51
|
+
leadingIcon?: React.ComponentType<NativeIconProps>;
|
|
52
|
+
trailingIcon?: React.ComponentType<NativeIconProps>;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Capsule button. One `filled` (or toned) action per screen; the rest tonal/outlined/text. */
|
|
56
|
+
export function Button({
|
|
57
|
+
children,
|
|
58
|
+
variant = 'filled',
|
|
59
|
+
tone,
|
|
60
|
+
size = 'md',
|
|
61
|
+
loading,
|
|
62
|
+
loadingLabel,
|
|
63
|
+
fullWidth,
|
|
64
|
+
leadingIcon: Leading,
|
|
65
|
+
trailingIcon: Trailing,
|
|
66
|
+
disabled,
|
|
67
|
+
style,
|
|
68
|
+
accessibilityLabel,
|
|
69
|
+
...rest
|
|
70
|
+
}: ButtonProps) {
|
|
71
|
+
const theme = useTheme();
|
|
72
|
+
const { roles } = theme;
|
|
73
|
+
const [bgRole, fgRole] = tone ? TONE_ROLES[tone] : VARIANT[variant];
|
|
74
|
+
const height = controlHeight(theme, size);
|
|
75
|
+
const fg = roles[fgRole];
|
|
76
|
+
const inactive = disabled || loading;
|
|
77
|
+
const label = typeof children === 'string' ? children : undefined;
|
|
78
|
+
|
|
79
|
+
const body = (
|
|
80
|
+
<View
|
|
81
|
+
style={{
|
|
82
|
+
flexDirection: 'row',
|
|
83
|
+
alignItems: 'center',
|
|
84
|
+
justifyContent: 'center',
|
|
85
|
+
gap: 8,
|
|
86
|
+
opacity: disabled ? disabledOpacity(theme) : 1,
|
|
87
|
+
}}
|
|
88
|
+
>
|
|
89
|
+
{loading ? (
|
|
90
|
+
<ActivityIndicator size="small" color={fg} />
|
|
91
|
+
) : (
|
|
92
|
+
Leading && <Leading size={ICON[size]} color={fg} />
|
|
93
|
+
)}
|
|
94
|
+
{typeof children === 'string' ? (
|
|
95
|
+
<Text variant={LABEL[size]} style={{ color: fg }} numberOfLines={1}>
|
|
96
|
+
{children}
|
|
97
|
+
</Text>
|
|
98
|
+
) : (
|
|
99
|
+
children
|
|
100
|
+
)}
|
|
101
|
+
{!loading && Trailing && <Trailing size={ICON[size]} color={fg} />}
|
|
102
|
+
</View>
|
|
103
|
+
);
|
|
104
|
+
|
|
105
|
+
const shape: StyleProp<ViewStyle> = [
|
|
106
|
+
{
|
|
107
|
+
height,
|
|
108
|
+
paddingHorizontal: PAD[size],
|
|
109
|
+
borderRadius: theme.tokens.shape.control,
|
|
110
|
+
justifyContent: 'center',
|
|
111
|
+
alignSelf: fullWidth ? 'stretch' : 'flex-start',
|
|
112
|
+
backgroundColor: bgRole ? roles[bgRole] : 'transparent',
|
|
113
|
+
},
|
|
114
|
+
variant === 'outlined' && !tone && { borderWidth: 1, borderColor: roles['outline-variant'] },
|
|
115
|
+
disabled && bgRole && { backgroundColor: roles['on-surface'] + '1f' }, // disabled-container 12%
|
|
116
|
+
style,
|
|
117
|
+
];
|
|
118
|
+
|
|
119
|
+
return (
|
|
120
|
+
<Pressable
|
|
121
|
+
accessibilityLabel={accessibilityLabel ?? (loading && loadingLabel ? loadingLabel : label)}
|
|
122
|
+
accessibilityState={{ disabled: !!inactive, busy: !!loading }}
|
|
123
|
+
disabled={inactive}
|
|
124
|
+
hitSlop={hitSlopFor(height)}
|
|
125
|
+
stateLayer={fgRole}
|
|
126
|
+
style={
|
|
127
|
+
variant === 'glass' && !tone ? { alignSelf: fullWidth ? 'stretch' : 'flex-start' } : shape
|
|
128
|
+
}
|
|
129
|
+
{...rest}
|
|
130
|
+
>
|
|
131
|
+
{variant === 'glass' && !tone ? (
|
|
132
|
+
<Surface
|
|
133
|
+
material="glass"
|
|
134
|
+
radius="control"
|
|
135
|
+
style={[{ height, paddingHorizontal: PAD[size], justifyContent: 'center' }, style]}
|
|
136
|
+
>
|
|
137
|
+
{body}
|
|
138
|
+
</Surface>
|
|
139
|
+
) : (
|
|
140
|
+
body
|
|
141
|
+
)}
|
|
142
|
+
</Pressable>
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
export interface IconButtonProps extends Omit<PressableProps, 'children'> {
|
|
147
|
+
icon: React.ComponentType<NativeIconProps>;
|
|
148
|
+
/** Required: the accessible name (what it does, not "button"). */
|
|
149
|
+
label: string;
|
|
150
|
+
/** @default 'text' */
|
|
151
|
+
variant?: ButtonVariant;
|
|
152
|
+
tone?: ToneValue;
|
|
153
|
+
/** @default 'md' */
|
|
154
|
+
size?: ControlSize;
|
|
155
|
+
/** Toggle state for icon toggles (favourite, mute). */
|
|
156
|
+
selected?: boolean;
|
|
157
|
+
haptic?: HapticToken;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Circular icon-only button; always 44pt+ to touch. */
|
|
161
|
+
export function IconButton({
|
|
162
|
+
icon: Glyph,
|
|
163
|
+
label,
|
|
164
|
+
variant = 'text',
|
|
165
|
+
tone,
|
|
166
|
+
size = 'md',
|
|
167
|
+
selected,
|
|
168
|
+
disabled,
|
|
169
|
+
style,
|
|
170
|
+
...rest
|
|
171
|
+
}: IconButtonProps) {
|
|
172
|
+
const theme = useTheme();
|
|
173
|
+
const [bgRole, fgRole] = tone ? TONE_ROLES[tone] : VARIANT[variant];
|
|
174
|
+
const fgSelected: RoleName = selected
|
|
175
|
+
? 'primary'
|
|
176
|
+
: variant === 'text' && !tone
|
|
177
|
+
? 'on-surface-variant'
|
|
178
|
+
: fgRole;
|
|
179
|
+
const height = controlHeight(theme, size);
|
|
180
|
+
return (
|
|
181
|
+
<Pressable
|
|
182
|
+
accessibilityLabel={label}
|
|
183
|
+
accessibilityState={{ disabled: !!disabled, selected }}
|
|
184
|
+
disabled={disabled}
|
|
185
|
+
hitSlop={hitSlopFor(height)}
|
|
186
|
+
stateLayer={fgRole}
|
|
187
|
+
style={[
|
|
188
|
+
{
|
|
189
|
+
width: height,
|
|
190
|
+
height,
|
|
191
|
+
borderRadius: height / 2,
|
|
192
|
+
alignItems: 'center',
|
|
193
|
+
justifyContent: 'center',
|
|
194
|
+
backgroundColor: bgRole ? theme.roles[bgRole] : 'transparent',
|
|
195
|
+
opacity: disabled ? disabledOpacity(theme) : 1,
|
|
196
|
+
},
|
|
197
|
+
variant === 'outlined' && { borderWidth: 1, borderColor: theme.roles['outline-variant'] },
|
|
198
|
+
style,
|
|
199
|
+
]}
|
|
200
|
+
{...rest}
|
|
201
|
+
>
|
|
202
|
+
<Glyph
|
|
203
|
+
size={ICON[size] === 'xs' ? 'sm' : size === 'xl' ? 'lg' : 'md'}
|
|
204
|
+
color={theme.roles[fgSelected]}
|
|
205
|
+
/>
|
|
206
|
+
</Pressable>
|
|
207
|
+
);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
export interface ButtonGroupProps {
|
|
211
|
+
children: React.ReactNode;
|
|
212
|
+
/** 'stack' puts actions full-width in a column (bottom of forms). @default 'row' */
|
|
213
|
+
direction?: 'row' | 'stack';
|
|
214
|
+
style?: StyleProp<ViewStyle>;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/** Related actions together. In a stack, put the primary action first. */
|
|
218
|
+
export function ButtonGroup({ children, direction = 'row', style }: ButtonGroupProps) {
|
|
219
|
+
const { tokens } = useTheme();
|
|
220
|
+
return (
|
|
221
|
+
<View
|
|
222
|
+
style={[
|
|
223
|
+
direction === 'row'
|
|
224
|
+
? { flexDirection: 'row', flexWrap: 'wrap', gap: tokens.spacing[2] }
|
|
225
|
+
: { gap: tokens.spacing[3], alignItems: 'stretch' },
|
|
226
|
+
style,
|
|
227
|
+
]}
|
|
228
|
+
>
|
|
229
|
+
{direction === 'stack'
|
|
230
|
+
? React.Children.map(children, (c) =>
|
|
231
|
+
React.isValidElement<ButtonProps>(c) ? React.cloneElement(c, { fullWidth: true }) : c,
|
|
232
|
+
)
|
|
233
|
+
: children}
|
|
234
|
+
</View>
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
export interface LinkProps extends Omit<PressableProps, 'children'> {
|
|
239
|
+
children: string;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/** Inline text action. Opens nothing by itself: pass `onPress` (e.g. router.push / Linking.openURL). */
|
|
243
|
+
export function Link({ children, style, ...rest }: LinkProps) {
|
|
244
|
+
return (
|
|
245
|
+
<Pressable
|
|
246
|
+
accessibilityRole="link"
|
|
247
|
+
accessibilityLabel={children}
|
|
248
|
+
stateLayer={false}
|
|
249
|
+
pressScale={false}
|
|
250
|
+
hitSlop={8}
|
|
251
|
+
style={style}
|
|
252
|
+
{...rest}
|
|
253
|
+
>
|
|
254
|
+
<Text variant="label-lg" color="primary" style={{ textDecorationLine: 'underline' }}>
|
|
255
|
+
{children}
|
|
256
|
+
</Text>
|
|
257
|
+
</Pressable>
|
|
258
|
+
);
|
|
259
|
+
}
|