@vvedantb/native-ui 0.2.0 → 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 +88 -8
- 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/FloatingTabBar.tsx
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import type { ComponentProps } from "react";
|
|
2
1
|
import { Pressable, Text, View } from "react-native";
|
|
3
2
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
|
4
|
-
import {
|
|
3
|
+
import { typeStyle } from "./controlStyles";
|
|
5
4
|
import { selectionHaptic } from "./haptics";
|
|
6
5
|
import { FloatingSurface } from "./FloatingSurface";
|
|
6
|
+
import { isTabBarRouteVisible, type FloatingTabOptions } from "./tabBarRoutes";
|
|
7
7
|
import { useLargerText } from "./theme";
|
|
8
8
|
import {
|
|
9
9
|
FLOATING_INSET,
|
|
@@ -12,15 +12,38 @@ import {
|
|
|
12
12
|
FLOATING_TAB_BAR_HEIGHT_LARGE,
|
|
13
13
|
} from "./floating";
|
|
14
14
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
15
|
+
/**
|
|
16
|
+
* The fields this bar reads from the props Expo Router's `Tabs` passes to
|
|
17
|
+
* `tabBar` (React Navigation's `BottomTabBarProps`, Expo SDK 57). Declared
|
|
18
|
+
* here so the tab bar does not import `expo-router`.
|
|
19
|
+
*/
|
|
20
|
+
export type FloatingTabBarProps = {
|
|
21
|
+
state: {
|
|
22
|
+
index: number;
|
|
23
|
+
routes: readonly { key: string; name: string; params?: object }[];
|
|
24
|
+
};
|
|
25
|
+
descriptors: Record<string, { options: FloatingTabOptions }>;
|
|
26
|
+
navigation: {
|
|
27
|
+
emit(event: {
|
|
28
|
+
type: "tabPress";
|
|
29
|
+
target: string;
|
|
30
|
+
canPreventDefault: true;
|
|
31
|
+
}): { defaultPrevented: boolean };
|
|
32
|
+
navigate(name: string, params?: object): void;
|
|
33
|
+
};
|
|
34
|
+
};
|
|
18
35
|
|
|
36
|
+
/**
|
|
37
|
+
* The floating pill tab bar, for the `tabBar` prop of Expo Router's `Tabs`.
|
|
38
|
+
* Hide a tab with `options={{ href: null }}` on its `Tabs.Screen`: the bar
|
|
39
|
+
* draws no tab for it (`isTabBarRouteVisible`), and the other tabs keep their
|
|
40
|
+
* focus and navigation.
|
|
41
|
+
*/
|
|
19
42
|
export function FloatingTabBar({
|
|
20
43
|
state,
|
|
21
44
|
descriptors,
|
|
22
45
|
navigation,
|
|
23
|
-
}:
|
|
46
|
+
}: FloatingTabBarProps) {
|
|
24
47
|
const insets = useSafeAreaInsets();
|
|
25
48
|
const largerText = useLargerText();
|
|
26
49
|
const height = largerText
|
|
@@ -30,10 +53,11 @@ export function FloatingTabBar({
|
|
|
30
53
|
return (
|
|
31
54
|
<View
|
|
32
55
|
pointerEvents="box-none"
|
|
56
|
+
// Inside the home indicator and, in landscape, the notch.
|
|
33
57
|
style={{
|
|
34
58
|
position: "absolute",
|
|
35
|
-
left: FLOATING_INSET,
|
|
36
|
-
right: FLOATING_INSET,
|
|
59
|
+
left: insets.left + FLOATING_INSET,
|
|
60
|
+
right: insets.right + FLOATING_INSET,
|
|
37
61
|
bottom: FLOATING_INSET + Math.max(insets.bottom, 8),
|
|
38
62
|
}}
|
|
39
63
|
>
|
|
@@ -44,11 +68,12 @@ export function FloatingTabBar({
|
|
|
44
68
|
accessibilityRole="tablist"
|
|
45
69
|
>
|
|
46
70
|
{state.routes.map((route, index) => {
|
|
47
|
-
const focused = state.index === index;
|
|
48
71
|
const options = descriptors[route.key]?.options;
|
|
49
|
-
if (!options) {
|
|
72
|
+
if (!isTabBarRouteVisible(options)) {
|
|
50
73
|
return null;
|
|
51
74
|
}
|
|
75
|
+
// `index` counts hidden routes too, as `state.index` does.
|
|
76
|
+
const focused = state.index === index;
|
|
52
77
|
const label =
|
|
53
78
|
typeof options.tabBarLabel === "string"
|
|
54
79
|
? options.tabBarLabel
|
|
@@ -90,10 +115,13 @@ export function FloatingTabBar({
|
|
|
90
115
|
: null}
|
|
91
116
|
<Text
|
|
92
117
|
numberOfLines={1}
|
|
118
|
+
// The bar has a fixed height (`tabBarReserve`), so labels
|
|
119
|
+
// scale with Dynamic Type only up to what fits in it.
|
|
120
|
+
maxFontSizeMultiplier={1.5}
|
|
93
121
|
className={`font-instrument-sans-semibold ${
|
|
94
122
|
largerText ? "text-base" : "text-sm"
|
|
95
123
|
}`}
|
|
96
|
-
style={{ color }}
|
|
124
|
+
style={[{ color }, typeStyle("semibold")]}
|
|
97
125
|
>
|
|
98
126
|
{label}
|
|
99
127
|
</Text>
|
package/src/Input.tsx
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
2
|
import { Input as PrimitiveInput } from "./primitives/input";
|
|
3
3
|
import { cn } from "./cn";
|
|
4
|
+
import { fieldStyle, fieldTextStyle, typeStyle } from "./controlStyles";
|
|
5
|
+
import { palette } from "./palette";
|
|
4
6
|
import { useLargerText } from "./theme";
|
|
7
|
+
import { useScheme } from "./useScheme";
|
|
5
8
|
|
|
6
9
|
type InputProps = React.ComponentPropsWithoutRef<typeof PrimitiveInput> & {
|
|
7
10
|
className?: string;
|
|
@@ -10,17 +13,25 @@ type InputProps = React.ComponentPropsWithoutRef<typeof PrimitiveInput> & {
|
|
|
10
13
|
const Input = React.forwardRef<
|
|
11
14
|
React.ElementRef<typeof PrimitiveInput>,
|
|
12
15
|
InputProps
|
|
13
|
-
>(({ className, placeholderTextColor, ...props }, ref) => {
|
|
16
|
+
>(({ className, placeholderTextColor, style, ...props }, ref) => {
|
|
14
17
|
const largerText = useLargerText();
|
|
18
|
+
const scheme = useScheme();
|
|
15
19
|
return (
|
|
16
20
|
<PrimitiveInput
|
|
17
21
|
ref={ref}
|
|
18
|
-
placeholderTextColor={placeholderTextColor ??
|
|
22
|
+
placeholderTextColor={placeholderTextColor ?? palette.fieldPlaceholder[scheme]}
|
|
19
23
|
className={cn(
|
|
20
|
-
|
|
21
|
-
|
|
24
|
+
// The web field: h-10 px-3.5 py-2, 16 px type (its phone size).
|
|
25
|
+
"min-h-10 border px-3.5 py-2 text-base font-instrument-sans",
|
|
26
|
+
largerText && "min-h-16 px-4 py-4 text-xl font-instrument-sans-medium",
|
|
22
27
|
className,
|
|
23
28
|
)}
|
|
29
|
+
style={[
|
|
30
|
+
fieldStyle(scheme),
|
|
31
|
+
fieldTextStyle(scheme),
|
|
32
|
+
largerText ? typeStyle("medium") : null,
|
|
33
|
+
style,
|
|
34
|
+
]}
|
|
24
35
|
{...props}
|
|
25
36
|
/>
|
|
26
37
|
);
|
package/src/Label.tsx
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { Text, type TextProps } from "react-native";
|
|
2
|
+
import { cn } from "./cn";
|
|
3
|
+
import { sizeStyle, textStyle, typeStyle } from "./controlStyles";
|
|
4
|
+
import { useLargerText } from "./theme";
|
|
5
|
+
import { useScheme } from "./useScheme";
|
|
6
|
+
|
|
7
|
+
export type LabelProps = TextProps & {
|
|
8
|
+
className?: string;
|
|
9
|
+
/** Dims the label (the web `peer-disabled` style). */
|
|
10
|
+
disabled?: boolean;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Field label, the web `Label` type: `text-sm` medium (14 / 500), `text-lg`
|
|
15
|
+
* with larger text. Link it to a field with
|
|
16
|
+
* `nativeID` here and `accessibilityLabelledBy` on the field (Android), or
|
|
17
|
+
* give the field its own `accessibilityLabel`. The colour is a style that
|
|
18
|
+
* follows the colour scheme; recolour with `style`.
|
|
19
|
+
*/
|
|
20
|
+
export function Label({ className, disabled, style, ...props }: LabelProps) {
|
|
21
|
+
const largerText = useLargerText();
|
|
22
|
+
const scheme = useScheme();
|
|
23
|
+
const size = largerText ? "text-lg" : "text-sm";
|
|
24
|
+
return (
|
|
25
|
+
<Text
|
|
26
|
+
className={cn("font-instrument-sans-medium", size, disabled && "opacity-70", className)}
|
|
27
|
+
style={[textStyle("foreground", scheme), sizeStyle(size), typeStyle("medium"), style]}
|
|
28
|
+
{...props}
|
|
29
|
+
/>
|
|
30
|
+
);
|
|
31
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import { Pressable, Text, View } from "react-native";
|
|
3
|
+
import { cn } from "./cn";
|
|
4
|
+
import { textStyle, typeStyle } from "./controlStyles";
|
|
5
|
+
import { Switch } from "./Switch";
|
|
6
|
+
import { useLargerText } from "./theme";
|
|
7
|
+
import { useScheme } from "./useScheme";
|
|
8
|
+
|
|
9
|
+
export type LabeledSwitchRowProps = {
|
|
10
|
+
label: string;
|
|
11
|
+
description?: string;
|
|
12
|
+
checked: boolean;
|
|
13
|
+
onCheckedChange?: (checked: boolean) => void;
|
|
14
|
+
disabled?: boolean;
|
|
15
|
+
icon?: ReactNode;
|
|
16
|
+
/** Replaces the switch. */
|
|
17
|
+
trailing?: ReactNode;
|
|
18
|
+
className?: string;
|
|
19
|
+
testID?: string;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Label, optional description and a `Switch`, as the web `LabeledSwitchRow`.
|
|
24
|
+
* The text toggles the switch too (the web `<label htmlFor>`). Use
|
|
25
|
+
* `SettingsToggleRow` inside a `SettingsSection` list instead.
|
|
26
|
+
*/
|
|
27
|
+
export function LabeledSwitchRow({
|
|
28
|
+
label,
|
|
29
|
+
description,
|
|
30
|
+
checked,
|
|
31
|
+
onCheckedChange,
|
|
32
|
+
disabled,
|
|
33
|
+
icon,
|
|
34
|
+
trailing,
|
|
35
|
+
className,
|
|
36
|
+
testID,
|
|
37
|
+
}: LabeledSwitchRowProps) {
|
|
38
|
+
const largerText = useLargerText();
|
|
39
|
+
const scheme = useScheme();
|
|
40
|
+
const toggle = () => onCheckedChange?.(!checked);
|
|
41
|
+
return (
|
|
42
|
+
<View className={cn("flex-row items-center justify-between gap-3", className)}>
|
|
43
|
+
<Pressable
|
|
44
|
+
testID={testID}
|
|
45
|
+
accessibilityRole={trailing ? undefined : "switch"}
|
|
46
|
+
accessibilityLabel={label}
|
|
47
|
+
accessibilityHint={description}
|
|
48
|
+
accessibilityState={
|
|
49
|
+
trailing ? undefined : { checked, disabled: Boolean(disabled) }
|
|
50
|
+
}
|
|
51
|
+
disabled={disabled || trailing != null}
|
|
52
|
+
onPress={toggle}
|
|
53
|
+
className="min-w-0 flex-1 flex-row items-start gap-2"
|
|
54
|
+
>
|
|
55
|
+
{icon ? <View className="mt-0.5 shrink-0">{icon}</View> : null}
|
|
56
|
+
<View className="min-w-0 flex-1">
|
|
57
|
+
<Text
|
|
58
|
+
className={cn(
|
|
59
|
+
"font-instrument-sans-semibold",
|
|
60
|
+
largerText ? "text-xl" : "text-lg",
|
|
61
|
+
disabled && "opacity-70",
|
|
62
|
+
)}
|
|
63
|
+
style={[textStyle("foreground", scheme), typeStyle("semibold")]}
|
|
64
|
+
>
|
|
65
|
+
{label}
|
|
66
|
+
</Text>
|
|
67
|
+
{description ? (
|
|
68
|
+
<Text
|
|
69
|
+
className={cn(
|
|
70
|
+
"mt-1 font-instrument-sans",
|
|
71
|
+
largerText ? "text-lg" : "text-base",
|
|
72
|
+
)}
|
|
73
|
+
style={textStyle("muted", scheme)}
|
|
74
|
+
>
|
|
75
|
+
{description}
|
|
76
|
+
</Text>
|
|
77
|
+
) : null}
|
|
78
|
+
</View>
|
|
79
|
+
</Pressable>
|
|
80
|
+
{trailing ?? (
|
|
81
|
+
// The row above is the accessible switch, so this one is hidden from assistive tech.
|
|
82
|
+
<View
|
|
83
|
+
accessibilityElementsHidden
|
|
84
|
+
importantForAccessibility="no-hide-descendants"
|
|
85
|
+
className="min-h-12 shrink-0 items-center justify-center"
|
|
86
|
+
>
|
|
87
|
+
<Switch
|
|
88
|
+
checked={checked}
|
|
89
|
+
onCheckedChange={onCheckedChange}
|
|
90
|
+
disabled={disabled}
|
|
91
|
+
/>
|
|
92
|
+
</View>
|
|
93
|
+
)}
|
|
94
|
+
</View>
|
|
95
|
+
);
|
|
96
|
+
}
|
package/src/LiquidGlass.tsx
CHANGED
|
@@ -1,14 +1,6 @@
|
|
|
1
|
-
import type { ReactNode } from "react";
|
|
2
|
-
|
|
3
1
|
export {
|
|
4
2
|
FloatingSurface as LiquidGlass,
|
|
5
3
|
GlassSheetBackground,
|
|
6
4
|
} from "./FloatingSurface";
|
|
7
|
-
|
|
8
|
-
export
|
|
9
|
-
return children;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export function GlassBlurTarget({ children }: { children: ReactNode }) {
|
|
13
|
-
return children;
|
|
14
|
-
}
|
|
5
|
+
export { GlassBlurProvider, GlassBlurTarget } from "./glassBlur";
|
|
6
|
+
export type { GlassBlurTargetProps } from "./glassBlur";
|
package/src/ListRow.tsx
CHANGED
|
@@ -3,8 +3,10 @@ import { Pressable, Text, View } from "react-native";
|
|
|
3
3
|
import type { IconProps } from "@tabler/icons-react-native";
|
|
4
4
|
import { NEUTRAL_500 } from "./colors";
|
|
5
5
|
import { cn } from "./cn";
|
|
6
|
+
import { textStyle, typeStyle } from "./controlStyles";
|
|
6
7
|
import { FLOATING_ROW_RADIUS } from "./floating";
|
|
7
8
|
import { useLargerText } from "./theme";
|
|
9
|
+
import { useScheme } from "./useScheme";
|
|
8
10
|
|
|
9
11
|
export function ListRow({
|
|
10
12
|
title,
|
|
@@ -28,6 +30,7 @@ export function ListRow({
|
|
|
28
30
|
testID?: string;
|
|
29
31
|
}) {
|
|
30
32
|
const largerText = useLargerText();
|
|
33
|
+
const scheme = useScheme();
|
|
31
34
|
const content = (
|
|
32
35
|
<>
|
|
33
36
|
{Icon ? <Icon size={largerText ? 30 : 26} color={iconColor} /> : null}
|
|
@@ -35,18 +38,20 @@ export function ListRow({
|
|
|
35
38
|
<Text
|
|
36
39
|
numberOfLines={2}
|
|
37
40
|
className={cn(
|
|
38
|
-
"font-instrument-sans-semibold
|
|
41
|
+
"font-instrument-sans-semibold",
|
|
39
42
|
largerText ? "text-xl" : "text-lg",
|
|
40
43
|
)}
|
|
44
|
+
style={[textStyle("foreground", scheme), typeStyle("semibold")]}
|
|
41
45
|
>
|
|
42
46
|
{title}
|
|
43
47
|
</Text>
|
|
44
48
|
{meta ? (
|
|
45
49
|
<Text
|
|
46
50
|
className={cn(
|
|
47
|
-
"font-instrument-sans
|
|
51
|
+
"font-instrument-sans",
|
|
48
52
|
largerText ? "text-lg" : "text-base",
|
|
49
53
|
)}
|
|
54
|
+
style={textStyle("muted", scheme)}
|
|
50
55
|
>
|
|
51
56
|
{meta}
|
|
52
57
|
</Text>
|
package/src/PillTabs.tsx
CHANGED
|
@@ -1,6 +1,19 @@
|
|
|
1
1
|
import { View, Text, Pressable } from "react-native";
|
|
2
|
+
import { cn } from "./cn";
|
|
3
|
+
import {
|
|
4
|
+
MIN_TOUCH_TARGET,
|
|
5
|
+
tabColors,
|
|
6
|
+
tabTriggerSize,
|
|
7
|
+
touchSlop,
|
|
8
|
+
typeStyle,
|
|
9
|
+
} from "./controlStyles";
|
|
2
10
|
import { useLargerText } from "./theme";
|
|
11
|
+
import { useScheme } from "./useScheme";
|
|
3
12
|
|
|
13
|
+
/**
|
|
14
|
+
* A row of pill tabs from an options array: the `pills` `TabsList` look and
|
|
15
|
+
* size (30 high, `text-sm` medium), as wide as its pills, at the start.
|
|
16
|
+
*/
|
|
4
17
|
export function PillTabs<T extends string>({
|
|
5
18
|
options,
|
|
6
19
|
value,
|
|
@@ -11,10 +24,16 @@ export function PillTabs<T extends string>({
|
|
|
11
24
|
onChange: (id: T) => void;
|
|
12
25
|
}) {
|
|
13
26
|
const largerText = useLargerText();
|
|
27
|
+
const scheme = useScheme();
|
|
28
|
+
const trigger = tabTriggerSize("pills", "default", largerText);
|
|
14
29
|
return (
|
|
15
|
-
<View
|
|
30
|
+
<View
|
|
31
|
+
accessibilityRole="tablist"
|
|
32
|
+
className="max-w-full flex-row flex-wrap gap-1.5 self-start"
|
|
33
|
+
>
|
|
16
34
|
{options.map((tab) => {
|
|
17
35
|
const isSelected = value === tab.id;
|
|
36
|
+
const colors = tabColors("pills", isSelected, scheme);
|
|
18
37
|
|
|
19
38
|
return (
|
|
20
39
|
<Pressable
|
|
@@ -24,22 +43,17 @@ export function PillTabs<T extends string>({
|
|
|
24
43
|
onPress={() => onChange(tab.id)}
|
|
25
44
|
accessibilityRole="tab"
|
|
26
45
|
accessibilityState={{ selected: isSelected }}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
}
|
|
32
|
-
style={
|
|
46
|
+
hitSlop={touchSlop(MIN_TOUCH_TARGET, trigger.minHeight)}
|
|
47
|
+
className={cn(
|
|
48
|
+
"shrink justify-center rounded-full border border-white/40 active:opacity-70 dark:border-white/10",
|
|
49
|
+
trigger.className,
|
|
50
|
+
)}
|
|
51
|
+
style={{ backgroundColor: colors.backgroundColor, minHeight: trigger.minHeight }}
|
|
33
52
|
>
|
|
34
53
|
<Text
|
|
35
54
|
numberOfLines={1}
|
|
36
|
-
className={
|
|
37
|
-
|
|
38
|
-
} ${
|
|
39
|
-
isSelected
|
|
40
|
-
? "text-pink-700 dark:text-pink-300"
|
|
41
|
-
: "text-muted-foreground"
|
|
42
|
-
}`}
|
|
55
|
+
className={cn("text-center font-instrument-sans-medium", trigger.textClassName)}
|
|
56
|
+
style={[{ color: colors.color }, typeStyle("medium")]}
|
|
43
57
|
>
|
|
44
58
|
{tab.label}
|
|
45
59
|
</Text>
|
package/src/Progress.tsx
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { useEffect } from "react";
|
|
2
|
+
import { View, type ViewProps } from "react-native";
|
|
3
|
+
import Animated, {
|
|
4
|
+
Easing,
|
|
5
|
+
useAnimatedStyle,
|
|
6
|
+
useSharedValue,
|
|
7
|
+
withTiming,
|
|
8
|
+
} from "react-native-reanimated";
|
|
9
|
+
import { cn } from "./cn";
|
|
10
|
+
import { palette } from "./palette";
|
|
11
|
+
import { useAccentColor } from "./theme";
|
|
12
|
+
import { useReduceMotion } from "./useReduceMotion";
|
|
13
|
+
import { useScheme } from "./useScheme";
|
|
14
|
+
|
|
15
|
+
export type ProgressProps = ViewProps & {
|
|
16
|
+
/** Current value, 0 to `max`. Default 0. */
|
|
17
|
+
value?: number | null;
|
|
18
|
+
/** Default 100. */
|
|
19
|
+
max?: number;
|
|
20
|
+
/** Indicator fill. Default the theme accent. */
|
|
21
|
+
indicatorColor?: string;
|
|
22
|
+
className?: string;
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
/** Determinate bar: 10 high, rounded, `palette.default` track. The fill eases over 300 ms (instant with Reduce Motion). */
|
|
26
|
+
export function Progress({
|
|
27
|
+
value,
|
|
28
|
+
max = 100,
|
|
29
|
+
indicatorColor,
|
|
30
|
+
className,
|
|
31
|
+
style,
|
|
32
|
+
...props
|
|
33
|
+
}: ProgressProps) {
|
|
34
|
+
const scheme = useScheme();
|
|
35
|
+
const accent = useAccentColor();
|
|
36
|
+
const reduceMotion = useReduceMotion();
|
|
37
|
+
const percent = Math.min(100, Math.max(0, ((value ?? 0) / max) * 100));
|
|
38
|
+
const width = useSharedValue(percent);
|
|
39
|
+
|
|
40
|
+
useEffect(() => {
|
|
41
|
+
width.value = reduceMotion
|
|
42
|
+
? percent
|
|
43
|
+
: withTiming(percent, {
|
|
44
|
+
duration: 300,
|
|
45
|
+
easing: Easing.bezier(0.22, 1, 0.36, 1),
|
|
46
|
+
});
|
|
47
|
+
}, [percent, reduceMotion, width]);
|
|
48
|
+
|
|
49
|
+
const indicatorStyle = useAnimatedStyle(() => ({
|
|
50
|
+
width: `${width.value}%` as const,
|
|
51
|
+
}));
|
|
52
|
+
|
|
53
|
+
return (
|
|
54
|
+
<View
|
|
55
|
+
accessibilityRole="progressbar"
|
|
56
|
+
accessibilityValue={{ min: 0, max, now: value ?? 0 }}
|
|
57
|
+
className={cn("h-2.5 w-full overflow-hidden rounded-full", className)}
|
|
58
|
+
style={[{ backgroundColor: palette.default[scheme] }, style]}
|
|
59
|
+
{...props}
|
|
60
|
+
>
|
|
61
|
+
<Animated.View
|
|
62
|
+
className="h-full rounded-full"
|
|
63
|
+
style={[{ backgroundColor: indicatorColor ?? accent }, indicatorStyle]}
|
|
64
|
+
/>
|
|
65
|
+
</View>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { Pressable, useWindowDimensions, View } from "react-native";
|
|
3
|
+
import { IconSearch, IconX } from "@tabler/icons-react-native";
|
|
4
|
+
import { Input as PrimitiveInput } from "./primitives/input";
|
|
5
|
+
import { cn } from "./cn";
|
|
6
|
+
import {
|
|
7
|
+
fieldStyle,
|
|
8
|
+
fieldTextStyle,
|
|
9
|
+
scaledFieldHeight,
|
|
10
|
+
searchInputSize,
|
|
11
|
+
sizeStyle,
|
|
12
|
+
touchSlop,
|
|
13
|
+
typeStyle,
|
|
14
|
+
type SearchInputSize,
|
|
15
|
+
} from "./controlStyles";
|
|
16
|
+
import { palette } from "./palette";
|
|
17
|
+
import { tokens } from "./tokens";
|
|
18
|
+
import { useLargerText } from "./theme";
|
|
19
|
+
import { useScheme } from "./useScheme";
|
|
20
|
+
|
|
21
|
+
type SearchInputProps = Omit<
|
|
22
|
+
React.ComponentPropsWithoutRef<typeof PrimitiveInput>,
|
|
23
|
+
"value" | "onChange" | "onChangeText"
|
|
24
|
+
> & {
|
|
25
|
+
value: string;
|
|
26
|
+
/** Called with the new text, as the web `onChange(value)`. */
|
|
27
|
+
onChange: (value: string) => void;
|
|
28
|
+
/** Accessibility label, and the placeholder when `placeholder` is not set. Default `"Search"`. */
|
|
29
|
+
label?: string;
|
|
30
|
+
/** `sm` is 32 high (web `h-8`), `md` is 40 (web `h-10`), border included. Default `"sm"`. */
|
|
31
|
+
size?: SearchInputSize;
|
|
32
|
+
/** Called after `onChange` whenever the value becomes empty (typed away or cleared). */
|
|
33
|
+
onClear?: () => void;
|
|
34
|
+
/** Accessibility label of the clear button. Default `"Clear search"`. */
|
|
35
|
+
clearLabel?: string;
|
|
36
|
+
/** Classes for the field. */
|
|
37
|
+
className?: string;
|
|
38
|
+
/** Classes for the `View` around the field, icon and clear button. */
|
|
39
|
+
wrapperClassName?: string;
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/** The web change rule: `onChange`, then `onClear` whenever the value becomes empty. */
|
|
43
|
+
export function searchChange(onChange: (value: string) => void, onClear?: () => void) {
|
|
44
|
+
return (next: string) => {
|
|
45
|
+
onChange(next);
|
|
46
|
+
if (onClear && next.length === 0) onClear();
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Search field, the web `SearchInput`: a 16 search icon at the start and,
|
|
52
|
+
* while there is text, a clear button (a 14 cross labelled "Clear search") at
|
|
53
|
+
* the end. Controlled with `value` and `onChange(text)`, as on the web. The
|
|
54
|
+
* field has the `Input` look (`fieldStyle`) at the web sizes from
|
|
55
|
+
* `searchInputSize`. The clear button is painted at the web 24 and reaches 44
|
|
56
|
+
* with its hit slop (`touchSlop`). It clears at once and leaves focus where it
|
|
57
|
+
* was, as the default web clear does after its dissolve. It is hidden when the
|
|
58
|
+
* field is not editable. Inside a `ScrollView`, set
|
|
59
|
+
* `keyboardShouldPersistTaps="handled"` so the first tap clears while the
|
|
60
|
+
* keyboard is up. The web `shortcut` hint and `toolbar` variant are not ported.
|
|
61
|
+
*/
|
|
62
|
+
const SearchInput = React.forwardRef<
|
|
63
|
+
React.ElementRef<typeof PrimitiveInput>,
|
|
64
|
+
SearchInputProps
|
|
65
|
+
>(
|
|
66
|
+
(
|
|
67
|
+
{
|
|
68
|
+
value,
|
|
69
|
+
onChange,
|
|
70
|
+
label = "Search",
|
|
71
|
+
placeholder,
|
|
72
|
+
size = "sm",
|
|
73
|
+
onClear,
|
|
74
|
+
clearLabel = "Clear search",
|
|
75
|
+
className,
|
|
76
|
+
wrapperClassName,
|
|
77
|
+
placeholderTextColor,
|
|
78
|
+
style,
|
|
79
|
+
...props
|
|
80
|
+
},
|
|
81
|
+
ref,
|
|
82
|
+
) => {
|
|
83
|
+
const largerText = useLargerText();
|
|
84
|
+
const scheme = useScheme();
|
|
85
|
+
const { fontScale } = useWindowDimensions();
|
|
86
|
+
const metrics = searchInputSize(size, largerText);
|
|
87
|
+
const change = searchChange(onChange, onClear);
|
|
88
|
+
const filled = value.length > 0;
|
|
89
|
+
const muted = tokens.color.muted[scheme];
|
|
90
|
+
return (
|
|
91
|
+
<View className={wrapperClassName}>
|
|
92
|
+
<PrimitiveInput
|
|
93
|
+
ref={ref}
|
|
94
|
+
value={value}
|
|
95
|
+
onChangeText={change}
|
|
96
|
+
accessibilityLabel={label}
|
|
97
|
+
placeholder={placeholder ?? label}
|
|
98
|
+
placeholderTextColor={placeholderTextColor ?? palette.fieldPlaceholder[scheme]}
|
|
99
|
+
// The web `enterKeyHint="search"`.
|
|
100
|
+
returnKeyType="search"
|
|
101
|
+
className={cn(
|
|
102
|
+
"border font-instrument-sans",
|
|
103
|
+
metrics.text,
|
|
104
|
+
largerText && "font-instrument-sans-medium",
|
|
105
|
+
className,
|
|
106
|
+
)}
|
|
107
|
+
style={[
|
|
108
|
+
fieldStyle(scheme),
|
|
109
|
+
fieldTextStyle(scheme),
|
|
110
|
+
{
|
|
111
|
+
minHeight: metrics.minHeight,
|
|
112
|
+
// The web border-box height, so the class line height cannot
|
|
113
|
+
// grow the field; it still grows with the system font size.
|
|
114
|
+
height: scaledFieldHeight(metrics.height, fontScale),
|
|
115
|
+
paddingTop: metrics.paddingY,
|
|
116
|
+
paddingBottom: metrics.paddingY,
|
|
117
|
+
paddingLeft: metrics.paddingLeft,
|
|
118
|
+
paddingRight: filled ? metrics.filledPaddingRight : metrics.paddingRight,
|
|
119
|
+
// The size without its line height: on a single-line field a
|
|
120
|
+
// line height can push the text off centre on iOS.
|
|
121
|
+
fontSize: sizeStyle(metrics.text).fontSize,
|
|
122
|
+
},
|
|
123
|
+
largerText ? typeStyle("medium") : null,
|
|
124
|
+
style,
|
|
125
|
+
]}
|
|
126
|
+
{...props}
|
|
127
|
+
/>
|
|
128
|
+
<View
|
|
129
|
+
pointerEvents="none"
|
|
130
|
+
className="absolute bottom-0 top-0 justify-center"
|
|
131
|
+
style={{ left: metrics.iconLeft }}
|
|
132
|
+
>
|
|
133
|
+
<IconSearch size={metrics.icon} color={muted} />
|
|
134
|
+
</View>
|
|
135
|
+
{filled && props.editable !== false ? (
|
|
136
|
+
<View
|
|
137
|
+
pointerEvents="box-none"
|
|
138
|
+
className="absolute bottom-0 top-0 justify-center"
|
|
139
|
+
style={{ right: metrics.clearRight }}
|
|
140
|
+
>
|
|
141
|
+
<Pressable
|
|
142
|
+
testID="search-input-clear"
|
|
143
|
+
accessibilityRole="button"
|
|
144
|
+
accessibilityLabel={clearLabel}
|
|
145
|
+
hitSlop={touchSlop(metrics.clear)}
|
|
146
|
+
onPress={() => change("")}
|
|
147
|
+
className="items-center justify-center rounded-md active:opacity-70"
|
|
148
|
+
style={{ width: metrics.clear, height: metrics.clear }}
|
|
149
|
+
>
|
|
150
|
+
{/* `strokeWidth`, not the web `stroke`: here `stroke` is the colour. */}
|
|
151
|
+
<IconX size={metrics.clearIcon} color={muted} strokeWidth={1.75} />
|
|
152
|
+
</Pressable>
|
|
153
|
+
</View>
|
|
154
|
+
) : null}
|
|
155
|
+
</View>
|
|
156
|
+
);
|
|
157
|
+
},
|
|
158
|
+
);
|
|
159
|
+
|
|
160
|
+
SearchInput.displayName = "SearchInput";
|
|
161
|
+
|
|
162
|
+
export { SearchInput };
|
|
163
|
+
export type { SearchInputProps };
|