@atlure/ui 0.2.0

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 (33) hide show
  1. package/README.md +117 -0
  2. package/package.json +78 -0
  3. package/src/components/avatar/avatar.tsx +43 -0
  4. package/src/components/avatar/utils.ts +12 -0
  5. package/src/components/badge/badge.tsx +31 -0
  6. package/src/components/button/button.tsx +47 -0
  7. package/src/components/button/types.ts +21 -0
  8. package/src/components/card/card-section.tsx +20 -0
  9. package/src/components/card/card.tsx +45 -0
  10. package/src/components/checkbox/checkbox.tsx +50 -0
  11. package/src/components/input/input.tsx +34 -0
  12. package/src/components/label/label.tsx +23 -0
  13. package/src/components/separator/separator.tsx +26 -0
  14. package/src/components/skeleton/hooks/use-pulse-opacity.ts +40 -0
  15. package/src/components/skeleton/skeleton.tsx +34 -0
  16. package/src/components/switch/switch.tsx +44 -0
  17. package/src/components/text/text.tsx +12 -0
  18. package/src/index.ts +28 -0
  19. package/src/lib/cn.ts +18 -0
  20. package/src/lib/touch-target.ts +9 -0
  21. package/src/nativewind-env.d.ts +1 -0
  22. package/src/variants/avatar-variants.ts +40 -0
  23. package/src/variants/badge-variants.ts +44 -0
  24. package/src/variants/button-variants.ts +59 -0
  25. package/src/variants/card-variants.ts +35 -0
  26. package/src/variants/checkbox-variants.ts +46 -0
  27. package/src/variants/index.ts +11 -0
  28. package/src/variants/input-variants.ts +34 -0
  29. package/src/variants/label-variants.ts +20 -0
  30. package/src/variants/separator-variants.ts +21 -0
  31. package/src/variants/skeleton-variants.ts +17 -0
  32. package/src/variants/switch-variants.ts +33 -0
  33. package/src/variants/text-variants.ts +27 -0
