@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.
Files changed (91) hide show
  1. package/README.md +171 -9
  2. package/package.json +84 -2
  3. package/src/AlertDialog.tsx +73 -0
  4. package/src/AmbientBackground.tsx +26 -11
  5. package/src/Avatar.tsx +141 -0
  6. package/src/Badge.tsx +108 -0
  7. package/src/BottomSheetContainer.tsx +7 -1
  8. package/src/Button.tsx +153 -52
  9. package/src/Card.tsx +111 -36
  10. package/src/Checkbox.tsx +78 -0
  11. package/src/Chip.tsx +5 -3
  12. package/src/ConfirmDialog.tsx +105 -0
  13. package/src/DateTimeField.tsx +6 -2
  14. package/src/Dialog.tsx +253 -0
  15. package/src/EmptyState.tsx +122 -7
  16. package/src/FloatingFAB.tsx +2 -4
  17. package/src/FloatingSurface.tsx +31 -9
  18. package/src/FloatingTabBar.tsx +39 -11
  19. package/src/Input.tsx +15 -4
  20. package/src/Label.tsx +31 -0
  21. package/src/LabeledSwitchRow.tsx +96 -0
  22. package/src/LiquidGlass.tsx +2 -10
  23. package/src/ListRow.tsx +7 -2
  24. package/src/PageTitle.tsx +81 -0
  25. package/src/PillTabs.tsx +28 -14
  26. package/src/Progress.tsx +67 -0
  27. package/src/SearchInput.tsx +163 -0
  28. package/src/Select.tsx +32 -6
  29. package/src/Separator.tsx +38 -0
  30. package/src/Sheet.tsx +107 -0
  31. package/src/Skeleton.tsx +25 -5
  32. package/src/Spinner.tsx +92 -0
  33. package/src/StatusDot.tsx +66 -0
  34. package/src/Switch.tsx +24 -7
  35. package/src/Tabs.tsx +190 -0
  36. package/src/Textarea.tsx +47 -0
  37. package/src/Toast.tsx +170 -0
  38. package/src/blur.ts +48 -0
  39. package/src/brand/AppIcons.tsx +106 -0
  40. package/src/brand/AppMark.tsx +83 -0
  41. package/src/brand/AppWordmark.tsx +97 -0
  42. package/src/brand/familyApps.ts +24 -0
  43. package/src/brand/index.ts +34 -0
  44. package/src/brand/svg.ts +263 -0
  45. package/src/brand/vProAppBenefits.ts +22 -0
  46. package/src/controlStyles.ts +395 -0
  47. package/src/dialogLayout.ts +20 -0
  48. package/src/entries/ambient.ts +3 -0
  49. package/src/entries/bottom-sheet.ts +2 -0
  50. package/src/entries/brand.ts +2 -0
  51. package/src/entries/button.ts +3 -0
  52. package/src/entries/date-time.ts +3 -0
  53. package/src/entries/floating.ts +24 -0
  54. package/src/entries/layout.ts +10 -0
  55. package/src/entries/marketing.ts +10 -0
  56. package/src/entries/page-title.ts +4 -0
  57. package/src/entries/primitives.ts +99 -0
  58. package/src/entries/search-input.ts +4 -0
  59. package/src/entries/tab-bar.ts +10 -0
  60. package/src/entries/theme.ts +15 -0
  61. package/src/entries/toast.ts +6 -0
  62. package/src/examples/ParityGallery.tsx +513 -0
  63. package/src/floating.ts +5 -1
  64. package/src/glassBlur.tsx +95 -0
  65. package/src/gradient.ts +21 -0
  66. package/src/haptics.ts +22 -2
  67. package/src/index.ts +101 -2
  68. package/src/initials.ts +10 -0
  69. package/src/marketing/PricingModal.tsx +410 -0
  70. package/src/marketing/UpgradeToProCard.tsx +103 -0
  71. package/src/marketing/pricing.ts +76 -0
  72. package/src/palette.ts +43 -0
  73. package/src/primitives/button.tsx +3 -1
  74. package/src/primitives/card.tsx +5 -2
  75. package/src/primitives/input.tsx +9 -2
  76. package/src/primitives/native-only-animated-view.tsx +10 -3
  77. package/src/primitives/overlay.tsx +179 -0
  78. package/src/primitives/select.tsx +25 -12
  79. package/src/primitives/switch.tsx +12 -13
  80. package/src/primitives/text.tsx +25 -8
  81. package/src/primitives/use-controllable-state.ts +24 -0
  82. package/src/settings/SettingsField.tsx +11 -2
  83. package/src/settings/SettingsSection.tsx +20 -6
  84. package/src/settings/SettingsToggleRow.tsx +7 -2
  85. package/src/tabBarRoutes.ts +52 -0
  86. package/src/toastChrome.ts +103 -0
  87. package/src/toastStore.ts +86 -0
  88. package/src/tokens.ts +19 -14
  89. package/src/typography.ts +24 -0
  90. package/src/useReduceMotion.ts +24 -0
  91. package/src/useScheme.ts +6 -0
@@ -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 { Tabs } from "expo-router";
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
- type TabBarProps = Parameters<
16
- NonNullable<ComponentProps<typeof Tabs>["tabBar"]>
17
- >[0];
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
- }: TabBarProps) {
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 ?? "#737373"}
22
+ placeholderTextColor={placeholderTextColor ?? palette.fieldPlaceholder[scheme]}
19
23
  className={cn(
20
- "min-h-14 rounded-full border border-white/40 bg-secondary px-4 py-3 text-lg font-instrument-sans-medium text-foreground dark:border-white/10",
21
- largerText && "min-h-16 py-4 text-xl",
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
+ }
@@ -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 function GlassBlurProvider({ children }: { children: ReactNode }) {
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 text-foreground",
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 text-muted-foreground",
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 className="flex-row gap-2">
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
- className={`flex-1 justify-center rounded-full border border-white/40 px-3 dark:border-white/10 ${
28
- largerText ? "min-h-16 py-3" : "min-h-14 py-3"
29
- } ${
30
- isSelected ? "bg-pink-100 dark:bg-pink-900/40" : "bg-secondary"
31
- }`}
32
- style={({ pressed }) => ({ opacity: pressed ? 0.7 : 1 })}
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={`text-center font-instrument-sans-semibold ${
37
- largerText ? "text-xl" : "text-lg"
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>
@@ -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
+ }