@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
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// Plan types and pure helpers for `PricingModal`, the same shapes and rules as
|
|
2
|
+
// the web `PricingModal` multi-plan layout. No React Native imports.
|
|
3
|
+
import type { ReactNode } from "react";
|
|
4
|
+
import type { AppName } from "../brand/svg";
|
|
5
|
+
import { V_PRO_APP_BENEFITS, type PricingPlanAppBenefit } from "../brand/vProAppBenefits";
|
|
6
|
+
|
|
7
|
+
export type PricingModalPeriod = "month" | "year";
|
|
8
|
+
|
|
9
|
+
export type PricingPlanBenefit = {
|
|
10
|
+
label: ReactNode;
|
|
11
|
+
/** `"check"` (default) or an app mark. */
|
|
12
|
+
icon?: "check" | AppName;
|
|
13
|
+
/** Dimmed row. */
|
|
14
|
+
muted?: boolean;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export type PricingPlan = {
|
|
18
|
+
id: string;
|
|
19
|
+
name: string;
|
|
20
|
+
/** Display strings per month, for example `{ month: "$10", year: "$8" }`. The yearly price is still per month. */
|
|
21
|
+
price: { month: string; year: string };
|
|
22
|
+
/** Default "Get started". */
|
|
23
|
+
ctaLabel?: string;
|
|
24
|
+
/** Line above the highlight, for example "Everything in Free, and:". */
|
|
25
|
+
intro?: ReactNode;
|
|
26
|
+
/** Large title with small text under it. */
|
|
27
|
+
highlight?: { title: string; description: string };
|
|
28
|
+
benefits?: readonly PricingPlanBenefit[];
|
|
29
|
+
/** Grouped per app, after `benefits`. The featured plan defaults to `V_PRO_APP_BENEFITS`. */
|
|
30
|
+
appBenefits?: readonly PricingPlanAppBenefit[];
|
|
31
|
+
/** The plan the user is on: the button reads "Current plan" and is disabled. */
|
|
32
|
+
current?: boolean;
|
|
33
|
+
/** Subtle ring on the card. `highlighted` is an alias. */
|
|
34
|
+
featured?: boolean;
|
|
35
|
+
highlighted?: boolean;
|
|
36
|
+
/** Text after the price. Default "/month". */
|
|
37
|
+
periodSuffix?: string;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
/** `featured`, else `highlighted` (the web rule: `featured ?? highlighted`). */
|
|
41
|
+
export function isEmphasised(plan: PricingPlan): boolean {
|
|
42
|
+
return Boolean(plan.featured ?? plan.highlighted);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** The plan's app groups: its own, else `V_PRO_APP_BENEFITS` for the emphasised plan, else none. */
|
|
46
|
+
export function planAppBenefits(
|
|
47
|
+
plan: PricingPlan,
|
|
48
|
+
): readonly PricingPlanAppBenefit[] | undefined {
|
|
49
|
+
return plan.appBenefits ?? (isEmphasised(plan) ? V_PRO_APP_BENEFITS : undefined);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Stable sort: the current app's group first, the rest in their order. */
|
|
53
|
+
export function sortAppBenefits(
|
|
54
|
+
groups: readonly PricingPlanAppBenefit[],
|
|
55
|
+
currentApp?: AppName,
|
|
56
|
+
): PricingPlanAppBenefit[] {
|
|
57
|
+
return [...groups].sort(
|
|
58
|
+
(a, b) => Number(b.app === currentApp) - Number(a.app === currentApp),
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** Label, variant and state of a plan's button, as on the web. */
|
|
63
|
+
export function planButton(plan: PricingPlan): {
|
|
64
|
+
label: string;
|
|
65
|
+
variant: "default" | "secondary" | "outline";
|
|
66
|
+
disabled: boolean;
|
|
67
|
+
} {
|
|
68
|
+
if (plan.current) {
|
|
69
|
+
return { label: "Current plan", variant: "outline", disabled: true };
|
|
70
|
+
}
|
|
71
|
+
return {
|
|
72
|
+
label: plan.ctaLabel ?? "Get started",
|
|
73
|
+
variant: isEmphasised(plan) ? "default" : "secondary",
|
|
74
|
+
disabled: false,
|
|
75
|
+
};
|
|
76
|
+
}
|
package/src/palette.ts
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Web semantic colours for React Native, converted from `@vvedantb/ui`
|
|
3
|
+
* `tokens.css` (oklch → sRGB hex, clipped to sRGB). `tokens.ts` holds the
|
|
4
|
+
* neutrals the native app ships; these are the colours it lacks (primary,
|
|
5
|
+
* danger, success, warning, overlay, segment, field placeholder, backdrop,
|
|
6
|
+
* dialog description).
|
|
7
|
+
*
|
|
8
|
+
* A test in this package converts `tokens.css` and fails when a value here
|
|
9
|
+
* drifts, so change `tokens.css` first and then this file.
|
|
10
|
+
*/
|
|
11
|
+
export const palette = {
|
|
12
|
+
/** `--accent` (web `primary`): near-black in light, near-white in dark. */
|
|
13
|
+
primary: { light: "#181818", dark: "#fcfcfc" },
|
|
14
|
+
primaryForeground: { light: "#fcfcfc", dark: "#181818" },
|
|
15
|
+
danger: { light: "#ff373c", dark: "#db3b3e" },
|
|
16
|
+
dangerForeground: { light: "#fcfcfc", dark: "#fcfcfc" },
|
|
17
|
+
success: { light: "#15c964", dark: "#15c964" },
|
|
18
|
+
successForeground: { light: "#161917", dark: "#161917" },
|
|
19
|
+
warning: { light: "#f5a523", dark: "#f7b750" },
|
|
20
|
+
warningForeground: { light: "#1a1815", dark: "#1a1815" },
|
|
21
|
+
/** `--default` (web `secondary`): neutral control fill. */
|
|
22
|
+
default: { light: "#ebebeb", dark: "#272727" },
|
|
23
|
+
defaultForeground: { light: "#181818", dark: "#fcfcfc" },
|
|
24
|
+
overlay: { light: "#ffffff", dark: "#181818" },
|
|
25
|
+
overlayForeground: { light: "#181818", dark: "#fcfcfc" },
|
|
26
|
+
segment: { light: "#ffffff", dark: "#474747" },
|
|
27
|
+
segmentForeground: { light: "#181818", dark: "#fcfcfc" },
|
|
28
|
+
surfaceTertiary: { light: "#eaeaea", dark: "#282627" },
|
|
29
|
+
fieldPlaceholder: { light: "#727272", dark: "#a0a0a0" },
|
|
30
|
+
backdrop: { light: "rgba(0, 0, 0, 0.5)", dark: "rgba(0, 0, 0, 0.6)" },
|
|
31
|
+
/** Web `DialogDescription` text: `--overlay-foreground` mixed 72% into `--overlay` (oklch), for 4.5:1 on the overlay. */
|
|
32
|
+
overlayMuted: { light: "#505050", dark: "#b5b5b5" },
|
|
33
|
+
/** Fill of the web destructive `Button` (`oklch(47.2% 0.195 25.7)`), the same in both schemes. */
|
|
34
|
+
destructive: "#af0015",
|
|
35
|
+
} as const;
|
|
36
|
+
|
|
37
|
+
export type Palette = typeof palette;
|
|
38
|
+
|
|
39
|
+
/** `#rrggbb` with an alpha, as `rgba(...)`. For tints such as the badge fills (web `bg-danger/12`). */
|
|
40
|
+
export function withAlpha(hex: string, alpha: number): string {
|
|
41
|
+
const n = Number.parseInt(hex.slice(1, 7), 16);
|
|
42
|
+
return `rgba(${(n >> 16) & 255}, ${(n >> 8) & 255}, ${n & 255}, ${alpha})`;
|
|
43
|
+
}
|
|
@@ -47,7 +47,9 @@ const buttonVariants = cva(
|
|
|
47
47
|
},
|
|
48
48
|
);
|
|
49
49
|
|
|
50
|
-
|
|
50
|
+
// No weight: a class context cannot carry the `typeStyle` that a face class
|
|
51
|
+
// needs, so `Button` sets the medium face and its weight on its label.
|
|
52
|
+
const buttonTextVariants = cva("text-sm text-foreground", {
|
|
51
53
|
variants: {
|
|
52
54
|
variant: {
|
|
53
55
|
default: "text-primary-foreground",
|
package/src/primitives/card.tsx
CHANGED
|
@@ -2,6 +2,7 @@ import * as React from "react";
|
|
|
2
2
|
import { View } from "react-native";
|
|
3
3
|
import { Text, TextClassContext } from "./text";
|
|
4
4
|
import { cn } from "../cn";
|
|
5
|
+
import { typeStyle } from "../controlStyles";
|
|
5
6
|
|
|
6
7
|
type CardViewProps = React.ComponentPropsWithoutRef<typeof View> & {
|
|
7
8
|
className?: string;
|
|
@@ -40,13 +41,15 @@ const CardHeader = React.forwardRef<
|
|
|
40
41
|
const CardTitle = React.forwardRef<
|
|
41
42
|
React.ElementRef<typeof Text>,
|
|
42
43
|
React.ComponentPropsWithoutRef<typeof Text> & { className?: string }
|
|
43
|
-
>(({ className, ...props }, ref) => {
|
|
44
|
+
>(({ className, style, ...props }, ref) => {
|
|
45
|
+
// The web `font-semibold`: the semibold face, with its weight as a style.
|
|
44
46
|
return (
|
|
45
47
|
<Text
|
|
46
48
|
ref={ref}
|
|
47
49
|
role="heading"
|
|
48
50
|
aria-level={3}
|
|
49
|
-
className={cn("font-semibold leading-none", className)}
|
|
51
|
+
className={cn("font-instrument-sans-semibold leading-none", className)}
|
|
52
|
+
style={[typeStyle("semibold"), style]}
|
|
50
53
|
{...props}
|
|
51
54
|
/>
|
|
52
55
|
);
|
package/src/primitives/input.tsx
CHANGED
|
@@ -1,16 +1,24 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
2
|
import { Platform, TextInput } from "react-native";
|
|
3
3
|
import { cn } from "../cn";
|
|
4
|
+
import { palette } from "../palette";
|
|
5
|
+
import { useScheme } from "../useScheme";
|
|
4
6
|
|
|
5
7
|
type InputProps = React.ComponentPropsWithoutRef<typeof TextInput> & {
|
|
6
8
|
className?: string;
|
|
7
9
|
};
|
|
8
10
|
|
|
9
11
|
const Input = React.forwardRef<React.ElementRef<typeof TextInput>, InputProps>(
|
|
10
|
-
({ className, ...props }, ref) => {
|
|
12
|
+
({ className, placeholderTextColor, ...props }, ref) => {
|
|
13
|
+
const scheme = useScheme();
|
|
11
14
|
return (
|
|
12
15
|
<TextInput
|
|
13
16
|
ref={ref}
|
|
17
|
+
// A prop, not a `placeholder:` class: on native NativeWind turns that
|
|
18
|
+
// class into `placeholderTextColor`, which beats the prop. The old
|
|
19
|
+
// `placeholder:text-muted-foreground/50` was the light grey at half
|
|
20
|
+
// opacity in apps that nest `dark` under `muted-foreground`.
|
|
21
|
+
placeholderTextColor={placeholderTextColor ?? palette.fieldPlaceholder[scheme]}
|
|
14
22
|
className={cn(
|
|
15
23
|
"flex w-full flex-row items-center rounded-lg border-0 bg-secondary px-3 py-2 text-base text-foreground",
|
|
16
24
|
props.editable === false &&
|
|
@@ -22,7 +30,6 @@ const Input = React.forwardRef<React.ElementRef<typeof TextInput>, InputProps>(
|
|
|
22
30
|
),
|
|
23
31
|
Platform.select({
|
|
24
32
|
web: "outline-none transition-[color,box-shadow] md:text-sm",
|
|
25
|
-
native: "placeholder:text-muted-foreground/50",
|
|
26
33
|
}),
|
|
27
34
|
className,
|
|
28
35
|
)}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { Platform, View, type ViewProps } from "react-native";
|
|
2
2
|
import Animated, { type AnimatedProps } from "react-native-reanimated";
|
|
3
|
+
import { useReduceMotion } from "../useReduceMotion";
|
|
3
4
|
|
|
4
5
|
type NativeOnlyAnimatedViewProps = ViewProps & {
|
|
5
6
|
entering?: AnimatedProps<ViewProps>["entering"];
|
|
@@ -7,6 +8,11 @@ type NativeOnlyAnimatedViewProps = ViewProps & {
|
|
|
7
8
|
layout?: AnimatedProps<ViewProps>["layout"];
|
|
8
9
|
};
|
|
9
10
|
|
|
11
|
+
/**
|
|
12
|
+
* `Animated.View` with layout animations on iOS and Android, a plain `View`
|
|
13
|
+
* on web. With Reduce Motion on, it drops the animations: sheets, toasts and
|
|
14
|
+
* the `Select` popover appear and leave in place.
|
|
15
|
+
*/
|
|
10
16
|
function NativeOnlyAnimatedView({
|
|
11
17
|
children,
|
|
12
18
|
entering,
|
|
@@ -14,15 +20,16 @@ function NativeOnlyAnimatedView({
|
|
|
14
20
|
layout,
|
|
15
21
|
...props
|
|
16
22
|
}: NativeOnlyAnimatedViewProps) {
|
|
23
|
+
const reduceMotion = useReduceMotion();
|
|
17
24
|
if (Platform.OS === "web") {
|
|
18
25
|
return <View {...props}>{children}</View>;
|
|
19
26
|
}
|
|
20
27
|
|
|
21
28
|
return (
|
|
22
29
|
<Animated.View
|
|
23
|
-
entering={entering}
|
|
24
|
-
exiting={exiting}
|
|
25
|
-
layout={layout}
|
|
30
|
+
entering={reduceMotion ? undefined : entering}
|
|
31
|
+
exiting={reduceMotion ? undefined : exiting}
|
|
32
|
+
layout={reduceMotion ? undefined : layout}
|
|
26
33
|
{...props}
|
|
27
34
|
>
|
|
28
35
|
{children}
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
import * as Slot from "@rn-primitives/slot";
|
|
2
|
+
import {
|
|
3
|
+
createContext,
|
|
4
|
+
useContext,
|
|
5
|
+
type ComponentPropsWithoutRef,
|
|
6
|
+
type ReactNode,
|
|
7
|
+
} from "react";
|
|
8
|
+
import {
|
|
9
|
+
KeyboardAvoidingView,
|
|
10
|
+
Modal,
|
|
11
|
+
Pressable,
|
|
12
|
+
StyleSheet,
|
|
13
|
+
View,
|
|
14
|
+
type GestureResponderEvent,
|
|
15
|
+
} from "react-native";
|
|
16
|
+
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
|
17
|
+
import { palette } from "../palette";
|
|
18
|
+
import { useReduceMotion } from "../useReduceMotion";
|
|
19
|
+
import { useScheme } from "../useScheme";
|
|
20
|
+
import { useControllableState } from "./use-controllable-state";
|
|
21
|
+
|
|
22
|
+
type OverlayContextValue = {
|
|
23
|
+
open: boolean;
|
|
24
|
+
setOpen: (open: boolean) => void;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const OverlayContext = createContext<OverlayContextValue | null>(null);
|
|
28
|
+
|
|
29
|
+
export function useOverlay(part: string): OverlayContextValue {
|
|
30
|
+
const context = useContext(OverlayContext);
|
|
31
|
+
if (!context) {
|
|
32
|
+
throw new Error(`${part} must be inside its Dialog, AlertDialog or Sheet root.`);
|
|
33
|
+
}
|
|
34
|
+
return context;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export type OverlayRootProps = {
|
|
38
|
+
/** Open state when the parent controls it. */
|
|
39
|
+
open?: boolean;
|
|
40
|
+
/** Initial open state when `open` is not set. Default `false`. */
|
|
41
|
+
defaultOpen?: boolean;
|
|
42
|
+
/** Called with the next open state (trigger, close, backdrop, Android back). */
|
|
43
|
+
onOpenChange?: (open: boolean) => void;
|
|
44
|
+
children?: ReactNode;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/** Root shared by `Dialog`, `AlertDialog` and `Sheet`: holds the open state, renders no view. */
|
|
48
|
+
export function OverlayRoot({
|
|
49
|
+
open,
|
|
50
|
+
defaultOpen = false,
|
|
51
|
+
onOpenChange,
|
|
52
|
+
children,
|
|
53
|
+
}: OverlayRootProps) {
|
|
54
|
+
const [value, setValue] = useControllableState({
|
|
55
|
+
value: open,
|
|
56
|
+
defaultValue: defaultOpen,
|
|
57
|
+
onChange: onOpenChange,
|
|
58
|
+
});
|
|
59
|
+
return (
|
|
60
|
+
<OverlayContext.Provider value={{ open: value, setOpen: setValue }}>
|
|
61
|
+
{children}
|
|
62
|
+
</OverlayContext.Provider>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export type OverlayPressableProps = ComponentPropsWithoutRef<typeof Pressable> & {
|
|
67
|
+
/** Merge the press handler into the single child (for example a `Button`) instead of wrapping it. */
|
|
68
|
+
asChild?: boolean;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
function OverlayPressable({
|
|
72
|
+
asChild,
|
|
73
|
+
next,
|
|
74
|
+
part,
|
|
75
|
+
onPress,
|
|
76
|
+
...props
|
|
77
|
+
}: OverlayPressableProps & { next: boolean; part: string }) {
|
|
78
|
+
const { setOpen } = useOverlay(part);
|
|
79
|
+
const Component = asChild ? Slot.Pressable : Pressable;
|
|
80
|
+
return (
|
|
81
|
+
<Component
|
|
82
|
+
accessibilityRole="button"
|
|
83
|
+
{...props}
|
|
84
|
+
onPress={(event: GestureResponderEvent) => {
|
|
85
|
+
onPress?.(event);
|
|
86
|
+
setOpen(next);
|
|
87
|
+
}}
|
|
88
|
+
/>
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function OverlayTrigger(props: OverlayPressableProps & { part: string }) {
|
|
93
|
+
return <OverlayPressable {...props} next />;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export function OverlayClose(props: OverlayPressableProps & { part: string }) {
|
|
97
|
+
return <OverlayPressable {...props} next={false} />;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* RN `Modal` with a tinted backdrop, safe-area padding and keyboard avoidance.
|
|
102
|
+
* `placement="center"` centres the child (dialogs) inside the insets on every
|
|
103
|
+
* side; `"bottom"` pins it to the bottom edge (sheets), which pad their own
|
|
104
|
+
* bottom inset. The backdrop closes the overlay only when `dismissible`;
|
|
105
|
+
* Android back and the VoiceOver escape gesture always close it, as Escape
|
|
106
|
+
* does on the web. With Reduce Motion on, the fade is off.
|
|
107
|
+
*/
|
|
108
|
+
export function OverlayModal({
|
|
109
|
+
part,
|
|
110
|
+
placement,
|
|
111
|
+
dismissible,
|
|
112
|
+
children,
|
|
113
|
+
}: {
|
|
114
|
+
part: string;
|
|
115
|
+
placement: "center" | "bottom";
|
|
116
|
+
dismissible: boolean;
|
|
117
|
+
children: ReactNode;
|
|
118
|
+
}) {
|
|
119
|
+
const { open, setOpen } = useOverlay(part);
|
|
120
|
+
const scheme = useScheme();
|
|
121
|
+
const insets = useSafeAreaInsets();
|
|
122
|
+
const reduceMotion = useReduceMotion();
|
|
123
|
+
const close = () => setOpen(false);
|
|
124
|
+
|
|
125
|
+
return (
|
|
126
|
+
<Modal
|
|
127
|
+
visible={open}
|
|
128
|
+
transparent
|
|
129
|
+
animationType={reduceMotion ? "none" : "fade"}
|
|
130
|
+
statusBarTranslucent
|
|
131
|
+
onRequestClose={close}
|
|
132
|
+
>
|
|
133
|
+
{/* Android too: a modal window with `statusBarTranslucent` may not
|
|
134
|
+
resize for the keyboard, which then covers the fields. The padding
|
|
135
|
+
is the measured overlap, so it drops to 0 where the window does
|
|
136
|
+
resize. */}
|
|
137
|
+
<KeyboardAvoidingView behavior="padding" style={styles.fill}>
|
|
138
|
+
<Pressable
|
|
139
|
+
testID={`${part}-backdrop`}
|
|
140
|
+
accessible={dismissible}
|
|
141
|
+
accessibilityRole="button"
|
|
142
|
+
accessibilityLabel="Close"
|
|
143
|
+
disabled={!dismissible}
|
|
144
|
+
onPress={close}
|
|
145
|
+
style={[StyleSheet.absoluteFill, { backgroundColor: palette.backdrop[scheme] }]}
|
|
146
|
+
/>
|
|
147
|
+
<View
|
|
148
|
+
pointerEvents="box-none"
|
|
149
|
+
onAccessibilityEscape={close}
|
|
150
|
+
style={[
|
|
151
|
+
styles.fill,
|
|
152
|
+
placement === "bottom"
|
|
153
|
+
? {
|
|
154
|
+
justifyContent: "flex-end",
|
|
155
|
+
paddingTop: insets.top + 16,
|
|
156
|
+
paddingLeft: insets.left,
|
|
157
|
+
paddingRight: insets.right,
|
|
158
|
+
}
|
|
159
|
+
: {
|
|
160
|
+
justifyContent: "center",
|
|
161
|
+
paddingTop: insets.top + 16,
|
|
162
|
+
paddingBottom: insets.bottom + 16,
|
|
163
|
+
paddingLeft: insets.left + 16,
|
|
164
|
+
paddingRight: insets.right + 16,
|
|
165
|
+
},
|
|
166
|
+
]}
|
|
167
|
+
>
|
|
168
|
+
{children}
|
|
169
|
+
</View>
|
|
170
|
+
</KeyboardAvoidingView>
|
|
171
|
+
</Modal>
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
const styles = StyleSheet.create({
|
|
176
|
+
fill: {
|
|
177
|
+
flex: 1,
|
|
178
|
+
},
|
|
179
|
+
});
|
|
@@ -6,13 +6,16 @@ import {
|
|
|
6
6
|
ChevronUpIcon,
|
|
7
7
|
} from "lucide-react-native";
|
|
8
8
|
import * as React from "react";
|
|
9
|
-
import { Platform, ScrollView, StyleSheet, View } from "react-native";
|
|
9
|
+
import { Platform, ScrollView, StyleSheet, Text, View } from "react-native";
|
|
10
10
|
import { FadeIn, FadeOut } from "react-native-reanimated";
|
|
11
11
|
import { FullWindowOverlay as RNFullWindowOverlay } from "react-native-screens";
|
|
12
12
|
import { NativeOnlyAnimatedView } from "./native-only-animated-view";
|
|
13
13
|
import { TextClassContext } from "./text";
|
|
14
14
|
import { LiquidGlass } from "../LiquidGlass";
|
|
15
15
|
import { cn } from "../cn";
|
|
16
|
+
import { textStyle, typeStyle } from "../controlStyles";
|
|
17
|
+
import { tokens } from "../tokens";
|
|
18
|
+
import { useScheme } from "../useScheme";
|
|
16
19
|
|
|
17
20
|
type Option = SelectPrimitive.Option;
|
|
18
21
|
|
|
@@ -30,14 +33,18 @@ type SelectValueProps = React.ComponentPropsWithoutRef<
|
|
|
30
33
|
const SelectValue = React.forwardRef<
|
|
31
34
|
React.ElementRef<typeof SelectPrimitive.Value>,
|
|
32
35
|
SelectValueProps
|
|
33
|
-
>(({ className, ...props }, ref) => {
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
36
|
+
>(({ className, style, ...props }, ref) => {
|
|
37
|
+
const classes = cn("line-clamp-1 text-sm text-foreground", className);
|
|
38
|
+
if (Platform.OS === "web") {
|
|
39
|
+
// Radix `Select.Value` drops `className` and `style`, so the placeholder
|
|
40
|
+
// took the page colour. Its span inherits them from this Text instead.
|
|
41
|
+
return (
|
|
42
|
+
<Text className={classes} style={style}>
|
|
43
|
+
<SelectPrimitive.Value ref={ref} {...props} />
|
|
44
|
+
</Text>
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
return <SelectPrimitive.Value ref={ref} className={classes} style={style} {...props} />;
|
|
41
48
|
});
|
|
42
49
|
|
|
43
50
|
type SelectTriggerProps = React.ComponentPropsWithoutRef<
|
|
@@ -51,6 +58,7 @@ const SelectTrigger = React.forwardRef<
|
|
|
51
58
|
React.ElementRef<typeof SelectPrimitive.Trigger>,
|
|
52
59
|
SelectTriggerProps
|
|
53
60
|
>(({ className, children, size = "default", disabled, ...props }, ref) => {
|
|
61
|
+
const scheme = useScheme();
|
|
54
62
|
return (
|
|
55
63
|
<SelectPrimitive.Trigger
|
|
56
64
|
ref={ref}
|
|
@@ -64,7 +72,7 @@ const SelectTrigger = React.forwardRef<
|
|
|
64
72
|
{...props}
|
|
65
73
|
>
|
|
66
74
|
<>{children}</>
|
|
67
|
-
<ChevronDown size={16} color=
|
|
75
|
+
<ChevronDown size={16} color={tokens.color.muted[scheme]} />
|
|
68
76
|
</SelectPrimitive.Trigger>
|
|
69
77
|
);
|
|
70
78
|
});
|
|
@@ -155,6 +163,7 @@ const SelectItem = React.forwardRef<
|
|
|
155
163
|
React.ElementRef<typeof SelectPrimitive.Item>,
|
|
156
164
|
SelectItemProps
|
|
157
165
|
>(({ className, disabled, ...props }, ref) => {
|
|
166
|
+
const scheme = useScheme();
|
|
158
167
|
return (
|
|
159
168
|
<SelectPrimitive.Item
|
|
160
169
|
ref={ref}
|
|
@@ -168,10 +177,14 @@ const SelectItem = React.forwardRef<
|
|
|
168
177
|
>
|
|
169
178
|
<View className="absolute right-2 size-3.5 items-center justify-center">
|
|
170
179
|
<SelectPrimitive.ItemIndicator>
|
|
171
|
-
<Check size={16} color=
|
|
180
|
+
<Check size={16} color={tokens.color.muted[scheme]} />
|
|
172
181
|
</SelectPrimitive.ItemIndicator>
|
|
173
182
|
</View>
|
|
174
|
-
<SelectPrimitive.ItemText
|
|
183
|
+
<SelectPrimitive.ItemText
|
|
184
|
+
className="select-none text-sm font-instrument-sans-medium"
|
|
185
|
+
// One object: on web the item text is a Radix slot, which drops arrays.
|
|
186
|
+
style={{ ...textStyle("foreground", scheme), ...typeStyle("medium") }}
|
|
187
|
+
/>
|
|
175
188
|
</SelectPrimitive.Item>
|
|
176
189
|
);
|
|
177
190
|
});
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as SwitchPrimitives from "@rn-primitives/switch";
|
|
2
2
|
import * as React from "react";
|
|
3
|
-
import { Platform } from "react-native";
|
|
3
|
+
import { Platform, type ViewStyle } from "react-native";
|
|
4
4
|
import { cn } from "../cn";
|
|
5
5
|
|
|
6
6
|
type SwitchProps = React.ComponentPropsWithoutRef<
|
|
@@ -8,8 +8,12 @@ type SwitchProps = React.ComponentPropsWithoutRef<
|
|
|
8
8
|
> & {
|
|
9
9
|
className?: string;
|
|
10
10
|
thumbClassName?: string;
|
|
11
|
-
|
|
12
|
-
|
|
11
|
+
/**
|
|
12
|
+
* Track and thumb colours. Flat objects: on web each part is a Radix slot,
|
|
13
|
+
* which spreads `style`, so these are the only styles applied there.
|
|
14
|
+
*/
|
|
15
|
+
trackStyle?: ViewStyle;
|
|
16
|
+
thumbStyle?: ViewStyle;
|
|
13
17
|
};
|
|
14
18
|
|
|
15
19
|
const Switch = React.forwardRef<
|
|
@@ -20,8 +24,8 @@ const Switch = React.forwardRef<
|
|
|
20
24
|
{
|
|
21
25
|
className,
|
|
22
26
|
thumbClassName,
|
|
23
|
-
|
|
24
|
-
|
|
27
|
+
trackStyle,
|
|
28
|
+
thumbStyle,
|
|
25
29
|
checked,
|
|
26
30
|
style,
|
|
27
31
|
disabled,
|
|
@@ -30,16 +34,10 @@ const Switch = React.forwardRef<
|
|
|
30
34
|
ref,
|
|
31
35
|
) => {
|
|
32
36
|
const isChecked = Boolean(checked);
|
|
33
|
-
const trackColor =
|
|
34
|
-
Platform.OS !== "web" && (onColor || offColor)
|
|
35
|
-
? { backgroundColor: isChecked ? onColor : offColor }
|
|
36
|
-
: null;
|
|
37
37
|
const resolvedStyle =
|
|
38
38
|
Platform.OS === "web" || typeof style === "function"
|
|
39
|
-
?
|
|
40
|
-
:
|
|
41
|
-
? [trackColor, style]
|
|
42
|
-
: style;
|
|
39
|
+
? trackStyle
|
|
40
|
+
: [trackStyle, style];
|
|
43
41
|
|
|
44
42
|
return (
|
|
45
43
|
<SwitchPrimitives.Root
|
|
@@ -59,6 +57,7 @@ const Switch = React.forwardRef<
|
|
|
59
57
|
{...props}
|
|
60
58
|
>
|
|
61
59
|
<SwitchPrimitives.Thumb
|
|
60
|
+
style={thumbStyle}
|
|
62
61
|
className={cn(
|
|
63
62
|
"h-4 w-4 rounded-full bg-background transition-transform",
|
|
64
63
|
isChecked
|
package/src/primitives/text.tsx
CHANGED
|
@@ -3,6 +3,7 @@ import { cva, type VariantProps } from "class-variance-authority";
|
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
import { Platform, Text as RNText, type Role } from "react-native";
|
|
5
5
|
import { cn } from "../cn";
|
|
6
|
+
import { typeStyle } from "../controlStyles";
|
|
6
7
|
|
|
7
8
|
const textVariants = cva(
|
|
8
9
|
cn(
|
|
@@ -16,27 +17,27 @@ const textVariants = cva(
|
|
|
16
17
|
variant: {
|
|
17
18
|
default: "",
|
|
18
19
|
h1: cn(
|
|
19
|
-
"text-4xl font-
|
|
20
|
+
"text-4xl font-instrument-sans-semibold tracking-tight",
|
|
20
21
|
Platform.select({ web: "scroll-m-20" }),
|
|
21
22
|
),
|
|
22
23
|
h2: cn(
|
|
23
|
-
"text-3xl font-semibold tracking-tight border-border border-b pb-2",
|
|
24
|
+
"text-3xl font-instrument-sans-semibold tracking-tight border-border border-b pb-2",
|
|
24
25
|
Platform.select({ web: "scroll-m-20" }),
|
|
25
26
|
),
|
|
26
27
|
h3: cn(
|
|
27
|
-
"text-2xl font-semibold tracking-tight",
|
|
28
|
+
"text-2xl font-instrument-sans-semibold tracking-tight",
|
|
28
29
|
Platform.select({ web: "scroll-m-20" }),
|
|
29
30
|
),
|
|
30
31
|
h4: cn(
|
|
31
|
-
"text-xl font-semibold tracking-tight",
|
|
32
|
+
"text-xl font-instrument-sans-semibold tracking-tight",
|
|
32
33
|
Platform.select({ web: "scroll-m-20" }),
|
|
33
34
|
),
|
|
34
35
|
p: "leading-7",
|
|
35
36
|
blockquote: "border-l-2 border-border pl-4 italic",
|
|
36
|
-
code: "bg-muted rounded px-[0.3rem] py-[0.2rem] text-sm font-semibold",
|
|
37
|
+
code: "bg-muted rounded px-[0.3rem] py-[0.2rem] text-sm font-instrument-sans-semibold",
|
|
37
38
|
lead: "text-xl text-muted-foreground",
|
|
38
|
-
large: "text-lg font-semibold",
|
|
39
|
-
small: "text-sm font-medium",
|
|
39
|
+
large: "text-lg font-instrument-sans-semibold",
|
|
40
|
+
small: "text-sm font-instrument-sans-medium",
|
|
40
41
|
muted: "text-sm text-muted-foreground",
|
|
41
42
|
},
|
|
42
43
|
},
|
|
@@ -65,6 +66,20 @@ const ARIA_LEVEL: Partial<Record<TextVariant, string>> = {
|
|
|
65
66
|
h4: "4",
|
|
66
67
|
};
|
|
67
68
|
|
|
69
|
+
// The weight of each variant's `font-instrument-sans-*` class, set as a style
|
|
70
|
+
// beside it (`typeStyle`): where an app maps every one of those classes to one
|
|
71
|
+
// family, the class alone draws Regular. Headings are semibold, as the web
|
|
72
|
+
// titles; `h1` was `font-extrabold` before 0.3.13.
|
|
73
|
+
const WEIGHT: Partial<Record<TextVariant, ReturnType<typeof typeStyle>>> = {
|
|
74
|
+
h1: typeStyle("semibold"),
|
|
75
|
+
h2: typeStyle("semibold"),
|
|
76
|
+
h3: typeStyle("semibold"),
|
|
77
|
+
h4: typeStyle("semibold"),
|
|
78
|
+
code: typeStyle("semibold"),
|
|
79
|
+
large: typeStyle("semibold"),
|
|
80
|
+
small: typeStyle("medium"),
|
|
81
|
+
};
|
|
82
|
+
|
|
68
83
|
const TextClassContext = React.createContext<string | undefined>(undefined);
|
|
69
84
|
|
|
70
85
|
type TextProps = React.ComponentPropsWithoutRef<typeof RNText> &
|
|
@@ -74,9 +89,10 @@ type TextProps = React.ComponentPropsWithoutRef<typeof RNText> &
|
|
|
74
89
|
};
|
|
75
90
|
|
|
76
91
|
const Text = React.forwardRef<React.ElementRef<typeof RNText>, TextProps>(
|
|
77
|
-
({ className, asChild = false, variant = "default", ...props }, ref) => {
|
|
92
|
+
({ className, asChild = false, variant = "default", style, ...props }, ref) => {
|
|
78
93
|
const textClass = React.useContext(TextClassContext);
|
|
79
94
|
const Component = asChild ? Slot.Text : RNText;
|
|
95
|
+
const weight = variant ? WEIGHT[variant] : undefined;
|
|
80
96
|
|
|
81
97
|
return (
|
|
82
98
|
<Component
|
|
@@ -84,6 +100,7 @@ const Text = React.forwardRef<React.ElementRef<typeof RNText>, TextProps>(
|
|
|
84
100
|
className={cn(textVariants({ variant }), textClass, className)}
|
|
85
101
|
role={variant ? ROLE[variant] : undefined}
|
|
86
102
|
aria-level={variant ? ARIA_LEVEL[variant] : undefined}
|
|
103
|
+
style={weight ? [weight, style] : style}
|
|
87
104
|
{...props}
|
|
88
105
|
/>
|
|
89
106
|
);
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { useCallback, useState } from "react";
|
|
2
|
+
|
|
3
|
+
/** `value` when the parent controls it, else local state seeded by `defaultValue`. `onChange` fires either way. */
|
|
4
|
+
export function useControllableState<T>({
|
|
5
|
+
value,
|
|
6
|
+
defaultValue,
|
|
7
|
+
onChange,
|
|
8
|
+
}: {
|
|
9
|
+
value: T | undefined;
|
|
10
|
+
defaultValue: T;
|
|
11
|
+
onChange?: (next: T) => void;
|
|
12
|
+
}) {
|
|
13
|
+
const [local, setLocal] = useState(defaultValue);
|
|
14
|
+
const controlled = value !== undefined;
|
|
15
|
+
const current = controlled ? value : local;
|
|
16
|
+
const setValue = useCallback(
|
|
17
|
+
(next: T) => {
|
|
18
|
+
if (!controlled) setLocal(next);
|
|
19
|
+
onChange?.(next);
|
|
20
|
+
},
|
|
21
|
+
[controlled, onChange],
|
|
22
|
+
);
|
|
23
|
+
return [current, setValue] as const;
|
|
24
|
+
}
|