package/README.md ADDED
@@ -0,0 +1,117 @@
1
+ # @atlure/ui
2
+
3
+ React Native components for `atlure-paw`, styled with NativeWind against `@atlure/tokens`.
4
+
5
+ ## This package ships TypeScript source, not compiled output
6
+
7
+ `exports` points straight at `./src/index.ts` and `files` includes `src`. That is deliberate and it is
8
+ the one thing not to "fix".
9
+
10
+ NativeWind's `className` is a **Babel-time JSX transform** (`jsxImportSource: "nativewind"`). If this
11
+ package were precompiled — by `react-native-builder-bob`, `tsup` or plain `tsc` — the JSX would be
12
+ frozen against `react/jsx-runtime`, `className` would become a prop nobody reads, and every component
13
+ would render unstyled with no error to explain why. The consuming app's Babel pipeline has to see the
14
+ original JSX.
15
+
16
+ ## Consumer setup
17
+
18
+ Two steps, both required.
19
+
20
+ **1. Add this package's source to your Tailwind `content` array.** Classes live in files inside
21
+ `node_modules`, so Tailwind will not find them otherwise and every utility used only by this package
22
+ will be missing from the compiled output:
23
+
24
+ ```js
25
+ module.exports = {
26
+ presets: [require("nativewind/preset"), require("@atlure/tailwind-preset")],
27
+ content: ["./app/**/*.{ts,tsx}", "./node_modules/@atlure/ui/src/**/*.{ts,tsx}"],
28
+ };
29
+ ```
30
+
31
+ The NativeWind preset must come first.
32
+
33
+ **2. Let Metro transpile this package.** Because it ships source, `node_modules/@atlure/ui` must not
34
+ be excluded from the Babel transform. In a default Expo project it already is not.
35
+
36
+ ## Usage
37
+
38
+ ```tsx
39
+ import { Avatar, Button, Card, CardContent, Text } from "@atlure/ui";
40
+
41
+ export function SitterCard({ sitter, onOpen, openLabel }) {
42
+ return (
43
+ <Card onPress={onOpen} accessibilityLabel={openLabel}>
44
+ <CardContent className="flex-row items-center gap-md">
45
+ <Avatar name={sitter.displayName} uri={sitter.avatarUrl} />
46
+ <Text variant="title">{sitter.displayName}</Text>
47
+ </CardContent>
48
+ </Card>
49
+ );
50
+ }
51
+ ```
52
+
53
+ Components: `Text`, `Button`, `Card` (`CardHeader`, `CardContent`, `CardFooter`), `Input`, `Label`,
54
+ `Badge`, `Avatar`, `Separator`, `Skeleton`, `Switch`, `Checkbox`.
55
+
56
+ Every user-facing string is a prop. This package contains no display copy, so i18n stays in the app.
57
+
58
+ ## Variant recipes are shared, render layers are not
59
+
60
+ The `cva` recipes live in `src/variants/` as standalone files with no React Native imports, and are
61
+ also exported from `@atlure/ui/variants`:
62
+
63
+ ```ts
64
+ import { buttonVariants, buttonLabelVariants } from "@atlure/ui/variants";
65
+ ```
66
+
67
+ `@atlure/ui-web` imports the same recipes and applies them to DOM elements, so a variant's spacing and
68
+ colour are defined once for both platforms and only the rendering differs. Consequently the class
69
+ strings are restricted to the **React Native-safe Tailwind subset**:
70
+
71
+ | Not allowed | Use instead |
72
+ |---|---|
73
+ | `space-x-*`, `space-y-*` | `gap-*` |
74
+ | `divide-*` | an explicit `Separator` |
75
+ | `grid`, `grid-cols-*` | `flex-row` / `flex-col` |
76
+ | descendant or sibling selectors | a class on the child |
77
+ | implicit row direction | `flex-row`, always — React Native defaults to column |
78
+
79
+ Values come from the token scales (`h-control-md`, `text-base`, `bg-primary`, `border-border/20`).
80
+ There is no raw hex anywhere in this package; to change a colour, edit `packages/tokens/src/tokens.ts`.
81
+
82
+ ## The barrel is generated
83
+
84
+ `src/index.ts` is emitted by `scripts/generate-barrel.mjs` from a directory walk of `src`, and is
85
+ committed. Run `pnpm build` after adding a component — hand-editing it is pointless, since the next
86
+ build overwrites it, and CI's `git diff --exit-code` fails if it is stale.
87
+
88
+ This exists because a hand-maintained barrel is a guaranteed merge conflict whenever two branches add
89
+ a component. Modules under a `hooks/` directory and any `utils.ts` are treated as internal and left
90
+ out of the public surface.
91
+
92
+ ## Accessibility
93
+
94
+ Every interactive component sets `accessibilityRole`, `accessibilityLabel` and `accessibilityState`,
95
+ **and** the matching `aria-*` alias. Both are needed: `react-native-web`, which Expo uses for the web
96
+ build, ignores `accessibilityState` in favour of `aria-*`, so state such as "checked" would silently
97
+ never be announced there.
98
+
99
+ Touch targets are guaranteed to reach 44px via `hitSlop`, computed from the `controlHeight` token
100
+ rather than hard-coded — `touchTargetHitSlop` pads a 36px or 40px control out to the minimum without
101
+ changing how it looks.
102
+
103
+ ## Tests
104
+
105
+ `pnpm test` runs Vitest with React Testing Library. `react-native` is aliased to `react-native-web`
106
+ so components render into jsdom and can be queried by role and accessible name.
107
+
108
+ `@atlure/tokens` is resolved to `../tokens/src` for typechecking and testing, via `paths` in
109
+ `tsconfig.json` and an alias in `vitest.config.ts`. Published consumers still get `dist` through that
110
+ package's `exports`; resolving to source here means this package's checks do not depend on tokens having
111
+ been built first, and can never read a stale `dist`.
112
+
113
+ What that proves: press handling, disabled and loading behaviour, controlled toggle state, change
114
+ propagation, accessible roles and names, and the barrel generator's output.
115
+
116
+ What it does **not** prove: that NativeWind resolves any of these class strings. That requires a device
117
+ or simulator — a green test run here says nothing about styling.
package/package.json ADDED
@@ -0,0 +1,78 @@
1
+ {
2
+ "name": "@atlure/ui",
3
+ "version": "0.2.0",
4
+ "description": "Atlure React Native component library: NativeWind primitives built on @atlure/tokens, shipped as untranspiled TypeScript source",
5
+ "license": "MIT",
6
+ "author": "David Moreira",
7
+ "repository": {
8
+ "type": "git",
9
+ "url": "git+https://github.com/simpxlify/atlure-ui.git",
10
+ "directory": "packages/ui"
11
+ },
12
+ "type": "module",
13
+ "sideEffects": false,
14
+ "main": "./src/index.ts",
15
+ "types": "./src/index.ts",
16
+ "exports": {
17
+ ".": {
18
+ "types": "./src/index.ts",
19
+ "default": "./src/index.ts"
20
+ },
21
+ "./variants": {
22
+ "types": "./src/variants/index.ts",
23
+ "default": "./src/variants/index.ts"
24
+ },
25
+ "./package.json": "./package.json"
26
+ },
27
+ "files": [
28
+ "src",
29
+ "!src/**/*.test.ts",
30
+ "!src/**/*.test.tsx",
31
+ "README.md"
32
+ ],
33
+ "publishConfig": {
34
+ "access": "public"
35
+ },
36
+ "dependencies": {
37
+ "class-variance-authority": "0.7.1",
38
+ "clsx": "2.1.1",
39
+ "tailwind-merge": "2.6.0",
40
+ "@atlure/tokens": "0.2.0"
41
+ },
42
+ "peerDependencies": {
43
+ "nativewind": "^4.2.6",
44
+ "react": ">=18.3.0",
45
+ "react-native": ">=0.81.0",
46
+ "react-native-reanimated": ">=3.16.0",
47
+ "react-native-safe-area-context": ">=4.10.0",
48
+ "tailwindcss": "^3.4.19"
49
+ },
50
+ "peerDependenciesMeta": {
51
+ "react-native-reanimated": {
52
+ "optional": true
53
+ },
54
+ "react-native-safe-area-context": {
55
+ "optional": true
56
+ }
57
+ },
58
+ "devDependencies": {
59
+ "@testing-library/jest-dom": "6.6.3",
60
+ "@testing-library/react": "16.3.0",
61
+ "@types/react": "19.1.8",
62
+ "jsdom": "26.1.0",
63
+ "nativewind": "4.2.6",
64
+ "react": "19.1.0",
65
+ "react-dom": "19.1.0",
66
+ "react-native": "0.81.4",
67
+ "react-native-web": "0.21.1",
68
+ "tailwindcss": "3.4.19",
69
+ "typescript": "5.9.3",
70
+ "vitest": "3.2.4"
71
+ },
72
+ "scripts": {
73
+ "generate:barrel": "node scripts/generate-barrel.mjs",
74
+ "build": "pnpm run generate:barrel",
75
+ "typecheck": "tsc -p tsconfig.json --noEmit",
76
+ "test": "vitest run"
77
+ }
78
+ }
@@ -0,0 +1,43 @@
1
+ import { Image, View, type ViewProps } from "react-native";
2
+
3
+ import { cn } from "../../lib/cn";
4
+ import {
5
+ avatarFallbackVariants,
6
+ avatarVariants,
7
+ type AvatarVariantProps,
8
+ } from "../../variants/avatar-variants";
9
+ import { Text } from "../text/text";
10
+ import { toInitials } from "./utils";
11
+
12
+ export interface AvatarProps extends ViewProps {
13
+ name: string;
14
+ uri?: string | null;
15
+ size?: NonNullable<AvatarVariantProps["size"]>;
16
+ hasRing?: boolean;
17
+ }
18
+
19
+ export function Avatar({
20
+ name,
21
+ uri = null,
22
+ size = "md",
23
+ hasRing = false,
24
+ className,
25
+ ...viewProps
26
+ }: AvatarProps) {
27
+ const initials = toInitials(name);
28
+
29
+ return (
30
+ <View
31
+ accessibilityRole="image"
32
+ accessibilityLabel={name}
33
+ className={cn(avatarVariants({ size, hasRing }), className)}
34
+ {...viewProps}
35
+ >
36
+ {uri ? (
37
+ <Image source={{ uri }} accessibilityLabel={name} className="h-full w-full" />
38
+ ) : (
39
+ <Text className={avatarFallbackVariants({ size })}>{initials}</Text>
40
+ )}
41
+ </View>
42
+ );
43
+ }
@@ -0,0 +1,12 @@
1
+ const MAX_INITIALS = 2;
2
+
3
+ export function toInitials(name: string): string {
4
+ return name
5
+ .trim()
6
+ .split(/\s+/)
7
+ .filter((namePart) => namePart.length > 0)
8
+ .slice(0, MAX_INITIALS)
9
+ .map((namePart) => namePart[0] ?? "")
10
+ .join("")
11
+ .toUpperCase();
12
+ }
@@ -0,0 +1,31 @@
1
+ import { View, type ViewProps } from "react-native";
2
+
3
+ import { cn } from "../../lib/cn";
4
+ import {
5
+ badgeLabelVariants,
6
+ badgeVariants,
7
+ type BadgeVariantProps,
8
+ } from "../../variants/badge-variants";
9
+ import { Text } from "../text/text";
10
+
11
+ export interface BadgeProps extends ViewProps {
12
+ label: string;
13
+ variant?: NonNullable<BadgeVariantProps["variant"]>;
14
+ size?: NonNullable<BadgeVariantProps["size"]>;
15
+ labelClassName?: string;
16
+ }
17
+
18
+ export function Badge({
19
+ label,
20
+ variant = "primary",
21
+ size = "sm",
22
+ className,
23
+ labelClassName,
24
+ ...viewProps
25
+ }: BadgeProps) {
26
+ return (
27
+ <View className={cn(badgeVariants({ variant, size }), className)} {...viewProps}>
28
+ <Text className={cn(badgeLabelVariants({ variant, size }), labelClassName)}>{label}</Text>
29
+ </View>
30
+ );
31
+ }
@@ -0,0 +1,47 @@
1
+ import { ActivityIndicator, Pressable } from "react-native";
2
+
3
+ import { cn } from "../../lib/cn";
4
+ import { touchTargetHitSlop } from "../../lib/touch-target";
5
+ import { buttonLabelVariants, buttonVariants } from "../../variants/button-variants";
6
+ import { Text } from "../text/text";
7
+ import type { ButtonProps } from "./types";
8
+
9
+ export function Button({
10
+ label,
11
+ variant = "primary",
12
+ size = "md",
13
+ isFullWidth = false,
14
+ isDisabled = false,
15
+ isLoading = false,
16
+ leadingIcon,
17
+ trailingIcon,
18
+ accessibilityLabel,
19
+ className,
20
+ labelClassName,
21
+ ...pressableProps
22
+ }: ButtonProps) {
23
+ const isPressBlocked = isDisabled || isLoading;
24
+ const hasAdornment = Boolean(leadingIcon ?? trailingIcon) || isLoading;
25
+
26
+ return (
27
+ <Pressable
28
+ accessibilityRole="button"
29
+ accessibilityLabel={accessibilityLabel ?? label}
30
+ accessibilityState={{ disabled: isPressBlocked, busy: isLoading }}
31
+ aria-disabled={isPressBlocked}
32
+ aria-busy={isLoading}
33
+ disabled={isPressBlocked}
34
+ hitSlop={touchTargetHitSlop(size)}
35
+ className={cn(
36
+ buttonVariants({ variant, size, isFullWidth, isDisabled: isPressBlocked }),
37
+ hasAdornment && "gap-sm",
38
+ className,
39
+ )}
40
+ {...pressableProps}
41
+ >
42
+ {isLoading ? <ActivityIndicator size="small" /> : leadingIcon}
43
+ <Text className={cn(buttonLabelVariants({ variant, size }), labelClassName)}>{label}</Text>
44
+ {!isLoading && trailingIcon}
45
+ </Pressable>
46
+ );
47
+ }
@@ -0,0 +1,21 @@
1
+ import type { ReactNode } from "react";
2
+ import type { PressableProps } from "react-native";
3
+
4
+ import type { ControlSize } from "../../lib/touch-target";
5
+ import type { ButtonVariantProps } from "../../variants/button-variants";
6
+
7
+ export type ButtonSize = Extract<ControlSize, "sm" | "md" | "lg">;
8
+
9
+ export interface ButtonProps
10
+ extends Omit<PressableProps, "children" | "disabled" | "accessibilityState"> {
11
+ label: string;
12
+ variant?: NonNullable<ButtonVariantProps["variant"]>;
13
+ size?: ButtonSize;
14
+ isFullWidth?: boolean;
15
+ isDisabled?: boolean;
16
+ isLoading?: boolean;
17
+ leadingIcon?: ReactNode;
18
+ trailingIcon?: ReactNode;
19
+ className?: string;
20
+ labelClassName?: string;
21
+ }
@@ -0,0 +1,20 @@
1
+ import { View, type ViewProps } from "react-native";
2
+
3
+ import { cn } from "../../lib/cn";
4
+ import { cardSectionVariants } from "../../variants/card-variants";
5
+
6
+ export interface CardSectionProps extends ViewProps {}
7
+
8
+ export function CardHeader({ className, ...viewProps }: CardSectionProps) {
9
+ return <View className={cn(cardSectionVariants({ section: "header" }), className)} {...viewProps} />;
10
+ }
11
+
12
+ export function CardContent({ className, ...viewProps }: CardSectionProps) {
13
+ return (
14
+ <View className={cn(cardSectionVariants({ section: "content" }), className)} {...viewProps} />
15
+ );
16
+ }
17
+
18
+ export function CardFooter({ className, ...viewProps }: CardSectionProps) {
19
+ return <View className={cn(cardSectionVariants({ section: "footer" }), className)} {...viewProps} />;
20
+ }
@@ -0,0 +1,45 @@
1
+ import { Pressable, View, type ViewProps } from "react-native";
2
+
3
+ import { cn } from "../../lib/cn";
4
+ import { cardVariants, type CardVariantProps } from "../../variants/card-variants";
5
+
6
+ export interface CardProps extends ViewProps, Pick<CardVariantProps, "variant"> {
7
+ onPress?: () => void;
8
+ accessibilityLabel?: string;
9
+ isDisabled?: boolean;
10
+ }
11
+
12
+ export function Card({
13
+ variant,
14
+ onPress,
15
+ accessibilityLabel,
16
+ isDisabled = false,
17
+ className,
18
+ children,
19
+ ...viewProps
20
+ }: CardProps) {
21
+ const cardClassName = cn(cardVariants({ variant, isPressable: Boolean(onPress) }), className);
22
+
23
+ if (!onPress) {
24
+ return (
25
+ <View className={cardClassName} {...viewProps}>
26
+ {children}
27
+ </View>
28
+ );
29
+ }
30
+
31
+ return (
32
+ <Pressable
33
+ accessibilityRole="button"
34
+ accessibilityLabel={accessibilityLabel}
35
+ accessibilityState={{ disabled: isDisabled }}
36
+ aria-disabled={isDisabled}
37
+ disabled={isDisabled}
38
+ onPress={onPress}
39
+ className={cardClassName}
40
+ {...viewProps}
41
+ >
42
+ {children}
43
+ </Pressable>
44
+ );
45
+ }
@@ -0,0 +1,50 @@
1
+ import { Pressable, type PressableProps, View } from "react-native";
2
+
3
+ import { cn } from "../../lib/cn";
4
+ import { touchTargetHitSlop } from "../../lib/touch-target";
5
+ import {
6
+ checkboxBoxVariants,
7
+ checkboxIndicatorVariants,
8
+ checkboxRowVariants,
9
+ } from "../../variants/checkbox-variants";
10
+ import { Label } from "../label/label";
11
+
12
+ export interface CheckboxProps
13
+ extends Omit<PressableProps, "children" | "disabled" | "onPress" | "accessibilityState"> {
14
+ isChecked: boolean;
15
+ onValueChange: (isChecked: boolean) => void;
16
+ label?: string;
17
+ isDisabled?: boolean;
18
+ isInvalid?: boolean;
19
+ }
20
+
21
+ export function Checkbox({
22
+ isChecked,
23
+ onValueChange,
24
+ label,
25
+ isDisabled = false,
26
+ isInvalid = false,
27
+ accessibilityLabel,
28
+ className,
29
+ ...pressableProps
30
+ }: CheckboxProps) {
31
+ return (
32
+ <Pressable
33
+ accessibilityRole="checkbox"
34
+ accessibilityLabel={accessibilityLabel ?? label}
35
+ accessibilityState={{ checked: isChecked, disabled: isDisabled }}
36
+ aria-checked={isChecked}
37
+ aria-disabled={isDisabled}
38
+ disabled={isDisabled}
39
+ hitSlop={touchTargetHitSlop("md")}
40
+ onPress={() => onValueChange(!isChecked)}
41
+ className={cn(checkboxRowVariants({ isDisabled }), className)}
42
+ {...pressableProps}
43
+ >
44
+ <View className={checkboxBoxVariants({ isChecked, isInvalid })}>
45
+ <View className={checkboxIndicatorVariants({ isChecked })} />
46
+ </View>
47
+ {label ? <Label isDisabled={isDisabled} isInvalid={isInvalid}>{label}</Label> : null}
48
+ </Pressable>
49
+ );
50
+ }
@@ -0,0 +1,34 @@
1
+ import { TextInput, type TextInputProps } from "react-native";
2
+
3
+ import { cn } from "../../lib/cn";
4
+ import { touchTargetHitSlop } from "../../lib/touch-target";
5
+ import { inputVariants, type InputVariantProps } from "../../variants/input-variants";
6
+
7
+ export interface InputProps extends Omit<TextInputProps, "editable" | "multiline"> {
8
+ size?: NonNullable<InputVariantProps["size"]>;
9
+ isDisabled?: boolean;
10
+ isInvalid?: boolean;
11
+ isMultiline?: boolean;
12
+ }
13
+
14
+ export function Input({
15
+ size = "md",
16
+ isDisabled = false,
17
+ isInvalid = false,
18
+ isMultiline = false,
19
+ className,
20
+ ...textInputProps
21
+ }: InputProps) {
22
+ return (
23
+ <TextInput
24
+ accessibilityState={{ disabled: isDisabled }}
25
+ aria-disabled={isDisabled}
26
+ aria-invalid={isInvalid}
27
+ editable={!isDisabled}
28
+ multiline={isMultiline}
29
+ hitSlop={touchTargetHitSlop(size)}
30
+ className={cn(inputVariants({ size, isInvalid, isDisabled, isMultiline }), className)}
31
+ {...textInputProps}
32
+ />
33
+ );
34
+ }
@@ -0,0 +1,23 @@
1
+ import { cn } from "../../lib/cn";
2
+ import { labelVariants } from "../../variants/label-variants";
3
+ import { Text, type TextProps } from "../text/text";
4
+
5
+ export interface LabelProps extends Omit<TextProps, "variant" | "tone"> {
6
+ isDisabled?: boolean;
7
+ isInvalid?: boolean;
8
+ }
9
+
10
+ export function Label({
11
+ isDisabled = false,
12
+ isInvalid = false,
13
+ className,
14
+ ...textProps
15
+ }: LabelProps) {
16
+ return (
17
+ <Text
18
+ accessibilityRole="text"
19
+ className={cn(labelVariants({ isDisabled, isInvalid }), className)}
20
+ {...textProps}
21
+ />
22
+ );
23
+ }
@@ -0,0 +1,26 @@
1
+ import { View, type ViewProps } from "react-native";
2
+
3
+ import { cn } from "../../lib/cn";
4
+ import { separatorVariants, type SeparatorVariantProps } from "../../variants/separator-variants";
5
+
6
+ export interface SeparatorProps extends Omit<ViewProps, "children"> {
7
+ orientation?: NonNullable<SeparatorVariantProps["orientation"]>;
8
+ spacing?: NonNullable<SeparatorVariantProps["spacing"]>;
9
+ }
10
+
11
+ export function Separator({
12
+ orientation = "horizontal",
13
+ spacing = "none",
14
+ className,
15
+ ...viewProps
16
+ }: SeparatorProps) {
17
+ return (
18
+ <View
19
+ accessibilityRole="none"
20
+ accessibilityElementsHidden
21
+ importantForAccessibility="no-hide-descendants"
22
+ className={cn(separatorVariants({ orientation, spacing }), className)}
23
+ {...viewProps}
24
+ />
25
+ );
26
+ }
@@ -0,0 +1,40 @@
1
+ import { useEffect, useRef } from "react";
2
+ import { Animated, Easing } from "react-native";
3
+
4
+ const PULSE_DURATION_MS = 900;
5
+ const MIN_OPACITY = 0.4;
6
+ const MAX_OPACITY = 1;
7
+
8
+ export function usePulseOpacity(isEnabled: boolean): Animated.Value {
9
+ const opacity = useRef(new Animated.Value(MAX_OPACITY)).current;
10
+
11
+ useEffect(() => {
12
+ if (!isEnabled) {
13
+ opacity.setValue(MAX_OPACITY);
14
+ return;
15
+ }
16
+
17
+ const pulse = Animated.loop(
18
+ Animated.sequence([
19
+ Animated.timing(opacity, {
20
+ toValue: MIN_OPACITY,
21
+ duration: PULSE_DURATION_MS,
22
+ easing: Easing.inOut(Easing.ease),
23
+ useNativeDriver: true,
24
+ }),
25
+ Animated.timing(opacity, {
26
+ toValue: MAX_OPACITY,
27
+ duration: PULSE_DURATION_MS,
28
+ easing: Easing.inOut(Easing.ease),
29
+ useNativeDriver: true,
30
+ }),
31
+ ]),
32
+ );
33
+
34
+ pulse.start();
35
+
36
+ return () => pulse.stop();
37
+ }, [isEnabled, opacity]);
38
+
39
+ return opacity;
40
+ }
@@ -0,0 +1,34 @@
1
+ import { Animated, type ViewProps } from "react-native";
2
+
3
+ import { cn } from "../../lib/cn";
4
+ import { skeletonVariants, type SkeletonVariantProps } from "../../variants/skeleton-variants";
5
+ import { usePulseOpacity } from "./hooks/use-pulse-opacity";
6
+
7
+ export interface SkeletonProps extends Omit<ViewProps, "children"> {
8
+ shape?: NonNullable<SkeletonVariantProps["shape"]>;
9
+ accessibilityLabel: string;
10
+ isAnimated?: boolean;
11
+ }
12
+
13
+ export function Skeleton({
14
+ shape = "line",
15
+ accessibilityLabel,
16
+ isAnimated = true,
17
+ className,
18
+ style,
19
+ ...viewProps
20
+ }: SkeletonProps) {
21
+ const opacity = usePulseOpacity(isAnimated);
22
+
23
+ return (
24
+ <Animated.View
25
+ accessibilityRole="progressbar"
26
+ accessibilityLabel={accessibilityLabel}
27
+ accessibilityState={{ busy: true }}
28
+ aria-busy
29
+ className={cn(skeletonVariants({ shape }), className)}
30
+ style={[{ opacity }, style]}
31
+ {...viewProps}
32
+ />
33
+ );
34
+ }
@@ -0,0 +1,44 @@
1
+ import { Pressable, type PressableProps, View } from "react-native";
2
+
3
+ import { cn } from "../../lib/cn";
4
+ import { touchTargetHitSlop } from "../../lib/touch-target";
5
+ import { switchThumbVariants, switchTrackVariants } from "../../variants/switch-variants";
6
+ import { Label } from "../label/label";
7
+
8
+ export interface SwitchProps
9
+ extends Omit<PressableProps, "children" | "disabled" | "onPress" | "accessibilityState"> {
10
+ isChecked: boolean;
11
+ onValueChange: (isChecked: boolean) => void;
12
+ label?: string;
13
+ isDisabled?: boolean;
14
+ }
15
+
16
+ export function Switch({
17
+ isChecked,
18
+ onValueChange,
19
+ label,
20
+ isDisabled = false,
21
+ accessibilityLabel,
22
+ className,
23
+ ...pressableProps
24
+ }: SwitchProps) {
25
+ return (
26
+ <Pressable
27
+ accessibilityRole="switch"
28
+ accessibilityLabel={accessibilityLabel ?? label}
29
+ accessibilityState={{ checked: isChecked, disabled: isDisabled }}
30
+ aria-checked={isChecked}
31
+ aria-disabled={isDisabled}
32
+ disabled={isDisabled}
33
+ hitSlop={touchTargetHitSlop("md")}
34
+ onPress={() => onValueChange(!isChecked)}
35
+ className={cn("flex-row items-center gap-sm", className)}
36
+ {...pressableProps}
37
+ >
38
+ <View className={switchTrackVariants({ isChecked, isDisabled })}>
39
+ <View className={switchThumbVariants({ isChecked })} />
40
+ </View>
41
+ {label ? <Label isDisabled={isDisabled}>{label}</Label> : null}
42
+ </Pressable>
43
+ );
44
+ }
@@ -0,0 +1,12 @@
1
+ import { Text as ReactNativeText, type TextProps as ReactNativeTextProps } from "react-native";
2
+
3
+ import { cn } from "../../lib/cn";
4
+ import { textVariants, type TextVariantProps } from "../../variants/text-variants";
5
+
6
+ export interface TextProps extends ReactNativeTextProps, TextVariantProps {}
7
+
8
+ export function Text({ variant, tone, className, ...textProps }: TextProps) {
9
+ return (
10
+ <ReactNativeText className={cn(textVariants({ variant, tone }), className)} {...textProps} />
11
+ );
12
+ }
package/src/index.ts ADDED
@@ -0,0 +1,28 @@
1
+ /* atlure-generated-barrel-do-not-edit */
2
+
3
+ export * from "./components/avatar/avatar";
4
+ export * from "./components/badge/badge";
5
+ export * from "./components/button/button";
6
+ export * from "./components/button/types";
7
+ export * from "./components/card/card";
8
+ export * from "./components/card/card-section";
9
+ export * from "./components/checkbox/checkbox";
10
+ export * from "./components/input/input";
11
+ export * from "./components/label/label";
12
+ export * from "./components/separator/separator";
13
+ export * from "./components/skeleton/skeleton";
14
+ export * from "./components/switch/switch";
15
+ export * from "./components/text/text";
16
+ export * from "./lib/cn";
17
+ export * from "./lib/touch-target";
18
+ export * from "./variants/avatar-variants";
19
+ export * from "./variants/badge-variants";
20
+ export * from "./variants/button-variants";
21
+ export * from "./variants/card-variants";
22
+ export * from "./variants/checkbox-variants";
23
+ export * from "./variants/input-variants";
24
+ export * from "./variants/label-variants";
25
+ export * from "./variants/separator-variants";
26
+ export * from "./variants/skeleton-variants";
27
+ export * from "./variants/switch-variants";
28
+ export * from "./variants/text-variants";
package/src/lib/cn.ts ADDED
@@ -0,0 +1,18 @@
1
+ import { controlHeight } from "@atlure/tokens";
2
+ import { type ClassValue, clsx } from "clsx";
3
+ import { extendTailwindMerge } from "tailwind-merge";
4
+
5
+ const controlHeightClassValues = Object.keys(controlHeight).map((scaleKey) => `control-${scaleKey}`);
6
+
7
+ const twMerge = extendTailwindMerge({
8
+ extend: {
9
+ classGroups: {
10
+ h: [{ h: controlHeightClassValues }],
11
+ "min-h": [{ "min-h": controlHeightClassValues }],
12
+ },
13
+ },
14
+ });
15
+
16
+ export function cn(...classValues: ClassValue[]): string {
17
+ return twMerge(clsx(classValues));
18
+ }
@@ -0,0 +1,9 @@
1
+ import { controlHeight } from "@atlure/tokens";
2
+
3
+ export const MIN_TOUCH_TARGET_SIZE = 44;
4
+
5
+ export type ControlSize = keyof typeof controlHeight;
6
+
7
+ export function touchTargetHitSlop(size: ControlSize): number {
8
+ return Math.max(0, Math.ceil((MIN_TOUCH_TARGET_SIZE - controlHeight[size]) / 2));
9
+ }
@@ -0,0 +1 @@
1
+ /// <reference types="nativewind/types" />
@@ -0,0 +1,40 @@
1
+ import { cva, type VariantProps } from "class-variance-authority";
2
+
3
+ export const avatarVariants = cva(
4
+ "items-center justify-center overflow-hidden rounded-full bg-muted",
5
+ {
6
+ variants: {
7
+ size: {
8
+ sm: "h-8 w-8",
9
+ md: "h-10 w-10",
10
+ lg: "h-12 w-12",
11
+ xl: "h-16 w-16",
12
+ },
13
+ hasRing: {
14
+ true: "border-2 border-primary",
15
+ false: "",
16
+ },
17
+ },
18
+ defaultVariants: {
19
+ size: "md",
20
+ hasRing: false,
21
+ },
22
+ },
23
+ );
24
+
25
+ export const avatarFallbackVariants = cva("font-semibold uppercase text-muted-foreground", {
26
+ variants: {
27
+ size: {
28
+ sm: "text-xs",
29
+ md: "text-sm",
30
+ lg: "text-base",
31
+ xl: "text-xl",
32
+ },
33
+ },
34
+ defaultVariants: {
35
+ size: "md",
36
+ },
37
+ });
38
+
39
+ export type AvatarVariantProps = VariantProps<typeof avatarVariants>;
40
+ export type AvatarFallbackVariantProps = VariantProps<typeof avatarFallbackVariants>;
@@ -0,0 +1,44 @@
1
+ import { cva, type VariantProps } from "class-variance-authority";
2
+
3
+ export const badgeVariants = cva("flex-row items-center justify-center self-start rounded-full", {
4
+ variants: {
5
+ variant: {
6
+ primary: "bg-primary",
7
+ secondary: "bg-secondary",
8
+ outline: "border border-border/20 bg-transparent",
9
+ destructive: "bg-destructive",
10
+ muted: "bg-muted",
11
+ },
12
+ size: {
13
+ sm: "px-sm py-xs",
14
+ md: "px-md py-xs",
15
+ },
16
+ },
17
+ defaultVariants: {
18
+ variant: "primary",
19
+ size: "sm",
20
+ },
21
+ });
22
+
23
+ export const badgeLabelVariants = cva("font-medium", {
24
+ variants: {
25
+ variant: {
26
+ primary: "text-primary-foreground",
27
+ secondary: "text-secondary-foreground",
28
+ outline: "text-foreground",
29
+ destructive: "text-destructive-foreground",
30
+ muted: "text-muted-foreground",
31
+ },
32
+ size: {
33
+ sm: "text-xs",
34
+ md: "text-sm",
35
+ },
36
+ },
37
+ defaultVariants: {
38
+ variant: "primary",
39
+ size: "sm",
40
+ },
41
+ });
42
+
43
+ export type BadgeVariantProps = VariantProps<typeof badgeVariants>;
44
+ export type BadgeLabelVariantProps = VariantProps<typeof badgeLabelVariants>;
@@ -0,0 +1,59 @@
1
+ import { cva, type VariantProps } from "class-variance-authority";
2
+
3
+ export const buttonVariants = cva(
4
+ "flex-row items-center justify-center rounded-md border border-transparent",
5
+ {
6
+ variants: {
7
+ variant: {
8
+ primary: "bg-primary",
9
+ secondary: "bg-secondary",
10
+ outline: "border-border/20 bg-transparent",
11
+ ghost: "bg-transparent",
12
+ destructive: "bg-destructive",
13
+ },
14
+ size: {
15
+ sm: "h-control-sm px-sm",
16
+ md: "h-control-md px-md",
17
+ lg: "h-control-lg px-lg",
18
+ },
19
+ isFullWidth: {
20
+ true: "w-full",
21
+ false: "self-start",
22
+ },
23
+ isDisabled: {
24
+ true: "opacity-50",
25
+ false: "",
26
+ },
27
+ },
28
+ defaultVariants: {
29
+ variant: "primary",
30
+ size: "md",
31
+ isFullWidth: false,
32
+ isDisabled: false,
33
+ },
34
+ },
35
+ );
36
+
37
+ export const buttonLabelVariants = cva("text-center font-medium", {
38
+ variants: {
39
+ variant: {
40
+ primary: "text-primary-foreground",
41
+ secondary: "text-secondary-foreground",
42
+ outline: "text-foreground",
43
+ ghost: "text-foreground",
44
+ destructive: "text-destructive-foreground",
45
+ },
46
+ size: {
47
+ sm: "text-sm",
48
+ md: "text-base",
49
+ lg: "text-base",
50
+ },
51
+ },
52
+ defaultVariants: {
53
+ variant: "primary",
54
+ size: "md",
55
+ },
56
+ });
57
+
58
+ export type ButtonVariantProps = VariantProps<typeof buttonVariants>;
59
+ export type ButtonLabelVariantProps = VariantProps<typeof buttonLabelVariants>;
@@ -0,0 +1,35 @@
1
+ import { cva, type VariantProps } from "class-variance-authority";
2
+
3
+ export const cardVariants = cva("overflow-hidden rounded-lg bg-card", {
4
+ variants: {
5
+ variant: {
6
+ outlined: "border border-border/20",
7
+ elevated: "border border-border/10 shadow-md",
8
+ flat: "border border-transparent",
9
+ },
10
+ isPressable: {
11
+ true: "min-h-control-lg",
12
+ false: "",
13
+ },
14
+ },
15
+ defaultVariants: {
16
+ variant: "outlined",
17
+ isPressable: false,
18
+ },
19
+ });
20
+
21
+ export const cardSectionVariants = cva("", {
22
+ variants: {
23
+ section: {
24
+ header: "gap-xs px-md pb-sm pt-md",
25
+ content: "gap-sm px-md py-sm",
26
+ footer: "flex-row items-center justify-between px-md pb-md pt-sm",
27
+ },
28
+ },
29
+ defaultVariants: {
30
+ section: "content",
31
+ },
32
+ });
33
+
34
+ export type CardVariantProps = VariantProps<typeof cardVariants>;
35
+ export type CardSectionVariantProps = VariantProps<typeof cardSectionVariants>;
@@ -0,0 +1,46 @@
1
+ import { cva, type VariantProps } from "class-variance-authority";
2
+
3
+ export const checkboxRowVariants = cva("flex-row items-center gap-sm", {
4
+ variants: {
5
+ isDisabled: {
6
+ true: "opacity-50",
7
+ false: "",
8
+ },
9
+ },
10
+ defaultVariants: {
11
+ isDisabled: false,
12
+ },
13
+ });
14
+
15
+ export const checkboxBoxVariants = cva("h-6 w-6 items-center justify-center rounded-sm border-2", {
16
+ variants: {
17
+ isChecked: {
18
+ true: "border-primary bg-primary",
19
+ false: "border-border/20 bg-input-background",
20
+ },
21
+ isInvalid: {
22
+ true: "border-destructive",
23
+ false: "",
24
+ },
25
+ },
26
+ defaultVariants: {
27
+ isChecked: false,
28
+ isInvalid: false,
29
+ },
30
+ });
31
+
32
+ export const checkboxIndicatorVariants = cva("h-3 w-3 rounded-sm", {
33
+ variants: {
34
+ isChecked: {
35
+ true: "bg-primary-foreground",
36
+ false: "bg-transparent",
37
+ },
38
+ },
39
+ defaultVariants: {
40
+ isChecked: false,
41
+ },
42
+ });
43
+
44
+ export type CheckboxRowVariantProps = VariantProps<typeof checkboxRowVariants>;
45
+ export type CheckboxBoxVariantProps = VariantProps<typeof checkboxBoxVariants>;
46
+ export type CheckboxIndicatorVariantProps = VariantProps<typeof checkboxIndicatorVariants>;
@@ -0,0 +1,11 @@
1
+ export * from "./avatar-variants";
2
+ export * from "./badge-variants";
3
+ export * from "./button-variants";
4
+ export * from "./card-variants";
5
+ export * from "./checkbox-variants";
6
+ export * from "./input-variants";
7
+ export * from "./label-variants";
8
+ export * from "./separator-variants";
9
+ export * from "./skeleton-variants";
10
+ export * from "./switch-variants";
11
+ export * from "./text-variants";
@@ -0,0 +1,34 @@
1
+ import { cva, type VariantProps } from "class-variance-authority";
2
+
3
+ export const inputVariants = cva(
4
+ "w-full rounded-md border bg-input-background px-md text-base text-foreground",
5
+ {
6
+ variants: {
7
+ size: {
8
+ sm: "h-control-sm",
9
+ md: "h-control-md",
10
+ lg: "h-control-lg",
11
+ },
12
+ isInvalid: {
13
+ true: "border-destructive",
14
+ false: "border-border/20",
15
+ },
16
+ isDisabled: {
17
+ true: "opacity-50",
18
+ false: "",
19
+ },
20
+ isMultiline: {
21
+ true: "h-auto min-h-control-lg py-sm",
22
+ false: "",
23
+ },
24
+ },
25
+ defaultVariants: {
26
+ size: "md",
27
+ isInvalid: false,
28
+ isDisabled: false,
29
+ isMultiline: false,
30
+ },
31
+ },
32
+ );
33
+
34
+ export type InputVariantProps = VariantProps<typeof inputVariants>;
@@ -0,0 +1,20 @@
1
+ import { cva, type VariantProps } from "class-variance-authority";
2
+
3
+ export const labelVariants = cva("text-sm font-medium text-foreground", {
4
+ variants: {
5
+ isDisabled: {
6
+ true: "opacity-50",
7
+ false: "",
8
+ },
9
+ isInvalid: {
10
+ true: "text-destructive",
11
+ false: "",
12
+ },
13
+ },
14
+ defaultVariants: {
15
+ isDisabled: false,
16
+ isInvalid: false,
17
+ },
18
+ });
19
+
20
+ export type LabelVariantProps = VariantProps<typeof labelVariants>;
@@ -0,0 +1,21 @@
1
+ import { cva, type VariantProps } from "class-variance-authority";
2
+
3
+ export const separatorVariants = cva("bg-border/20", {
4
+ variants: {
5
+ orientation: {
6
+ horizontal: "h-px w-full",
7
+ vertical: "w-px self-stretch",
8
+ },
9
+ spacing: {
10
+ none: "",
11
+ sm: "my-sm",
12
+ md: "my-md",
13
+ },
14
+ },
15
+ defaultVariants: {
16
+ orientation: "horizontal",
17
+ spacing: "none",
18
+ },
19
+ });
20
+
21
+ export type SeparatorVariantProps = VariantProps<typeof separatorVariants>;
@@ -0,0 +1,17 @@
1
+ import { cva, type VariantProps } from "class-variance-authority";
2
+
3
+ export const skeletonVariants = cva("bg-muted", {
4
+ variants: {
5
+ shape: {
6
+ line: "h-4 w-full rounded-sm",
7
+ title: "h-6 w-2/3 rounded-sm",
8
+ block: "h-24 w-full rounded-lg",
9
+ circle: "h-10 w-10 rounded-full",
10
+ },
11
+ },
12
+ defaultVariants: {
13
+ shape: "line",
14
+ },
15
+ });
16
+
17
+ export type SkeletonVariantProps = VariantProps<typeof skeletonVariants>;
@@ -0,0 +1,33 @@
1
+ import { cva, type VariantProps } from "class-variance-authority";
2
+
3
+ export const switchTrackVariants = cva("h-7 w-12 flex-row items-center rounded-full p-xs", {
4
+ variants: {
5
+ isChecked: {
6
+ true: "justify-end bg-primary",
7
+ false: "justify-start bg-muted",
8
+ },
9
+ isDisabled: {
10
+ true: "opacity-50",
11
+ false: "",
12
+ },
13
+ },
14
+ defaultVariants: {
15
+ isChecked: false,
16
+ isDisabled: false,
17
+ },
18
+ });
19
+
20
+ export const switchThumbVariants = cva("h-5 w-5 rounded-full", {
21
+ variants: {
22
+ isChecked: {
23
+ true: "bg-primary-foreground",
24
+ false: "bg-background",
25
+ },
26
+ },
27
+ defaultVariants: {
28
+ isChecked: false,
29
+ },
30
+ });
31
+
32
+ export type SwitchTrackVariantProps = VariantProps<typeof switchTrackVariants>;
33
+ export type SwitchThumbVariantProps = VariantProps<typeof switchThumbVariants>;
@@ -0,0 +1,27 @@
1
+ import { cva, type VariantProps } from "class-variance-authority";
2
+
3
+ export const textVariants = cva("text-foreground", {
4
+ variants: {
5
+ variant: {
6
+ display: "text-3xl font-bold",
7
+ heading: "text-2xl font-bold",
8
+ title: "text-xl font-semibold",
9
+ subtitle: "text-lg font-medium",
10
+ body: "text-base font-normal",
11
+ caption: "text-sm font-normal",
12
+ overline: "text-xs font-medium uppercase",
13
+ },
14
+ tone: {
15
+ default: "text-foreground",
16
+ muted: "text-muted-foreground",
17
+ primary: "text-primary",
18
+ destructive: "text-destructive",
19
+ },
20
+ },
21
+ defaultVariants: {
22
+ variant: "body",
23
+ tone: "default",
24
+ },
25
+ });
26
+
27
+ export type TextVariantProps = VariantProps<typeof textVariants>;