@vvedantb/native-ui 0.2.1 → 0.3.12
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 +160 -9
- package/package.json +80 -2
- package/src/AlertDialog.tsx +73 -0
- package/src/AmbientBackground.tsx +26 -11
- package/src/Avatar.tsx +141 -0
- package/src/Badge.tsx +108 -0
- package/src/BottomSheetContainer.tsx +7 -1
- package/src/Button.tsx +153 -52
- package/src/Card.tsx +111 -36
- package/src/Checkbox.tsx +78 -0
- package/src/Chip.tsx +5 -3
- package/src/ConfirmDialog.tsx +105 -0
- package/src/DateTimeField.tsx +6 -2
- package/src/Dialog.tsx +253 -0
- package/src/EmptyState.tsx +122 -7
- package/src/FloatingFAB.tsx +2 -4
- package/src/FloatingSurface.tsx +31 -9
- package/src/FloatingTabBar.tsx +39 -11
- package/src/Input.tsx +15 -4
- package/src/Label.tsx +31 -0
- package/src/LabeledSwitchRow.tsx +96 -0
- package/src/LiquidGlass.tsx +2 -10
- package/src/ListRow.tsx +7 -2
- package/src/PillTabs.tsx +28 -14
- package/src/Progress.tsx +67 -0
- package/src/SearchInput.tsx +163 -0
- package/src/Select.tsx +32 -6
- package/src/Separator.tsx +38 -0
- package/src/Sheet.tsx +107 -0
- package/src/Skeleton.tsx +25 -5
- package/src/Spinner.tsx +92 -0
- package/src/StatusDot.tsx +66 -0
- package/src/Switch.tsx +24 -7
- package/src/Tabs.tsx +190 -0
- package/src/Textarea.tsx +47 -0
- package/src/Toast.tsx +170 -0
- package/src/blur.ts +48 -0
- package/src/brand/AppIcons.tsx +106 -0
- package/src/brand/AppMark.tsx +83 -0
- package/src/brand/AppWordmark.tsx +97 -0
- package/src/brand/familyApps.ts +24 -0
- package/src/brand/index.ts +34 -0
- package/src/brand/svg.ts +263 -0
- package/src/brand/vProAppBenefits.ts +22 -0
- package/src/controlStyles.ts +360 -0
- package/src/dialogLayout.ts +20 -0
- package/src/entries/ambient.ts +3 -0
- package/src/entries/bottom-sheet.ts +2 -0
- package/src/entries/brand.ts +2 -0
- package/src/entries/button.ts +3 -0
- package/src/entries/date-time.ts +3 -0
- package/src/entries/floating.ts +24 -0
- package/src/entries/layout.ts +7 -0
- package/src/entries/marketing.ts +10 -0
- package/src/entries/primitives.ts +99 -0
- package/src/entries/search-input.ts +4 -0
- package/src/entries/tab-bar.ts +10 -0
- package/src/entries/theme.ts +15 -0
- package/src/entries/toast.ts +6 -0
- package/src/examples/ParityGallery.tsx +496 -0
- package/src/floating.ts +5 -1
- package/src/glassBlur.tsx +95 -0
- package/src/gradient.ts +21 -0
- package/src/haptics.ts +22 -2
- package/src/index.ts +99 -2
- package/src/initials.ts +10 -0
- package/src/marketing/PricingModal.tsx +410 -0
- package/src/marketing/UpgradeToProCard.tsx +103 -0
- package/src/marketing/pricing.ts +76 -0
- package/src/palette.ts +43 -0
- package/src/primitives/input.tsx +9 -2
- package/src/primitives/native-only-animated-view.tsx +10 -3
- package/src/primitives/overlay.tsx +179 -0
- package/src/primitives/select.tsx +25 -12
- package/src/primitives/switch.tsx +12 -13
- package/src/primitives/use-controllable-state.ts +24 -0
- package/src/settings/SettingsField.tsx +11 -2
- package/src/settings/SettingsSection.tsx +20 -6
- package/src/settings/SettingsToggleRow.tsx +7 -2
- package/src/tabBarRoutes.ts +52 -0
- package/src/toastChrome.ts +103 -0
- package/src/toastStore.ts +86 -0
- package/src/tokens.ts +19 -14
- package/src/typography.ts +24 -0
- package/src/useReduceMotion.ts +24 -0
- package/src/useScheme.ts +6 -0
package/src/Badge.tsx
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import { Text, View, type ViewProps } from "react-native";
|
|
3
|
+
import { cn } from "./cn";
|
|
4
|
+
import { typeStyle } from "./controlStyles";
|
|
5
|
+
import { palette, withAlpha } from "./palette";
|
|
6
|
+
import { tokens } from "./tokens";
|
|
7
|
+
import { useLargerText } from "./theme";
|
|
8
|
+
import { useScheme } from "./useScheme";
|
|
9
|
+
|
|
10
|
+
/** The web `Badge` variants. */
|
|
11
|
+
export type BadgeVariant =
|
|
12
|
+
| "default"
|
|
13
|
+
| "secondary"
|
|
14
|
+
| "destructive"
|
|
15
|
+
| "destructive-soft"
|
|
16
|
+
| "outline"
|
|
17
|
+
| "success"
|
|
18
|
+
| "warning";
|
|
19
|
+
|
|
20
|
+
export type BadgeProps = ViewProps & {
|
|
21
|
+
variant?: BadgeVariant;
|
|
22
|
+
className?: string;
|
|
23
|
+
/** Extra classes for the label when `children` is a string or number. */
|
|
24
|
+
textClassName?: string;
|
|
25
|
+
children?: ReactNode;
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
type Scheme = "light" | "dark";
|
|
29
|
+
|
|
30
|
+
/** Border, fill and text colours per variant, from the web classes (`border-danger/25 bg-danger/12 text-danger` and so on). */
|
|
31
|
+
export function badgeColors(variant: BadgeVariant, scheme: Scheme) {
|
|
32
|
+
const tone = (hex: string, border: number, fill: number) => ({
|
|
33
|
+
borderColor: withAlpha(hex, border),
|
|
34
|
+
backgroundColor: withAlpha(hex, fill),
|
|
35
|
+
color: hex,
|
|
36
|
+
});
|
|
37
|
+
switch (variant) {
|
|
38
|
+
case "secondary":
|
|
39
|
+
return {
|
|
40
|
+
borderColor: tokens.color.separator[scheme],
|
|
41
|
+
backgroundColor: palette.default[scheme],
|
|
42
|
+
color: palette.defaultForeground[scheme],
|
|
43
|
+
};
|
|
44
|
+
case "destructive":
|
|
45
|
+
return tone(palette.danger[scheme], 0.25, 0.12);
|
|
46
|
+
case "destructive-soft":
|
|
47
|
+
return {
|
|
48
|
+
borderColor: tokens.color.border[scheme],
|
|
49
|
+
backgroundColor: withAlpha(palette.danger[scheme], 0.1),
|
|
50
|
+
color: palette.danger[scheme],
|
|
51
|
+
};
|
|
52
|
+
case "outline":
|
|
53
|
+
return {
|
|
54
|
+
borderColor: tokens.color.separator[scheme],
|
|
55
|
+
backgroundColor: "transparent",
|
|
56
|
+
color: tokens.color.muted[scheme],
|
|
57
|
+
};
|
|
58
|
+
case "success":
|
|
59
|
+
return tone(palette.success[scheme], 0.25, 0.12);
|
|
60
|
+
case "warning":
|
|
61
|
+
return tone(palette.warning[scheme], 0.25, 0.12);
|
|
62
|
+
default:
|
|
63
|
+
return {
|
|
64
|
+
borderColor: tokens.color.border[scheme],
|
|
65
|
+
backgroundColor: palette.default[scheme],
|
|
66
|
+
color: tokens.color.foreground[scheme],
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Small rounded status label, with the web `Badge` variant names and colours. */
|
|
72
|
+
export function Badge({
|
|
73
|
+
variant = "default",
|
|
74
|
+
className,
|
|
75
|
+
textClassName,
|
|
76
|
+
children,
|
|
77
|
+
style,
|
|
78
|
+
...props
|
|
79
|
+
}: BadgeProps) {
|
|
80
|
+
const scheme = useScheme();
|
|
81
|
+
const largerText = useLargerText();
|
|
82
|
+
const { color, ...frame } = badgeColors(variant, scheme);
|
|
83
|
+
return (
|
|
84
|
+
<View
|
|
85
|
+
className={cn(
|
|
86
|
+
"flex-row items-center gap-1 self-start rounded-full border px-2.5 py-1",
|
|
87
|
+
className,
|
|
88
|
+
)}
|
|
89
|
+
style={[frame, style]}
|
|
90
|
+
{...props}
|
|
91
|
+
>
|
|
92
|
+
{typeof children === "string" || typeof children === "number" ? (
|
|
93
|
+
<Text
|
|
94
|
+
className={cn(
|
|
95
|
+
"font-instrument-sans-medium",
|
|
96
|
+
largerText ? "text-base" : "text-sm",
|
|
97
|
+
textClassName,
|
|
98
|
+
)}
|
|
99
|
+
style={[{ color }, typeStyle("medium")]}
|
|
100
|
+
>
|
|
101
|
+
{children}
|
|
102
|
+
</Text>
|
|
103
|
+
) : (
|
|
104
|
+
children
|
|
105
|
+
)}
|
|
106
|
+
</View>
|
|
107
|
+
);
|
|
108
|
+
}
|
|
@@ -7,6 +7,7 @@ import { View, Text, TouchableOpacity } from "react-native";
|
|
|
7
7
|
import { useRef, useEffect, useCallback, JSX } from "react";
|
|
8
8
|
import { useColorScheme } from "nativewind";
|
|
9
9
|
import { Colors } from "./colors";
|
|
10
|
+
import { textStyle, typeStyle } from "./controlStyles";
|
|
10
11
|
import { IconX } from "@tabler/icons-react-native";
|
|
11
12
|
import { BottomSheetDefaultBackdropProps } from "@gorhom/bottom-sheet/lib/typescript/components/bottomSheetBackdrop/types";
|
|
12
13
|
import { GlassSheetBackground } from "./LiquidGlass";
|
|
@@ -66,7 +67,12 @@ export function BottomSheetContainer({
|
|
|
66
67
|
<BottomSheetView className="p-5 pb-12">
|
|
67
68
|
<View className="mb-4 flex-row items-center justify-between gap-3">
|
|
68
69
|
<Text
|
|
69
|
-
|
|
70
|
+
accessibilityRole="header"
|
|
71
|
+
className={`${largerText ? "text-2xl" : "text-xl"} flex-1 font-instrument-sans-semibold`}
|
|
72
|
+
style={[
|
|
73
|
+
textStyle("foreground", colorScheme === "dark" ? "dark" : "light"),
|
|
74
|
+
typeStyle("semibold"),
|
|
75
|
+
]}
|
|
70
76
|
>
|
|
71
77
|
{title}
|
|
72
78
|
</Text>
|
package/src/Button.tsx
CHANGED
|
@@ -1,110 +1,211 @@
|
|
|
1
1
|
import { MaterialIcons } from "@expo/vector-icons";
|
|
2
2
|
import React from "react";
|
|
3
3
|
import { PINK_600 } from "./colors";
|
|
4
|
-
import {
|
|
4
|
+
import { View } from "react-native";
|
|
5
5
|
import { Button as PrimitiveButton } from "./primitives/button";
|
|
6
6
|
import { Text as PrimitiveText } from "./primitives/text";
|
|
7
7
|
import { cn } from "./cn";
|
|
8
|
+
import { buttonColors, sizeStyle, typeStyle, type ButtonVariant } from "./controlStyles";
|
|
9
|
+
import { useDialogLayout } from "./dialogLayout";
|
|
8
10
|
import { floatingShadow } from "./floating";
|
|
11
|
+
import { Spinner } from "./Spinner";
|
|
9
12
|
import { useLargerText } from "./theme";
|
|
13
|
+
import { tokens } from "./tokens";
|
|
14
|
+
import { useScheme } from "./useScheme";
|
|
15
|
+
|
|
16
|
+
/** `sm`, `default` and `lg` are the web names for `small`, `medium` and `large`. `icon` is a square button. */
|
|
17
|
+
type ButtonSize = "small" | "medium" | "large" | "sm" | "default" | "lg" | "icon";
|
|
10
18
|
|
|
11
19
|
interface ButtonProps extends Omit<
|
|
12
20
|
React.ComponentPropsWithoutRef<typeof PrimitiveButton>,
|
|
13
21
|
"variant" | "size" | "children" | "onPress"
|
|
14
22
|
> {
|
|
15
23
|
onPress?: () => void;
|
|
16
|
-
variant?:
|
|
24
|
+
variant?: ButtonVariant;
|
|
17
25
|
icon?: keyof typeof MaterialIcons.glyphMap;
|
|
18
26
|
leftIcon?: React.ReactNode;
|
|
27
|
+
/** Web name for `leftIcon`. `leftIcon` wins when both are set. */
|
|
28
|
+
startContent?: React.ReactNode;
|
|
29
|
+
/** Rendered after the label. */
|
|
30
|
+
endContent?: React.ReactNode;
|
|
19
31
|
loading?: boolean;
|
|
20
|
-
size?:
|
|
32
|
+
size?: ButtonSize;
|
|
33
|
+
/**
|
|
34
|
+
* Default `true`. In the stacked footer of a compact dialog the button fills
|
|
35
|
+
* the width whatever this says, as on the web.
|
|
36
|
+
*/
|
|
21
37
|
fullWidth?: boolean;
|
|
22
|
-
|
|
38
|
+
/** Label, or other content. Give a button with no text child an `accessibilityLabel`. */
|
|
39
|
+
children?: React.ReactNode;
|
|
40
|
+
/** `"shadow"` adds the floating shadow to `primary` and `default`. Default `"flat"`: the web button has none. */
|
|
23
41
|
shadowType?: "flat" | "shadow";
|
|
24
42
|
className?: string;
|
|
25
43
|
}
|
|
26
44
|
|
|
45
|
+
const SIZE_ALIAS = {
|
|
46
|
+
small: "small",
|
|
47
|
+
medium: "medium",
|
|
48
|
+
large: "large",
|
|
49
|
+
sm: "small",
|
|
50
|
+
default: "medium",
|
|
51
|
+
lg: "large",
|
|
52
|
+
icon: "icon",
|
|
53
|
+
} as const;
|
|
54
|
+
|
|
55
|
+
// `regular` is the web `Button` (`sm` h-8 px-3, `default` h-10 px-4, `lg`
|
|
56
|
+
// h-11 px-6, `icon` h-10 w-10). `larger` is the larger-text step-up from
|
|
57
|
+
// 0.3.2. Labelled sizes are minimum heights (`h-auto` drops the primitive's
|
|
58
|
+
// `h-10`), so a large Dynamic Type size grows the button instead of clipping
|
|
59
|
+
// the label.
|
|
27
60
|
const HEIGHT = {
|
|
28
|
-
regular: {
|
|
29
|
-
|
|
61
|
+
regular: {
|
|
62
|
+
small: "h-auto min-h-8 px-3",
|
|
63
|
+
medium: "h-auto min-h-10 px-4",
|
|
64
|
+
large: "h-auto min-h-11 px-6",
|
|
65
|
+
icon: "h-10 w-10 px-0",
|
|
66
|
+
},
|
|
67
|
+
larger: {
|
|
68
|
+
small: "h-auto min-h-14 px-5",
|
|
69
|
+
medium: "h-auto min-h-16 px-6",
|
|
70
|
+
large: "h-auto min-h-[72px] px-6",
|
|
71
|
+
icon: "h-14 w-14 px-0",
|
|
72
|
+
},
|
|
30
73
|
};
|
|
31
74
|
|
|
75
|
+
// Each class is also set as a style (`sizeStyle`): web `text-sm` is 14 / 20.
|
|
32
76
|
const TEXT = {
|
|
33
|
-
regular: { small: "text-
|
|
34
|
-
larger: { small: "text-base", medium: "text-xl", large: "text-2xl" },
|
|
77
|
+
regular: { small: "text-xs", medium: "text-sm", large: "text-sm", icon: "text-sm" },
|
|
78
|
+
larger: { small: "text-base", medium: "text-xl", large: "text-2xl", icon: "text-base" },
|
|
79
|
+
} as const;
|
|
80
|
+
|
|
81
|
+
// The web sizes are under the 44 pt touch target; the slop brings each to 48.
|
|
82
|
+
const HIT_SLOP = {
|
|
83
|
+
regular: { small: 8, medium: 4, large: 2, icon: 4 },
|
|
84
|
+
larger: { small: 8, medium: undefined, large: undefined, icon: 8 },
|
|
35
85
|
};
|
|
36
86
|
|
|
37
|
-
|
|
87
|
+
/** Web `rounded-lg` (`--radius`, 16). A style, because apps' NativeWind themes set their own `rounded-lg`. */
|
|
88
|
+
const RADIUS = tokens.radius.lg;
|
|
89
|
+
|
|
90
|
+
// Colours come from `buttonColors` as styles, except `tinted`, which keeps
|
|
91
|
+
// the pink classes that `outline` used before 0.3.0.
|
|
92
|
+
const VARIANT_CLASS: Partial<Record<ButtonVariant, string>> = {
|
|
93
|
+
link: "h-auto border-0 px-0",
|
|
94
|
+
tinted: "border-0 bg-pink-50 dark:bg-pink-900/20",
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
const TEXT_CLASS: Partial<Record<ButtonVariant, string>> = {
|
|
98
|
+
link: "underline",
|
|
99
|
+
tinted: "text-pink-700 dark:text-pink-300",
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const PRIMITIVE_VARIANT = {
|
|
103
|
+
primary: "default",
|
|
104
|
+
default: "default",
|
|
105
|
+
outline: "outline",
|
|
106
|
+
secondary: "secondary",
|
|
107
|
+
destructive: "destructive",
|
|
108
|
+
ghost: "ghost",
|
|
109
|
+
link: "link",
|
|
110
|
+
tinted: "secondary",
|
|
111
|
+
} as const;
|
|
112
|
+
|
|
113
|
+
export type { ButtonProps, ButtonSize, ButtonVariant };
|
|
38
114
|
|
|
39
115
|
export function Button({
|
|
40
116
|
onPress,
|
|
41
117
|
variant = "primary",
|
|
42
118
|
icon,
|
|
43
119
|
leftIcon,
|
|
120
|
+
startContent,
|
|
121
|
+
endContent,
|
|
44
122
|
loading,
|
|
45
123
|
size = "medium",
|
|
46
124
|
fullWidth = true,
|
|
47
125
|
children,
|
|
48
|
-
shadowType = "
|
|
126
|
+
shadowType = "flat",
|
|
49
127
|
className,
|
|
50
128
|
...props
|
|
51
129
|
}: ButtonProps) {
|
|
52
130
|
const scale = useLargerText() ? "larger" : "regular";
|
|
53
|
-
const
|
|
54
|
-
const
|
|
55
|
-
const
|
|
56
|
-
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
131
|
+
const scheme = useScheme();
|
|
132
|
+
const dialog = useDialogLayout();
|
|
133
|
+
const resolvedSize = SIZE_ALIAS[size];
|
|
134
|
+
const colors = buttonColors(variant, scheme);
|
|
135
|
+
const contentColor = colors?.color ?? PINK_600;
|
|
136
|
+
const elevate =
|
|
137
|
+
shadowType === "shadow" && (variant === "primary" || variant === "default");
|
|
138
|
+
const baseStyle = [
|
|
139
|
+
{ borderRadius: RADIUS },
|
|
140
|
+
elevate ? floatingShadow : null,
|
|
141
|
+
colors
|
|
142
|
+
? { backgroundColor: colors.backgroundColor, borderColor: colors.borderColor }
|
|
143
|
+
: null,
|
|
144
|
+
];
|
|
145
|
+
const callerStyle = props.style;
|
|
146
|
+
// As on the web, `loading` swaps the start content for the 16 ring (`h-4
|
|
147
|
+
// w-4`, Spinner `sm`) and keeps the label.
|
|
148
|
+
const start = loading ? (
|
|
149
|
+
<Spinner size="sm" color={contentColor} decorative />
|
|
150
|
+
) : (
|
|
151
|
+
leftIcon ??
|
|
152
|
+
startContent ??
|
|
153
|
+
(icon ? (
|
|
154
|
+
<MaterialIcons name={icon} size={scale === "larger" ? 26 : 16} color={contentColor} />
|
|
155
|
+
) : null)
|
|
156
|
+
);
|
|
63
157
|
|
|
64
158
|
return (
|
|
65
159
|
<PrimitiveButton
|
|
66
160
|
onPress={onPress}
|
|
67
|
-
variant={variant
|
|
68
|
-
hitSlop={
|
|
161
|
+
variant={PRIMITIVE_VARIANT[variant]}
|
|
162
|
+
hitSlop={HIT_SLOP[scale][resolvedSize]}
|
|
163
|
+
accessibilityRole="button"
|
|
69
164
|
className={cn(
|
|
70
|
-
"relative
|
|
71
|
-
HEIGHT[scale][
|
|
72
|
-
|
|
73
|
-
|
|
165
|
+
"relative border",
|
|
166
|
+
HEIGHT[scale][resolvedSize],
|
|
167
|
+
// Column flex children stretch by default (React Native Web too), so
|
|
168
|
+
// a button that is not full width must opt out with `self-start`.
|
|
169
|
+
resolvedSize !== "icon" && (fullWidth || dialog.stacked ? "w-full" : "self-start"),
|
|
170
|
+
// The web compact dialog: labelled buttons at least 44 high (`min-h-11`).
|
|
171
|
+
dialog.compact && scale === "regular" && resolvedSize !== "icon" && "min-h-11",
|
|
172
|
+
VARIANT_CLASS[variant],
|
|
74
173
|
className,
|
|
75
174
|
)}
|
|
76
175
|
{...props}
|
|
77
176
|
disabled={loading || props.disabled}
|
|
78
|
-
|
|
177
|
+
accessibilityState={{
|
|
178
|
+
...props.accessibilityState,
|
|
179
|
+
disabled: Boolean(loading || props.disabled),
|
|
180
|
+
busy: Boolean(loading),
|
|
181
|
+
}}
|
|
182
|
+
style={
|
|
183
|
+
typeof callerStyle === "function"
|
|
184
|
+
? (state) => [baseStyle, callerStyle(state)]
|
|
185
|
+
: [baseStyle, callerStyle]
|
|
186
|
+
}
|
|
79
187
|
>
|
|
80
188
|
<View className="flex-row items-center justify-center gap-2">
|
|
81
|
-
{
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
name={icon}
|
|
89
|
-
size={scale === "larger" ? 26 : 22}
|
|
90
|
-
color={contentColor}
|
|
91
|
-
/>
|
|
92
|
-
) : null)}
|
|
93
|
-
{typeof children === "string" || typeof children === "number" ? (
|
|
94
|
-
<PrimitiveText
|
|
95
|
-
className={cn(
|
|
96
|
-
TEXT[scale][size],
|
|
97
|
-
"font-instrument-sans-semibold",
|
|
98
|
-
isTinted ? "text-pink-700 dark:text-pink-300" : "text-white",
|
|
99
|
-
)}
|
|
100
|
-
>
|
|
101
|
-
{children}
|
|
102
|
-
</PrimitiveText>
|
|
103
|
-
) : (
|
|
104
|
-
children
|
|
189
|
+
{start}
|
|
190
|
+
{typeof children === "string" || typeof children === "number" ? (
|
|
191
|
+
<PrimitiveText
|
|
192
|
+
className={cn(
|
|
193
|
+
TEXT[scale][resolvedSize],
|
|
194
|
+
"font-instrument-sans-medium",
|
|
195
|
+
TEXT_CLASS[variant],
|
|
105
196
|
)}
|
|
106
|
-
|
|
197
|
+
style={[
|
|
198
|
+
colors ? { color: colors.color } : null,
|
|
199
|
+
sizeStyle(TEXT[scale][resolvedSize]),
|
|
200
|
+
typeStyle("medium"),
|
|
201
|
+
]}
|
|
202
|
+
>
|
|
203
|
+
{children}
|
|
204
|
+
</PrimitiveText>
|
|
205
|
+
) : (
|
|
206
|
+
children
|
|
107
207
|
)}
|
|
208
|
+
{endContent}
|
|
108
209
|
</View>
|
|
109
210
|
</PrimitiveButton>
|
|
110
211
|
);
|
package/src/Card.tsx
CHANGED
|
@@ -1,64 +1,139 @@
|
|
|
1
1
|
import type { ComponentType, ReactNode } from "react";
|
|
2
2
|
import type { IconProps } from "@tabler/icons-react-native";
|
|
3
|
-
import { View } from "react-native";
|
|
3
|
+
import { Text, View, type TextProps, type ViewProps } from "react-native";
|
|
4
4
|
import {
|
|
5
5
|
Card as PrimitiveCard,
|
|
6
|
-
CardContent,
|
|
7
|
-
CardHeader,
|
|
8
|
-
CardTitle,
|
|
6
|
+
CardContent as PrimitiveCardContent,
|
|
7
|
+
CardHeader as PrimitiveCardHeader,
|
|
8
|
+
CardTitle as PrimitiveCardTitle,
|
|
9
9
|
} from "./primitives/card";
|
|
10
|
-
import { FloatingSurface } from "./FloatingSurface";
|
|
11
10
|
import { cn } from "./cn";
|
|
11
|
+
import { cardStyle, sizeStyle, textStyle, typeStyle } from "./controlStyles";
|
|
12
12
|
import { useAccentColor, useLargerText } from "./theme";
|
|
13
|
+
import { useScheme } from "./useScheme";
|
|
13
14
|
|
|
14
15
|
export function Card({
|
|
15
16
|
children,
|
|
16
17
|
title,
|
|
18
|
+
description,
|
|
17
19
|
action,
|
|
18
20
|
leftIcon: Icon,
|
|
21
|
+
className,
|
|
19
22
|
testID,
|
|
20
23
|
}: {
|
|
21
24
|
children: ReactNode;
|
|
22
25
|
title?: string;
|
|
26
|
+
/** Muted line under the title (the web `CardDescription`). */
|
|
27
|
+
description?: string;
|
|
23
28
|
action?: ReactNode;
|
|
24
29
|
leftIcon?: ComponentType<IconProps>;
|
|
30
|
+
/** Classes on the card surface. */
|
|
31
|
+
className?: string;
|
|
25
32
|
testID?: string;
|
|
26
33
|
}) {
|
|
27
34
|
const largerText = useLargerText();
|
|
28
35
|
const accentColor = useAccentColor();
|
|
36
|
+
const scheme = useScheme();
|
|
29
37
|
const hasHeader = Boolean(title || action);
|
|
38
|
+
const titleSize = largerText ? "text-2xl" : "text-base";
|
|
39
|
+
// The web card: a flat `surfaceCard` fill at radius 16, with no border, no
|
|
40
|
+
// shadow and no blur. Dialogs, sheets and toasts float, as on the web.
|
|
30
41
|
return (
|
|
31
|
-
<
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
)
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
42
|
+
<PrimitiveCard
|
|
43
|
+
testID={testID}
|
|
44
|
+
className={cn("gap-4 overflow-hidden border-0 py-4", className)}
|
|
45
|
+
style={cardStyle(scheme)}
|
|
46
|
+
>
|
|
47
|
+
{hasHeader && (
|
|
48
|
+
<PrimitiveCardHeader className="px-4 py-0">
|
|
49
|
+
<View className="flex-row items-center justify-between">
|
|
50
|
+
<View className="flex-row items-center gap-2">
|
|
51
|
+
{Icon && (
|
|
52
|
+
<Icon size={largerText ? 28 : 24} color={accentColor} />
|
|
53
|
+
)}
|
|
54
|
+
{title ? (
|
|
55
|
+
<PrimitiveCardTitle
|
|
56
|
+
className={cn("font-instrument-sans-semibold leading-tight", titleSize)}
|
|
57
|
+
style={[
|
|
58
|
+
textStyle("foreground", scheme),
|
|
59
|
+
sizeStyle(titleSize, "tight"),
|
|
60
|
+
typeStyle("semibold"),
|
|
61
|
+
]}
|
|
62
|
+
>
|
|
63
|
+
{title}
|
|
64
|
+
</PrimitiveCardTitle>
|
|
65
|
+
) : null}
|
|
55
66
|
</View>
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
67
|
+
{action}
|
|
68
|
+
</View>
|
|
69
|
+
{description ? <CardDescription>{description}</CardDescription> : null}
|
|
70
|
+
</PrimitiveCardHeader>
|
|
71
|
+
)}
|
|
72
|
+
<PrimitiveCardContent className={cn(hasHeader && "pt-0", "px-4")}>
|
|
73
|
+
{children}
|
|
74
|
+
</PrimitiveCardContent>
|
|
75
|
+
</PrimitiveCard>
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Parts for composing a card body with the web part names. They sit inside
|
|
80
|
+
// `Card`'s padded content, so they add no side padding of their own. Text
|
|
81
|
+
// colours are styles that follow the colour scheme; recolour with `style`.
|
|
82
|
+
|
|
83
|
+
type PartProps = ViewProps & { className?: string };
|
|
84
|
+
type TextPartProps = TextProps & { className?: string };
|
|
85
|
+
|
|
86
|
+
export function CardHeader({ className, ...props }: PartProps) {
|
|
87
|
+
return <View className={cn("gap-1 pb-3", className)} {...props} />;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Web `font-semibold leading-none` at the inherited 16. The line height is
|
|
92
|
+
* `leading-tight` (20), not 16: below the face's ascent and descent, iOS and
|
|
93
|
+
* Android can clip the glyphs.
|
|
94
|
+
*/
|
|
95
|
+
export function CardTitle({ className, style, ...props }: TextPartProps) {
|
|
96
|
+
const largerText = useLargerText();
|
|
97
|
+
const scheme = useScheme();
|
|
98
|
+
const size = largerText ? "text-2xl" : "text-base";
|
|
99
|
+
return (
|
|
100
|
+
<Text
|
|
101
|
+
accessibilityRole="header"
|
|
102
|
+
className={cn("font-instrument-sans-semibold leading-tight", size, className)}
|
|
103
|
+
style={[
|
|
104
|
+
textStyle("foreground", scheme),
|
|
105
|
+
sizeStyle(size, "tight"),
|
|
106
|
+
typeStyle("semibold"),
|
|
107
|
+
style,
|
|
108
|
+
]}
|
|
109
|
+
{...props}
|
|
110
|
+
/>
|
|
111
|
+
);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Web `text-sm` (14 / 20) in the muted colour. */
|
|
115
|
+
export function CardDescription({ className, style, ...props }: TextPartProps) {
|
|
116
|
+
const largerText = useLargerText();
|
|
117
|
+
const scheme = useScheme();
|
|
118
|
+
const size = largerText ? "text-lg" : "text-sm";
|
|
119
|
+
return (
|
|
120
|
+
<Text
|
|
121
|
+
className={cn("font-instrument-sans", size, className)}
|
|
122
|
+
style={[textStyle("muted", scheme), sizeStyle(size), style]}
|
|
123
|
+
{...props}
|
|
124
|
+
/>
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
export function CardContent({ className, ...props }: PartProps) {
|
|
129
|
+
return <View className={cn("gap-3", className)} {...props} />;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function CardFooter({ className, ...props }: PartProps) {
|
|
133
|
+
return (
|
|
134
|
+
<View
|
|
135
|
+
className={cn("flex-row items-center justify-end gap-2 pt-4", className)}
|
|
136
|
+
{...props}
|
|
137
|
+
/>
|
|
63
138
|
);
|
|
64
139
|
}
|
package/src/Checkbox.tsx
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import { Pressable, type PressableProps } from "react-native";
|
|
3
|
+
import { IconCheck, IconMinus } from "@tabler/icons-react-native";
|
|
4
|
+
import { cn } from "./cn";
|
|
5
|
+
import { checkboxColors, touchSlop } from "./controlStyles";
|
|
6
|
+
import { selectionHaptic } from "./haptics";
|
|
7
|
+
import { useLargerText } from "./theme";
|
|
8
|
+
import { useScheme } from "./useScheme";
|
|
9
|
+
|
|
10
|
+
export type CheckboxProps = Omit<PressableProps, "onPress" | "children"> & {
|
|
11
|
+
/** `"indeterminate"` shows a dash; a press then checks it. */
|
|
12
|
+
checked: boolean | "indeterminate";
|
|
13
|
+
onCheckedChange?: (checked: boolean) => void;
|
|
14
|
+
/** Shown while checked. Default a check icon (a dash when indeterminate). */
|
|
15
|
+
icon?: ReactNode;
|
|
16
|
+
className?: string;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Controlled checkbox with a Pressable and a Tabler check, as the web
|
|
21
|
+
* `Checkbox` (`checked` / `onCheckedChange`). Colours from `checkboxColors`:
|
|
22
|
+
* the palette primary when checked, as on the web (not the theme accent).
|
|
23
|
+
* 16 square with a 14 check, as on the web (24 and 18 with larger text), plus
|
|
24
|
+
* a hit slop (`touchSlop`) that makes the target 44.
|
|
25
|
+
*/
|
|
26
|
+
export function Checkbox({
|
|
27
|
+
checked,
|
|
28
|
+
onCheckedChange,
|
|
29
|
+
icon,
|
|
30
|
+
disabled,
|
|
31
|
+
className,
|
|
32
|
+
style,
|
|
33
|
+
...props
|
|
34
|
+
}: CheckboxProps) {
|
|
35
|
+
const scheme = useScheme();
|
|
36
|
+
const largerText = useLargerText();
|
|
37
|
+
const on = checked !== false;
|
|
38
|
+
const { color, ...fill } = checkboxColors(on, scheme);
|
|
39
|
+
const boxStyle = { borderRadius: largerText ? 6 : 4, ...fill };
|
|
40
|
+
const box = largerText ? 24 : 16;
|
|
41
|
+
const iconSize = largerText ? 18 : 14;
|
|
42
|
+
return (
|
|
43
|
+
<Pressable
|
|
44
|
+
accessibilityRole="checkbox"
|
|
45
|
+
accessibilityState={{
|
|
46
|
+
checked: checked === "indeterminate" ? "mixed" : checked,
|
|
47
|
+
disabled: Boolean(disabled),
|
|
48
|
+
}}
|
|
49
|
+
hitSlop={touchSlop(box)}
|
|
50
|
+
disabled={disabled}
|
|
51
|
+
onPress={() => {
|
|
52
|
+
selectionHaptic();
|
|
53
|
+
onCheckedChange?.(checked !== true);
|
|
54
|
+
}}
|
|
55
|
+
className={cn(
|
|
56
|
+
"items-center justify-center border active:opacity-70",
|
|
57
|
+
largerText ? "h-6 w-6" : "h-4 w-4",
|
|
58
|
+
disabled && "opacity-50",
|
|
59
|
+
className,
|
|
60
|
+
)}
|
|
61
|
+
style={
|
|
62
|
+
typeof style === "function"
|
|
63
|
+
? (state) => [boxStyle, style(state)]
|
|
64
|
+
: [boxStyle, style]
|
|
65
|
+
}
|
|
66
|
+
{...props}
|
|
67
|
+
>
|
|
68
|
+
{on
|
|
69
|
+
? (icon ??
|
|
70
|
+
(checked === "indeterminate" ? (
|
|
71
|
+
<IconMinus size={iconSize} color={color} strokeWidth={2} />
|
|
72
|
+
) : (
|
|
73
|
+
<IconCheck size={iconSize} color={color} strokeWidth={2} />
|
|
74
|
+
)))
|
|
75
|
+
: null}
|
|
76
|
+
</Pressable>
|
|
77
|
+
);
|
|
78
|
+
}
|
package/src/Chip.tsx
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { Pressable, Text } from "react-native";
|
|
2
|
+
import { textStyle, typeStyle } from "./controlStyles";
|
|
2
3
|
import { useLargerText } from "./theme";
|
|
4
|
+
import { useScheme } from "./useScheme";
|
|
3
5
|
|
|
4
6
|
export function Chip({
|
|
5
7
|
label,
|
|
@@ -15,6 +17,7 @@ export function Chip({
|
|
|
15
17
|
round?: boolean;
|
|
16
18
|
}) {
|
|
17
19
|
const largerText = useLargerText();
|
|
20
|
+
const scheme = useScheme();
|
|
18
21
|
const size = round
|
|
19
22
|
? largerText
|
|
20
23
|
? "h-14 w-14"
|
|
@@ -37,10 +40,9 @@ export function Chip({
|
|
|
37
40
|
className={`font-instrument-sans-semibold ${
|
|
38
41
|
largerText ? "text-xl" : "text-lg"
|
|
39
42
|
} ${
|
|
40
|
-
selected
|
|
41
|
-
? "text-pink-700 dark:text-pink-300"
|
|
42
|
-
: "text-muted-foreground"
|
|
43
|
+
selected ? "text-pink-700 dark:text-pink-300" : ""
|
|
43
44
|
}`}
|
|
45
|
+
style={[selected ? null : textStyle("muted", scheme), typeStyle("semibold")]}
|
|
44
46
|
>
|
|
45
47
|
{label}
|
|
46
48
|
</Text>
|