panelui-native 0.2.0 → 0.5.1
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 +258 -0
- package/lib/module/components/accordion/index.js +312 -0
- package/lib/module/components/accordion/index.js.map +1 -0
- package/lib/module/components/alert/index.js +106 -13
- package/lib/module/components/alert/index.js.map +1 -1
- package/lib/module/components/avatar/index.js +58 -17
- package/lib/module/components/avatar/index.js.map +1 -1
- package/lib/module/components/badge/index.js +29 -8
- package/lib/module/components/badge/index.js.map +1 -1
- package/lib/module/components/button/index.js +87 -26
- package/lib/module/components/button/index.js.map +1 -1
- package/lib/module/components/checkbox/index.js +59 -18
- package/lib/module/components/checkbox/index.js.map +1 -1
- package/lib/module/components/empty-state/index.js +178 -0
- package/lib/module/components/empty-state/index.js.map +1 -0
- package/lib/module/components/frame/index.js +64 -13
- package/lib/module/components/frame/index.js.map +1 -1
- package/lib/module/components/input-group/index.js +132 -0
- package/lib/module/components/input-group/index.js.map +1 -0
- package/lib/module/components/label/index.js +117 -0
- package/lib/module/components/label/index.js.map +1 -0
- package/lib/module/components/progress/index.js +138 -0
- package/lib/module/components/progress/index.js.map +1 -0
- package/lib/module/components/steps/index.js +354 -0
- package/lib/module/components/steps/index.js.map +1 -0
- package/lib/module/components/tabs/index.js +9 -3
- package/lib/module/components/tabs/index.js.map +1 -1
- package/lib/module/components/timeline/index.js +633 -0
- package/lib/module/components/timeline/index.js.map +1 -0
- package/lib/module/components/toast/index.js +569 -0
- package/lib/module/components/toast/index.js.map +1 -0
- package/lib/module/components/toast/toast-store.js +81 -0
- package/lib/module/components/toast/toast-store.js.map +1 -0
- package/lib/module/components/typography/index.js +126 -0
- package/lib/module/components/typography/index.js.map +1 -0
- package/lib/module/icons/index.js +590 -8
- package/lib/module/icons/index.js.map +1 -1
- package/lib/module/index.js +11 -2
- package/lib/module/index.js.map +1 -1
- package/lib/module/providers/panel-ui-provider.js +2 -1
- package/lib/module/providers/panel-ui-provider.js.map +1 -1
- package/lib/module/theme/use-theme.js +90 -3
- package/lib/module/theme/use-theme.js.map +1 -1
- package/lib/typescript/src/components/accordion/index.d.ts +56 -0
- package/lib/typescript/src/components/accordion/index.d.ts.map +1 -0
- package/lib/typescript/src/components/alert/index.d.ts +56 -4
- package/lib/typescript/src/components/alert/index.d.ts.map +1 -1
- package/lib/typescript/src/components/avatar/index.d.ts +8 -1
- package/lib/typescript/src/components/avatar/index.d.ts.map +1 -1
- package/lib/typescript/src/components/badge/index.d.ts +38 -0
- package/lib/typescript/src/components/badge/index.d.ts.map +1 -1
- package/lib/typescript/src/components/button/index.d.ts +57 -0
- package/lib/typescript/src/components/button/index.d.ts.map +1 -1
- package/lib/typescript/src/components/checkbox/index.d.ts +50 -0
- package/lib/typescript/src/components/checkbox/index.d.ts.map +1 -1
- package/lib/typescript/src/components/empty-state/index.d.ts +73 -0
- package/lib/typescript/src/components/empty-state/index.d.ts.map +1 -0
- package/lib/typescript/src/components/frame/index.d.ts +39 -5
- package/lib/typescript/src/components/frame/index.d.ts.map +1 -1
- package/lib/typescript/src/components/input-group/index.d.ts +34 -0
- package/lib/typescript/src/components/input-group/index.d.ts.map +1 -0
- package/lib/typescript/src/components/label/index.d.ts +28 -0
- package/lib/typescript/src/components/label/index.d.ts.map +1 -0
- package/lib/typescript/src/components/progress/index.d.ts +132 -0
- package/lib/typescript/src/components/progress/index.d.ts.map +1 -0
- package/lib/typescript/src/components/steps/index.d.ts +62 -0
- package/lib/typescript/src/components/steps/index.d.ts.map +1 -0
- package/lib/typescript/src/components/tabs/index.d.ts.map +1 -1
- package/lib/typescript/src/components/timeline/index.d.ts +96 -0
- package/lib/typescript/src/components/timeline/index.d.ts.map +1 -0
- package/lib/typescript/src/components/toast/index.d.ts +176 -0
- package/lib/typescript/src/components/toast/index.d.ts.map +1 -0
- package/lib/typescript/src/components/toast/toast-store.d.ts +65 -0
- package/lib/typescript/src/components/toast/toast-store.d.ts.map +1 -0
- package/lib/typescript/src/components/typography/index.d.ts +77 -0
- package/lib/typescript/src/components/typography/index.d.ts.map +1 -0
- package/lib/typescript/src/icons/index.d.ts +60 -0
- package/lib/typescript/src/icons/index.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +13 -4
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/providers/panel-ui-provider.d.ts.map +1 -1
- package/lib/typescript/src/theme/use-theme.d.ts +64 -4
- package/lib/typescript/src/theme/use-theme.d.ts.map +1 -1
- package/package.json +22 -4
- package/src/components/accordion/index.tsx +344 -0
- package/src/components/alert/index.tsx +102 -16
- package/src/components/avatar/index.tsx +68 -20
- package/src/components/badge/index.tsx +38 -9
- package/src/components/button/index.tsx +102 -19
- package/src/components/checkbox/index.tsx +64 -12
- package/src/components/empty-state/index.tsx +136 -0
- package/src/components/frame/index.tsx +101 -31
- package/src/components/input-group/index.tsx +164 -0
- package/src/components/label/index.tsx +128 -0
- package/src/components/progress/index.tsx +147 -0
- package/src/components/steps/index.tsx +343 -0
- package/src/components/tabs/index.tsx +10 -2
- package/src/components/timeline/index.tsx +456 -0
- package/src/components/toast/index.tsx +568 -0
- package/src/components/toast/toast-store.ts +120 -0
- package/src/components/typography/index.tsx +121 -0
- package/src/icons/index.tsx +423 -7
- package/src/index.ts +114 -4
- package/src/providers/panel-ui-provider.tsx +3 -0
- package/src/theme/use-theme.ts +119 -4
- package/theme.css +330 -7
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { forwardRef, useState } from 'react';
|
|
1
|
+
import { forwardRef, useState, type ReactNode } from 'react';
|
|
2
2
|
import {
|
|
3
3
|
Image,
|
|
4
4
|
View,
|
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
} from 'react-native';
|
|
9
9
|
import { tv, type VariantProps } from 'tailwind-variants';
|
|
10
10
|
import { Text } from '../../primitives/text';
|
|
11
|
+
import { cn } from '../../utils/cn';
|
|
11
12
|
|
|
12
13
|
const avatarVariants = tv({
|
|
13
14
|
slots: {
|
|
@@ -37,32 +38,79 @@ export interface AvatarProps extends ViewProps, VariantProps<typeof avatarVarian
|
|
|
37
38
|
imageProps?: Omit<ImageProps, 'source'>;
|
|
38
39
|
}
|
|
39
40
|
|
|
40
|
-
|
|
41
|
-
({ className, size, source, fallback, imageProps, ...props }, ref) => {
|
|
41
|
+
const AvatarRoot = forwardRef<View, AvatarProps>(
|
|
42
|
+
({ className, size, source, fallback, imageProps, children, ...props }, ref) => {
|
|
42
43
|
const [errored, setErrored] = useState(false);
|
|
43
44
|
const { root, image, fallback: fallbackSlot } = avatarVariants({ size });
|
|
44
45
|
const showImage = !!source && !errored;
|
|
45
46
|
|
|
47
|
+
const face = showImage ? (
|
|
48
|
+
<Image
|
|
49
|
+
source={source}
|
|
50
|
+
onError={() => setErrored(true)}
|
|
51
|
+
className={image()}
|
|
52
|
+
{...imageProps}
|
|
53
|
+
/>
|
|
54
|
+
) : (
|
|
55
|
+
<Text className={fallbackSlot()}>{fallback ?? '?'}</Text>
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
// The plain avatar is a single clipped node.
|
|
59
|
+
if (!children) {
|
|
60
|
+
return (
|
|
61
|
+
<View
|
|
62
|
+
ref={ref}
|
|
63
|
+
accessibilityRole="image"
|
|
64
|
+
className={root({ className })}
|
|
65
|
+
{...props}
|
|
66
|
+
>
|
|
67
|
+
{face}
|
|
68
|
+
</View>
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// With an overlay it needs two: the face keeps overflow-hidden to round
|
|
73
|
+
// the image, which would otherwise cut a corner badge in half, so the
|
|
74
|
+
// overlay hangs off an unclipped wrapper around it.
|
|
46
75
|
return (
|
|
47
|
-
<View
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
{
|
|
52
|
-
>
|
|
53
|
-
{showImage ? (
|
|
54
|
-
<Image
|
|
55
|
-
source={source}
|
|
56
|
-
onError={() => setErrored(true)}
|
|
57
|
-
className={image()}
|
|
58
|
-
{...imageProps}
|
|
59
|
-
/>
|
|
60
|
-
) : (
|
|
61
|
-
<Text className={fallbackSlot()}>{fallback ?? '?'}</Text>
|
|
62
|
-
)}
|
|
76
|
+
<View ref={ref} className={cn('self-start', className)} {...props}>
|
|
77
|
+
<View accessibilityRole="image" className={root()}>
|
|
78
|
+
{face}
|
|
79
|
+
</View>
|
|
80
|
+
{children}
|
|
63
81
|
</View>
|
|
64
82
|
);
|
|
65
83
|
}
|
|
66
84
|
);
|
|
85
|
+
AvatarRoot.displayName = 'Avatar';
|
|
67
86
|
|
|
68
|
-
|
|
87
|
+
export interface AvatarBadgeProps extends ViewProps {
|
|
88
|
+
className?: string;
|
|
89
|
+
children?: ReactNode;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Overlay pinned to the avatar's top-right — an unread count, a presence dot.
|
|
94
|
+
*
|
|
95
|
+
* The ring is `border-background` so the badge separates from the image
|
|
96
|
+
* whatever surface the avatar sits on.
|
|
97
|
+
*/
|
|
98
|
+
const AvatarBadge = forwardRef<View, AvatarBadgeProps>(
|
|
99
|
+
({ className, children, ...props }, ref) => (
|
|
100
|
+
<View
|
|
101
|
+
ref={ref}
|
|
102
|
+
className={cn(
|
|
103
|
+
'absolute -right-1 -top-1 rounded-full border-2 border-background',
|
|
104
|
+
className
|
|
105
|
+
)}
|
|
106
|
+
{...props}
|
|
107
|
+
>
|
|
108
|
+
{children}
|
|
109
|
+
</View>
|
|
110
|
+
)
|
|
111
|
+
);
|
|
112
|
+
AvatarBadge.displayName = 'Avatar.Badge';
|
|
113
|
+
|
|
114
|
+
export const Avatar = Object.assign(AvatarRoot, {
|
|
115
|
+
Badge: AvatarBadge,
|
|
116
|
+
});
|
|
@@ -9,39 +9,68 @@ const badgeVariants = tv({
|
|
|
9
9
|
label: 'text-xs font-medium',
|
|
10
10
|
},
|
|
11
11
|
variants: {
|
|
12
|
+
shape: {
|
|
13
|
+
default: {},
|
|
14
|
+
/** A bare status dot, no label. */
|
|
15
|
+
dot: { root: 'h-2 w-2 rounded-full px-0 py-0' },
|
|
16
|
+
/** Circular, for a count sitting over an avatar or an icon. */
|
|
17
|
+
count: { root: 'h-5 min-w-5 rounded-full px-1.5 py-0' },
|
|
18
|
+
},
|
|
12
19
|
variant: {
|
|
13
20
|
default: { root: 'bg-primary', label: 'text-primary-foreground' },
|
|
14
21
|
secondary: { root: 'bg-secondary', label: 'text-secondary-foreground' },
|
|
15
22
|
outline: { root: 'border-border bg-transparent', label: 'text-foreground' },
|
|
16
23
|
destructive: { root: 'bg-destructive', label: 'text-white' },
|
|
17
|
-
success: { root: 'bg-success
|
|
18
|
-
warning: { root: 'bg-warning
|
|
19
|
-
info: { root: 'bg-info
|
|
24
|
+
success: { root: 'bg-success-subtle', label: 'text-success-foreground' },
|
|
25
|
+
warning: { root: 'bg-warning-subtle', label: 'text-warning-foreground' },
|
|
26
|
+
info: { root: 'bg-info-subtle', label: 'text-info-foreground' },
|
|
20
27
|
},
|
|
21
28
|
},
|
|
22
29
|
defaultVariants: {
|
|
23
30
|
variant: 'default',
|
|
31
|
+
shape: 'default',
|
|
24
32
|
},
|
|
25
33
|
});
|
|
26
34
|
|
|
35
|
+
/** Counts past this render as "99+" so the badge keeps its shape. */
|
|
36
|
+
const MAX_COUNT = 99;
|
|
37
|
+
|
|
27
38
|
export interface BadgeProps
|
|
28
39
|
extends ViewProps,
|
|
29
40
|
VariantProps<typeof badgeVariants> {
|
|
30
41
|
children?: ReactNode;
|
|
31
42
|
className?: string;
|
|
32
43
|
labelClassName?: string;
|
|
44
|
+
/**
|
|
45
|
+
* Renders a number instead of children, clamped to "99+". Implies the
|
|
46
|
+
* `count` shape unless you set one.
|
|
47
|
+
*/
|
|
48
|
+
count?: number;
|
|
33
49
|
}
|
|
34
50
|
|
|
35
51
|
export const Badge = forwardRef<View, BadgeProps>(
|
|
36
|
-
({ children, className, labelClassName, variant, ...props }, ref) => {
|
|
37
|
-
const
|
|
52
|
+
({ children, className, labelClassName, variant, shape, count, ...props }, ref) => {
|
|
53
|
+
const resolvedShape = shape ?? (count !== undefined ? 'count' : 'default');
|
|
54
|
+
const { root, label } = badgeVariants({ variant, shape: resolvedShape });
|
|
55
|
+
|
|
56
|
+
const content =
|
|
57
|
+
count !== undefined
|
|
58
|
+
? `${Math.min(count, MAX_COUNT)}${count > MAX_COUNT ? '+' : ''}`
|
|
59
|
+
: children;
|
|
38
60
|
|
|
39
61
|
return (
|
|
40
|
-
<View
|
|
41
|
-
{
|
|
42
|
-
|
|
62
|
+
<View
|
|
63
|
+
ref={ref}
|
|
64
|
+
accessibilityLabel={
|
|
65
|
+
count !== undefined ? `${count} unread` : undefined
|
|
66
|
+
}
|
|
67
|
+
className={root({ className })}
|
|
68
|
+
{...props}
|
|
69
|
+
>
|
|
70
|
+
{resolvedShape === 'dot' ? null : typeof content === 'string' ? (
|
|
71
|
+
<Text className={label({ className: labelClassName })}>{content}</Text>
|
|
43
72
|
) : (
|
|
44
|
-
|
|
73
|
+
content
|
|
45
74
|
)}
|
|
46
75
|
</View>
|
|
47
76
|
);
|
|
@@ -1,38 +1,53 @@
|
|
|
1
|
-
import { forwardRef,
|
|
1
|
+
import { forwardRef, type ReactNode } from 'react';
|
|
2
2
|
import type { View } from 'react-native';
|
|
3
3
|
import { tv, type VariantProps } from 'tailwind-variants';
|
|
4
|
+
import { useCSSVariable } from 'uniwind';
|
|
4
5
|
import {
|
|
5
6
|
AnimatedPressable,
|
|
6
7
|
type AnimatedPressableProps,
|
|
7
8
|
} from '../../primitives/animated-pressable';
|
|
8
9
|
import { Text } from '../../primitives/text';
|
|
10
|
+
import { IconColorProvider } from '../../icons';
|
|
11
|
+
import { Spinner } from '../spinner';
|
|
9
12
|
|
|
10
13
|
const buttonVariants = tv({
|
|
11
14
|
slots: {
|
|
12
15
|
root: 'flex-row items-center justify-center gap-2 rounded-lg border border-transparent',
|
|
13
16
|
label: 'font-medium',
|
|
17
|
+
spinner: '',
|
|
14
18
|
},
|
|
15
19
|
variants: {
|
|
16
20
|
variant: {
|
|
17
21
|
primary: {
|
|
18
22
|
root: 'border-primary bg-primary shadow-sm',
|
|
19
23
|
label: 'text-primary-foreground',
|
|
24
|
+
spinner: 'border-primary-foreground/32 border-t-primary-foreground',
|
|
20
25
|
},
|
|
21
26
|
secondary: {
|
|
22
27
|
root: 'bg-secondary',
|
|
23
28
|
label: 'text-secondary-foreground',
|
|
29
|
+
spinner: 'border-secondary-foreground/24 border-t-secondary-foreground',
|
|
24
30
|
},
|
|
25
31
|
outline: {
|
|
26
32
|
root: 'border-input bg-popover shadow-sm',
|
|
27
33
|
label: 'text-foreground',
|
|
34
|
+
spinner: 'border-muted border-t-foreground',
|
|
28
35
|
},
|
|
29
36
|
ghost: {
|
|
30
37
|
root: 'bg-transparent',
|
|
31
38
|
label: 'text-foreground',
|
|
39
|
+
spinner: 'border-muted border-t-foreground',
|
|
32
40
|
},
|
|
33
41
|
destructive: {
|
|
34
42
|
root: 'border-destructive bg-destructive shadow-sm',
|
|
35
43
|
label: 'text-white',
|
|
44
|
+
spinner: 'border-white/32 border-t-white',
|
|
45
|
+
},
|
|
46
|
+
/** Neutral surface for third-party sign-in, sized for a full-width stack. */
|
|
47
|
+
social: {
|
|
48
|
+
root: 'border-input bg-card shadow-sm',
|
|
49
|
+
label: 'text-foreground',
|
|
50
|
+
spinner: 'border-muted border-t-foreground',
|
|
36
51
|
},
|
|
37
52
|
},
|
|
38
53
|
size: {
|
|
@@ -41,6 +56,9 @@ const buttonVariants = tv({
|
|
|
41
56
|
lg: { root: 'h-12 px-6', label: 'text-lg' },
|
|
42
57
|
icon: { root: 'h-11 w-11 px-0' },
|
|
43
58
|
},
|
|
59
|
+
fullWidth: {
|
|
60
|
+
true: { root: 'w-full' },
|
|
61
|
+
},
|
|
44
62
|
disabled: {
|
|
45
63
|
true: { root: 'opacity-[0.64]' },
|
|
46
64
|
},
|
|
@@ -53,39 +71,104 @@ const buttonVariants = tv({
|
|
|
53
71
|
|
|
54
72
|
type ButtonVariantProps = VariantProps<typeof buttonVariants>;
|
|
55
73
|
|
|
74
|
+
const SPINNER_SIZE = { sm: 'sm', md: 'sm', lg: 'md', icon: 'sm' } as const;
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* The theme token each variant's content reads against. Icons in the content
|
|
78
|
+
* slots inherit the resolved value, so they follow the theme automatically.
|
|
79
|
+
*
|
|
80
|
+
* `destructive` is absent on purpose: its background is a saturated red in
|
|
81
|
+
* every theme, so its content is always white rather than a themed token.
|
|
82
|
+
*/
|
|
83
|
+
const CONTENT_COLOR_VAR: Record<
|
|
84
|
+
Exclude<NonNullable<ButtonVariantProps['variant']>, 'destructive'>,
|
|
85
|
+
string
|
|
86
|
+
> = {
|
|
87
|
+
primary: '--color-primary-foreground',
|
|
88
|
+
secondary: '--color-secondary-foreground',
|
|
89
|
+
outline: '--color-foreground',
|
|
90
|
+
ghost: '--color-foreground',
|
|
91
|
+
social: '--color-foreground',
|
|
92
|
+
};
|
|
93
|
+
|
|
56
94
|
export interface ButtonProps
|
|
57
95
|
extends Omit<AnimatedPressableProps, 'children' | 'disabled'>,
|
|
58
96
|
Omit<ButtonVariantProps, 'disabled'> {
|
|
59
97
|
children?: ReactNode;
|
|
60
98
|
disabled?: boolean;
|
|
99
|
+
/** Show a spinner and block presses while an action is in flight. */
|
|
100
|
+
loading?: boolean;
|
|
101
|
+
/** Content rendered before the label (replaced by the spinner while loading). */
|
|
102
|
+
startContent?: ReactNode;
|
|
103
|
+
/** Content rendered after the label. */
|
|
104
|
+
endContent?: ReactNode;
|
|
61
105
|
/** Extra classes for the label when children is a string. */
|
|
62
106
|
labelClassName?: string;
|
|
63
107
|
}
|
|
64
108
|
|
|
65
109
|
export const Button = forwardRef<View, ButtonProps>(
|
|
66
110
|
(
|
|
67
|
-
{
|
|
111
|
+
{
|
|
112
|
+
children,
|
|
113
|
+
className,
|
|
114
|
+
labelClassName,
|
|
115
|
+
variant,
|
|
116
|
+
size,
|
|
117
|
+
fullWidth,
|
|
118
|
+
disabled,
|
|
119
|
+
loading = false,
|
|
120
|
+
startContent,
|
|
121
|
+
endContent,
|
|
122
|
+
...props
|
|
123
|
+
},
|
|
68
124
|
ref
|
|
69
125
|
) => {
|
|
70
|
-
const
|
|
126
|
+
const isDisabled = disabled || loading;
|
|
127
|
+
const { root, label, spinner } = buttonVariants({
|
|
128
|
+
variant,
|
|
129
|
+
size,
|
|
130
|
+
fullWidth,
|
|
131
|
+
disabled: isDisabled,
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
// Icons in the content slots inherit this, so they stay legible when the
|
|
135
|
+
// theme inverts the button's background. Without it every caller has to
|
|
136
|
+
// hardcode a hex that is wrong in one theme or the other.
|
|
137
|
+
const themedColor = useCSSVariable(
|
|
138
|
+
CONTENT_COLOR_VAR[
|
|
139
|
+
variant === 'destructive' ? 'primary' : (variant ?? 'primary')
|
|
140
|
+
]
|
|
141
|
+
);
|
|
142
|
+
const contentColor =
|
|
143
|
+
variant === 'destructive'
|
|
144
|
+
? '#ffffff'
|
|
145
|
+
: typeof themedColor === 'string'
|
|
146
|
+
? themedColor
|
|
147
|
+
: undefined;
|
|
71
148
|
|
|
72
149
|
return (
|
|
73
|
-
<
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
150
|
+
<IconColorProvider color={contentColor}>
|
|
151
|
+
<AnimatedPressable
|
|
152
|
+
ref={ref}
|
|
153
|
+
accessibilityRole="button"
|
|
154
|
+
accessibilityState={{ disabled: isDisabled, busy: loading }}
|
|
155
|
+
disabled={isDisabled}
|
|
156
|
+
className={root({ className })}
|
|
157
|
+
{...props}
|
|
158
|
+
>
|
|
159
|
+
{loading ? (
|
|
160
|
+
<Spinner size={SPINNER_SIZE[size ?? 'md']} className={spinner()} />
|
|
161
|
+
) : (
|
|
162
|
+
startContent
|
|
163
|
+
)}
|
|
164
|
+
{typeof children === 'string' ? (
|
|
165
|
+
<Text className={label({ className: labelClassName })}>{children}</Text>
|
|
166
|
+
) : (
|
|
167
|
+
children
|
|
168
|
+
)}
|
|
169
|
+
{endContent}
|
|
170
|
+
</AnimatedPressable>
|
|
171
|
+
</IconColorProvider>
|
|
89
172
|
);
|
|
90
173
|
}
|
|
91
174
|
);
|
|
@@ -16,13 +16,39 @@ const checkboxVariants = tv({
|
|
|
16
16
|
row: 'flex-row items-center gap-2.5',
|
|
17
17
|
box: 'h-5 w-5 items-center justify-center rounded-md border border-input bg-background',
|
|
18
18
|
fill: 'absolute inset-0 items-center justify-center rounded-md bg-primary',
|
|
19
|
+
content: 'flex-1 gap-0.5',
|
|
19
20
|
label: 'text-sm text-foreground',
|
|
21
|
+
description: 'text-sm text-muted-foreground',
|
|
20
22
|
},
|
|
21
23
|
variants: {
|
|
24
|
+
variant: {
|
|
25
|
+
default: {},
|
|
26
|
+
/**
|
|
27
|
+
* The whole surface is the target — for pickable options where the
|
|
28
|
+
* checkbox is a confirmation rather than the affordance itself.
|
|
29
|
+
*/
|
|
30
|
+
card: {
|
|
31
|
+
row: 'w-full items-start gap-3 rounded-xl border border-border bg-card p-4',
|
|
32
|
+
label: 'text-base font-medium',
|
|
33
|
+
},
|
|
34
|
+
},
|
|
35
|
+
checked: {
|
|
36
|
+
true: {},
|
|
37
|
+
},
|
|
22
38
|
disabled: {
|
|
23
39
|
true: { row: 'opacity-50' },
|
|
24
40
|
},
|
|
25
41
|
},
|
|
42
|
+
compoundVariants: [
|
|
43
|
+
{
|
|
44
|
+
variant: 'card',
|
|
45
|
+
checked: true,
|
|
46
|
+
class: { row: 'border-primary bg-accent' },
|
|
47
|
+
},
|
|
48
|
+
],
|
|
49
|
+
defaultVariants: {
|
|
50
|
+
variant: 'default',
|
|
51
|
+
},
|
|
26
52
|
});
|
|
27
53
|
|
|
28
54
|
export interface CheckboxProps extends VariantProps<typeof checkboxVariants> {
|
|
@@ -32,12 +58,17 @@ export interface CheckboxProps extends VariantProps<typeof checkboxVariants> {
|
|
|
32
58
|
disabled?: boolean;
|
|
33
59
|
/** Optional label rendered next to the box; pressing it also toggles. */
|
|
34
60
|
label?: string;
|
|
61
|
+
/** Secondary line under the label, for extra context. */
|
|
62
|
+
description?: string;
|
|
35
63
|
}
|
|
36
64
|
|
|
37
65
|
export const Checkbox = forwardRef<View, CheckboxProps>(
|
|
38
|
-
(
|
|
66
|
+
(
|
|
67
|
+
{ className, checked, onCheckedChange, disabled, label, description, variant },
|
|
68
|
+
ref
|
|
69
|
+
) => {
|
|
39
70
|
const progress = useSharedValue(checked ? 1 : 0);
|
|
40
|
-
const slots = checkboxVariants({ disabled: !!disabled });
|
|
71
|
+
const slots = checkboxVariants({ variant, checked, disabled: !!disabled });
|
|
41
72
|
const checkColor = useCSSVariable('--color-primary-foreground');
|
|
42
73
|
|
|
43
74
|
useEffect(() => {
|
|
@@ -51,26 +82,47 @@ export const Checkbox = forwardRef<View, CheckboxProps>(
|
|
|
51
82
|
transform: [{ scale: 0.6 + progress.value * 0.4 }],
|
|
52
83
|
}));
|
|
53
84
|
|
|
85
|
+
const box = (
|
|
86
|
+
<View className={slots.box()}>
|
|
87
|
+
<Animated.View style={fillStyle} className={slots.fill()}>
|
|
88
|
+
<CheckIcon
|
|
89
|
+
size={12}
|
|
90
|
+
color={typeof checkColor === 'string' ? checkColor : '#fff'}
|
|
91
|
+
/>
|
|
92
|
+
</Animated.View>
|
|
93
|
+
</View>
|
|
94
|
+
);
|
|
95
|
+
|
|
54
96
|
return (
|
|
55
97
|
<Pressable
|
|
56
98
|
ref={ref}
|
|
57
99
|
accessibilityRole="checkbox"
|
|
58
100
|
accessibilityState={{ checked, disabled: !!disabled }}
|
|
59
101
|
accessibilityLabel={label}
|
|
102
|
+
accessibilityHint={description}
|
|
60
103
|
disabled={disabled}
|
|
61
104
|
onPress={() => onCheckedChange?.(!checked)}
|
|
62
105
|
hitSlop={8}
|
|
63
|
-
|
|
106
|
+
// A description makes the row two lines tall — align the box to the
|
|
107
|
+
// label rather than centring it against the whole block.
|
|
108
|
+
className={slots.row({
|
|
109
|
+
className:
|
|
110
|
+
description && variant !== 'card'
|
|
111
|
+
? `items-start ${className ?? ''}`
|
|
112
|
+
: className,
|
|
113
|
+
})}
|
|
64
114
|
>
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
115
|
+
{/* The card lays out content first so the box sits top-right. */}
|
|
116
|
+
{variant === 'card' ? null : box}
|
|
117
|
+
{label || description ? (
|
|
118
|
+
<View className={slots.content()}>
|
|
119
|
+
{label ? <Text className={slots.label()}>{label}</Text> : null}
|
|
120
|
+
{description ? (
|
|
121
|
+
<Text className={slots.description()}>{description}</Text>
|
|
122
|
+
) : null}
|
|
123
|
+
</View>
|
|
124
|
+
) : null}
|
|
125
|
+
{variant === 'card' ? box : null}
|
|
74
126
|
</Pressable>
|
|
75
127
|
);
|
|
76
128
|
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* EmptyState — the placeholder shown when a list or view has no content.
|
|
3
|
+
*
|
|
4
|
+
* Adapted from: cosscom/coss packages/ui/src/components/empty.tsx
|
|
5
|
+
* (centered Header / Media / Title / Description / Content anatomy, and the
|
|
6
|
+
* `icon` media variant that stacks two rotated ghost cards behind the icon).
|
|
7
|
+
* Coss does that with CSS pseudo-elements; on native it is two absolutely
|
|
8
|
+
* positioned siblings with static rotate transforms.
|
|
9
|
+
*/
|
|
10
|
+
import { forwardRef, type ReactNode } from 'react';
|
|
11
|
+
import { View, type Text as RNText, type ViewProps } from 'react-native';
|
|
12
|
+
import { tv, type VariantProps } from 'tailwind-variants';
|
|
13
|
+
import { Text, type TextProps } from '../../primitives/text';
|
|
14
|
+
|
|
15
|
+
const emptyStateVariants = tv({
|
|
16
|
+
slots: {
|
|
17
|
+
root: 'w-full flex-1 items-center justify-center gap-6 px-6 py-12',
|
|
18
|
+
header: 'w-full max-w-sm items-center gap-1',
|
|
19
|
+
title: 'text-center text-xl font-semibold text-foreground',
|
|
20
|
+
description: 'text-center text-sm text-muted-foreground',
|
|
21
|
+
content: 'w-full max-w-sm items-center gap-3',
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
const mediaVariants = tv({
|
|
26
|
+
slots: {
|
|
27
|
+
wrapper: 'items-center justify-center',
|
|
28
|
+
card: 'h-9 w-9 shrink-0 items-center justify-center',
|
|
29
|
+
ghost: 'absolute h-9 w-9 rounded-md border border-border bg-card',
|
|
30
|
+
},
|
|
31
|
+
variants: {
|
|
32
|
+
variant: {
|
|
33
|
+
default: {
|
|
34
|
+
card: 'bg-transparent',
|
|
35
|
+
ghost: 'hidden',
|
|
36
|
+
},
|
|
37
|
+
icon: {
|
|
38
|
+
card: 'rounded-md border border-border bg-card',
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
defaultVariants: {
|
|
43
|
+
variant: 'default',
|
|
44
|
+
},
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
export interface EmptyStateProps extends ViewProps {
|
|
48
|
+
className?: string;
|
|
49
|
+
children?: ReactNode;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const EmptyStateRoot = forwardRef<View, EmptyStateProps>(
|
|
53
|
+
({ className, ...props }, ref) => {
|
|
54
|
+
const { root } = emptyStateVariants();
|
|
55
|
+
return <View ref={ref} className={root({ className })} {...props} />;
|
|
56
|
+
}
|
|
57
|
+
);
|
|
58
|
+
EmptyStateRoot.displayName = 'EmptyState';
|
|
59
|
+
|
|
60
|
+
/** Groups Media, Title and Description into one centered column. */
|
|
61
|
+
const EmptyStateHeader = forwardRef<View, EmptyStateProps>(
|
|
62
|
+
({ className, ...props }, ref) => {
|
|
63
|
+
const { header } = emptyStateVariants();
|
|
64
|
+
return <View ref={ref} className={header({ className })} {...props} />;
|
|
65
|
+
}
|
|
66
|
+
);
|
|
67
|
+
EmptyStateHeader.displayName = 'EmptyState.Header';
|
|
68
|
+
|
|
69
|
+
export interface EmptyStateMediaProps
|
|
70
|
+
extends ViewProps,
|
|
71
|
+
VariantProps<typeof mediaVariants> {
|
|
72
|
+
className?: string;
|
|
73
|
+
children?: ReactNode;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Visual anchor above the title. `variant="icon"` frames the child in a small
|
|
78
|
+
* card with two rotated ghost cards fanned out behind it.
|
|
79
|
+
*/
|
|
80
|
+
const EmptyStateMedia = forwardRef<View, EmptyStateMediaProps>(
|
|
81
|
+
({ className, variant = 'default', children, ...props }, ref) => {
|
|
82
|
+
const { wrapper, card, ghost } = mediaVariants({ variant });
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<View ref={ref} className={wrapper({ className: `mb-4 ${className ?? ''}` })} {...props}>
|
|
86
|
+
{variant === 'icon' ? (
|
|
87
|
+
<>
|
|
88
|
+
<View
|
|
89
|
+
accessibilityElementsHidden
|
|
90
|
+
importantForAccessibility="no-hide-descendants"
|
|
91
|
+
className={`${ghost()} -translate-x-2 -rotate-[10deg] scale-90`}
|
|
92
|
+
/>
|
|
93
|
+
<View
|
|
94
|
+
accessibilityElementsHidden
|
|
95
|
+
importantForAccessibility="no-hide-descendants"
|
|
96
|
+
className={`${ghost()} translate-x-2 rotate-[10deg] scale-90`}
|
|
97
|
+
/>
|
|
98
|
+
</>
|
|
99
|
+
) : null}
|
|
100
|
+
<View className={card()}>{children}</View>
|
|
101
|
+
</View>
|
|
102
|
+
);
|
|
103
|
+
}
|
|
104
|
+
);
|
|
105
|
+
EmptyStateMedia.displayName = 'EmptyState.Media';
|
|
106
|
+
|
|
107
|
+
const EmptyStateTitle = forwardRef<RNText, TextProps>(({ className, ...props }, ref) => {
|
|
108
|
+
const { title } = emptyStateVariants();
|
|
109
|
+
return <Text ref={ref} className={title({ className })} {...props} />;
|
|
110
|
+
});
|
|
111
|
+
EmptyStateTitle.displayName = 'EmptyState.Title';
|
|
112
|
+
|
|
113
|
+
const EmptyStateDescription = forwardRef<RNText, TextProps>(
|
|
114
|
+
({ className, ...props }, ref) => {
|
|
115
|
+
const { description } = emptyStateVariants();
|
|
116
|
+
return <Text ref={ref} className={description({ className })} {...props} />;
|
|
117
|
+
}
|
|
118
|
+
);
|
|
119
|
+
EmptyStateDescription.displayName = 'EmptyState.Description';
|
|
120
|
+
|
|
121
|
+
/** Slot below the header, for actions such as a primary button. */
|
|
122
|
+
const EmptyStateContent = forwardRef<View, EmptyStateProps>(
|
|
123
|
+
({ className, ...props }, ref) => {
|
|
124
|
+
const { content } = emptyStateVariants();
|
|
125
|
+
return <View ref={ref} className={content({ className })} {...props} />;
|
|
126
|
+
}
|
|
127
|
+
);
|
|
128
|
+
EmptyStateContent.displayName = 'EmptyState.Content';
|
|
129
|
+
|
|
130
|
+
export const EmptyState = Object.assign(EmptyStateRoot, {
|
|
131
|
+
Header: EmptyStateHeader,
|
|
132
|
+
Media: EmptyStateMedia,
|
|
133
|
+
Title: EmptyStateTitle,
|
|
134
|
+
Description: EmptyStateDescription,
|
|
135
|
+
Content: EmptyStateContent,
|
|
136
|
+
});
|