@vvedantb/native-ui 0.2.1 → 0.3.13
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 +171 -9
- package/package.json +84 -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/PageTitle.tsx +81 -0
- 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 +395 -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 +10 -0
- package/src/entries/marketing.ts +10 -0
- package/src/entries/page-title.ts +4 -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 +513 -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 +101 -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/button.tsx +3 -1
- package/src/primitives/card.tsx +5 -2
- 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/text.tsx +25 -8
- 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>
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import type { ReactNode } from "react";
|
|
2
|
+
import { Text, View, type TextProps, type ViewProps } from "react-native";
|
|
3
|
+
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
|
4
|
+
import { cn } from "./cn";
|
|
5
|
+
import {
|
|
6
|
+
PAGE_HEADER_HEIGHT,
|
|
7
|
+
PAGE_TITLE_CLASS,
|
|
8
|
+
pageTitleStyle,
|
|
9
|
+
textStyle,
|
|
10
|
+
} from "./controlStyles";
|
|
11
|
+
import { useScheme } from "./useScheme";
|
|
12
|
+
|
|
13
|
+
export type PageTitleProps = TextProps & { className?: string };
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* The web page title (`Breadcrumb`: `text-xl font-semibold leading-tight
|
|
17
|
+
* tracking-tight`): Instrument Sans 600 at 20 / 25 with -0.5 letter spacing,
|
|
18
|
+
* in the scheme's foreground. A header for VoiceOver and TalkBack. One line,
|
|
19
|
+
* cut off at the end as the web `truncate`; pass `numberOfLines={0}` to wrap.
|
|
20
|
+
* It scales with the system font size.
|
|
21
|
+
*/
|
|
22
|
+
export function PageTitle({ className, style, ...props }: PageTitleProps) {
|
|
23
|
+
const scheme = useScheme();
|
|
24
|
+
return (
|
|
25
|
+
<Text
|
|
26
|
+
accessibilityRole="header"
|
|
27
|
+
numberOfLines={1}
|
|
28
|
+
className={cn(PAGE_TITLE_CLASS, className)}
|
|
29
|
+
style={[textStyle("foreground", scheme), pageTitleStyle(), style]}
|
|
30
|
+
{...props}
|
|
31
|
+
/>
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export type PageHeaderProps = Omit<ViewProps, "children"> & {
|
|
36
|
+
/** The page title, drawn with `PageTitle`. */
|
|
37
|
+
title: string;
|
|
38
|
+
/**
|
|
39
|
+
* Content at the end of the row, such as an "Update all" `Button` (pass it
|
|
40
|
+
* `fullWidth={false}`). It keeps its width; a long title is cut off instead.
|
|
41
|
+
*/
|
|
42
|
+
action?: ReactNode;
|
|
43
|
+
/**
|
|
44
|
+
* Pad the top by the safe-area inset. Default `true`, for a header at the top
|
|
45
|
+
* of a screen with no navigation header. Pass `false` under a native header
|
|
46
|
+
* or inside a view that already pads the inset.
|
|
47
|
+
*/
|
|
48
|
+
safeTop?: boolean;
|
|
49
|
+
className?: string;
|
|
50
|
+
/** Classes on the title. */
|
|
51
|
+
titleClassName?: string;
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* A screen's title row: `PageTitle` at the start and an optional `action` at
|
|
56
|
+
* the end, with the `ScreenContainer` side padding (`px-4`). The row is at
|
|
57
|
+
* least `PAGE_HEADER_HEIGHT` (56) high below the top inset, so tab roots with
|
|
58
|
+
* and without an action put their titles at the same height.
|
|
59
|
+
*/
|
|
60
|
+
export function PageHeader({
|
|
61
|
+
title,
|
|
62
|
+
action,
|
|
63
|
+
safeTop = true,
|
|
64
|
+
className,
|
|
65
|
+
titleClassName,
|
|
66
|
+
style,
|
|
67
|
+
...props
|
|
68
|
+
}: PageHeaderProps) {
|
|
69
|
+
const insets = useSafeAreaInsets();
|
|
70
|
+
const top = safeTop ? insets.top : 0;
|
|
71
|
+
return (
|
|
72
|
+
<View
|
|
73
|
+
className={cn("flex-row items-center justify-between gap-3 px-4", className)}
|
|
74
|
+
style={[{ paddingTop: top, minHeight: top + PAGE_HEADER_HEIGHT }, style]}
|
|
75
|
+
{...props}
|
|
76
|
+
>
|
|
77
|
+
<PageTitle className={cn("min-w-0 flex-1", titleClassName)}>{title}</PageTitle>
|
|
78
|
+
{action ? <View className="shrink-0">{action}</View> : null}
|
|
79
|
+
</View>
|
|
80
|
+
);
|
|
81
|
+
}
|
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
|
+
}
|