panelui-native 0.2.0 → 0.4.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.
- package/README.md +251 -0
- package/lib/module/components/alert/index.js +106 -13
- package/lib/module/components/alert/index.js.map +1 -1
- package/lib/module/components/badge/index.js +3 -3
- package/lib/module/components/badge/index.js.map +1 -1
- package/lib/module/components/button/index.js +44 -15
- package/lib/module/components/button/index.js.map +1 -1
- package/lib/module/components/checkbox/index.js +20 -7
- 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/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/toast/index.js +479 -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 +223 -2
- package/lib/module/icons/index.js.map +1 -1
- package/lib/module/index.js +9 -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/alert/index.d.ts +56 -4
- package/lib/typescript/src/components/alert/index.d.ts.map +1 -1
- package/lib/typescript/src/components/button/index.d.ts +39 -0
- package/lib/typescript/src/components/button/index.d.ts.map +1 -1
- package/lib/typescript/src/components/checkbox/index.d.ts +8 -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/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/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 +25 -0
- package/lib/typescript/src/icons/index.d.ts.map +1 -1
- package/lib/typescript/src/index.d.ts +10 -3
- 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/alert/index.tsx +102 -16
- package/src/components/badge/index.tsx +3 -3
- package/src/components/button/index.tsx +47 -7
- package/src/components/checkbox/index.tsx +17 -3
- package/src/components/empty-state/index.tsx +136 -0
- 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/toast/index.tsx +447 -0
- package/src/components/toast/toast-store.ts +120 -0
- package/src/components/typography/index.tsx +121 -0
- package/src/icons/index.tsx +152 -1
- package/src/index.ts +76 -3
- package/src/providers/panel-ui-provider.tsx +3 -0
- package/src/theme/use-theme.ts +119 -4
- package/theme.css +330 -7
|
@@ -1,31 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Alert — a status message with an optional leading status icon.
|
|
3
|
+
*
|
|
4
|
+
* Adapted from: heroui-inc/heroui-native src/components/alert/alert.tsx
|
|
5
|
+
* (Indicator / Content / Title / Description anatomy, status-driven icon and
|
|
6
|
+
* title colour), restyled with Coss UI tokens.
|
|
7
|
+
*
|
|
8
|
+
* The `variant` prop name is kept from PanelUI v0.2 rather than renamed to
|
|
9
|
+
* HeroUI's `status`, so existing code keeps working.
|
|
10
|
+
*/
|
|
1
11
|
import { createContext, forwardRef, useContext, type ReactNode } from 'react';
|
|
2
|
-
import { View, type ViewProps } from 'react-native';
|
|
12
|
+
import { View, type Text as RNText, type ViewProps } from 'react-native';
|
|
3
13
|
import { tv, type VariantProps } from 'tailwind-variants';
|
|
14
|
+
import { useCSSVariable } from 'uniwind';
|
|
4
15
|
import { cn } from '../../utils/cn';
|
|
5
16
|
import { Text, type TextProps } from '../../primitives/text';
|
|
17
|
+
import { AlertTriangleIcon, CheckCircleIcon, InfoIcon } from '../../icons';
|
|
6
18
|
|
|
7
19
|
const alertVariants = tv({
|
|
8
20
|
slots: {
|
|
9
|
-
root: 'w-full gap-
|
|
10
|
-
|
|
21
|
+
root: 'w-full flex-row gap-3 rounded-xl border px-3.5 py-3',
|
|
22
|
+
indicator: 'pt-px',
|
|
23
|
+
content: 'flex-1 gap-0.5',
|
|
24
|
+
title: 'text-sm font-medium',
|
|
11
25
|
description: 'text-sm text-muted-foreground',
|
|
12
26
|
},
|
|
13
27
|
variants: {
|
|
14
28
|
variant: {
|
|
15
29
|
default: {
|
|
16
30
|
root: 'border-border bg-surface',
|
|
31
|
+
title: 'text-card-foreground',
|
|
17
32
|
},
|
|
18
33
|
info: {
|
|
19
|
-
root: 'border-info/32 bg-info
|
|
34
|
+
root: 'border-info/32 bg-info-soft',
|
|
35
|
+
title: 'text-info-foreground',
|
|
20
36
|
},
|
|
21
37
|
success: {
|
|
22
|
-
root: 'border-success/32 bg-success
|
|
38
|
+
root: 'border-success/32 bg-success-soft',
|
|
39
|
+
title: 'text-success-foreground',
|
|
23
40
|
},
|
|
24
41
|
warning: {
|
|
25
|
-
root: 'border-warning/32 bg-warning
|
|
42
|
+
root: 'border-warning/32 bg-warning-soft',
|
|
43
|
+
title: 'text-warning-foreground',
|
|
26
44
|
},
|
|
27
45
|
destructive: {
|
|
28
|
-
root: 'border-destructive/32 bg-destructive
|
|
46
|
+
root: 'border-destructive/32 bg-destructive-soft',
|
|
47
|
+
title: 'text-destructive-foreground',
|
|
29
48
|
},
|
|
30
49
|
},
|
|
31
50
|
},
|
|
@@ -34,20 +53,42 @@ const alertVariants = tv({
|
|
|
34
53
|
},
|
|
35
54
|
});
|
|
36
55
|
|
|
37
|
-
type AlertVariant = VariantProps<typeof alertVariants>['variant']
|
|
56
|
+
type AlertVariant = NonNullable<VariantProps<typeof alertVariants>['variant']>;
|
|
38
57
|
|
|
39
58
|
const AlertContext = createContext<AlertVariant>('default');
|
|
40
59
|
|
|
60
|
+
/** CSS variable each variant's indicator icon takes its colour from. */
|
|
61
|
+
const INDICATOR_COLOR_VAR: Record<AlertVariant, string> = {
|
|
62
|
+
default: '--color-muted-foreground',
|
|
63
|
+
info: '--color-info-foreground',
|
|
64
|
+
success: '--color-success-foreground',
|
|
65
|
+
warning: '--color-warning-foreground',
|
|
66
|
+
destructive: '--color-destructive-foreground',
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
const INDICATOR_ICON: Record<AlertVariant, typeof InfoIcon> = {
|
|
70
|
+
default: InfoIcon,
|
|
71
|
+
info: InfoIcon,
|
|
72
|
+
success: CheckCircleIcon,
|
|
73
|
+
warning: AlertTriangleIcon,
|
|
74
|
+
destructive: AlertTriangleIcon,
|
|
75
|
+
};
|
|
76
|
+
|
|
41
77
|
export interface AlertProps extends ViewProps, VariantProps<typeof alertVariants> {
|
|
42
78
|
className?: string;
|
|
43
|
-
/**
|
|
79
|
+
/**
|
|
80
|
+
* Leading element rendered before the content.
|
|
81
|
+
*
|
|
82
|
+
* @deprecated Prefer `<Alert.Indicator>`, which picks a status icon for you.
|
|
83
|
+
* Still honoured so v0.2 call sites keep rendering.
|
|
84
|
+
*/
|
|
44
85
|
icon?: ReactNode;
|
|
45
86
|
children?: ReactNode;
|
|
46
87
|
}
|
|
47
88
|
|
|
48
89
|
const AlertRoot = forwardRef<View, AlertProps>(
|
|
49
90
|
({ className, variant = 'default', icon, children, ...props }, ref) => {
|
|
50
|
-
const { root } = alertVariants({ variant });
|
|
91
|
+
const { root, indicator, content } = alertVariants({ variant });
|
|
51
92
|
|
|
52
93
|
return (
|
|
53
94
|
<AlertContext.Provider value={variant}>
|
|
@@ -58,10 +99,10 @@ const AlertRoot = forwardRef<View, AlertProps>(
|
|
|
58
99
|
{...props}
|
|
59
100
|
>
|
|
60
101
|
{icon ? (
|
|
61
|
-
|
|
62
|
-
<View className=
|
|
63
|
-
<View className=
|
|
64
|
-
|
|
102
|
+
<>
|
|
103
|
+
<View className={indicator()}>{icon}</View>
|
|
104
|
+
<View className={content()}>{children}</View>
|
|
105
|
+
</>
|
|
65
106
|
) : (
|
|
66
107
|
children
|
|
67
108
|
)}
|
|
@@ -72,7 +113,48 @@ const AlertRoot = forwardRef<View, AlertProps>(
|
|
|
72
113
|
);
|
|
73
114
|
AlertRoot.displayName = 'Alert';
|
|
74
115
|
|
|
75
|
-
|
|
116
|
+
export interface AlertIndicatorProps extends ViewProps {
|
|
117
|
+
className?: string;
|
|
118
|
+
/** Overrides the size/colour of the default status icon. */
|
|
119
|
+
iconProps?: { size?: number; color?: string };
|
|
120
|
+
/** Replaces the default status icon entirely. */
|
|
121
|
+
children?: ReactNode;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Leading status icon. Resolves both the icon and its colour from the parent
|
|
126
|
+
* Alert's variant, so it follows the active theme.
|
|
127
|
+
*/
|
|
128
|
+
const AlertIndicator = forwardRef<View, AlertIndicatorProps>(
|
|
129
|
+
({ className, iconProps, children, ...props }, ref) => {
|
|
130
|
+
const variant = useContext(AlertContext);
|
|
131
|
+
const { indicator } = alertVariants({ variant });
|
|
132
|
+
const themeColor = useCSSVariable(INDICATOR_COLOR_VAR[variant]);
|
|
133
|
+
const Icon = INDICATOR_ICON[variant];
|
|
134
|
+
|
|
135
|
+
const color =
|
|
136
|
+
iconProps?.color ?? (typeof themeColor === 'string' ? themeColor : undefined);
|
|
137
|
+
|
|
138
|
+
return (
|
|
139
|
+
<View ref={ref} className={indicator({ className })} {...props}>
|
|
140
|
+
{children ?? <Icon size={iconProps?.size ?? 18} color={color} />}
|
|
141
|
+
</View>
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
);
|
|
145
|
+
AlertIndicator.displayName = 'Alert.Indicator';
|
|
146
|
+
|
|
147
|
+
/** Flex-1 wrapper for Alert.Title and Alert.Description. */
|
|
148
|
+
const AlertContent = forwardRef<View, ViewProps & { className?: string }>(
|
|
149
|
+
({ className, ...props }, ref) => {
|
|
150
|
+
const variant = useContext(AlertContext);
|
|
151
|
+
const { content } = alertVariants({ variant });
|
|
152
|
+
return <View ref={ref} className={content({ className })} {...props} />;
|
|
153
|
+
}
|
|
154
|
+
);
|
|
155
|
+
AlertContent.displayName = 'Alert.Content';
|
|
156
|
+
|
|
157
|
+
const AlertTitle = forwardRef<RNText, TextProps>(
|
|
76
158
|
({ className, ...props }, ref) => {
|
|
77
159
|
const variant = useContext(AlertContext);
|
|
78
160
|
const { title } = alertVariants({ variant });
|
|
@@ -81,7 +163,7 @@ const AlertTitle = forwardRef<React.ElementRef<typeof Text>, TextProps>(
|
|
|
81
163
|
);
|
|
82
164
|
AlertTitle.displayName = 'Alert.Title';
|
|
83
165
|
|
|
84
|
-
const AlertDescription = forwardRef<
|
|
166
|
+
const AlertDescription = forwardRef<RNText, TextProps>(
|
|
85
167
|
({ className, ...props }, ref) => {
|
|
86
168
|
const variant = useContext(AlertContext);
|
|
87
169
|
const { description } = alertVariants({ variant });
|
|
@@ -91,6 +173,10 @@ const AlertDescription = forwardRef<React.ElementRef<typeof Text>, TextProps>(
|
|
|
91
173
|
AlertDescription.displayName = 'Alert.Description';
|
|
92
174
|
|
|
93
175
|
export const Alert = Object.assign(AlertRoot, {
|
|
176
|
+
/** @optional Status icon rendered as the leading visual element. */
|
|
177
|
+
Indicator: AlertIndicator,
|
|
178
|
+
/** @optional Wrapper for title and description. */
|
|
179
|
+
Content: AlertContent,
|
|
94
180
|
Title: AlertTitle,
|
|
95
181
|
Description: AlertDescription,
|
|
96
182
|
});
|
|
@@ -14,9 +14,9 @@ const badgeVariants = tv({
|
|
|
14
14
|
secondary: { root: 'bg-secondary', label: 'text-secondary-foreground' },
|
|
15
15
|
outline: { root: 'border-border bg-transparent', label: 'text-foreground' },
|
|
16
16
|
destructive: { root: 'bg-destructive', label: 'text-white' },
|
|
17
|
-
success: { root: 'bg-success
|
|
18
|
-
warning: { root: 'bg-warning
|
|
19
|
-
info: { root: 'bg-info
|
|
17
|
+
success: { root: 'bg-success-subtle', label: 'text-success-foreground' },
|
|
18
|
+
warning: { root: 'bg-warning-subtle', label: 'text-warning-foreground' },
|
|
19
|
+
info: { root: 'bg-info-subtle', label: 'text-info-foreground' },
|
|
20
20
|
},
|
|
21
21
|
},
|
|
22
22
|
defaultVariants: {
|
|
@@ -1,4 +1,4 @@
|
|
|
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
4
|
import {
|
|
@@ -6,33 +6,40 @@ import {
|
|
|
6
6
|
type AnimatedPressableProps,
|
|
7
7
|
} from '../../primitives/animated-pressable';
|
|
8
8
|
import { Text } from '../../primitives/text';
|
|
9
|
+
import { Spinner } from '../spinner';
|
|
9
10
|
|
|
10
11
|
const buttonVariants = tv({
|
|
11
12
|
slots: {
|
|
12
13
|
root: 'flex-row items-center justify-center gap-2 rounded-lg border border-transparent',
|
|
13
14
|
label: 'font-medium',
|
|
15
|
+
spinner: '',
|
|
14
16
|
},
|
|
15
17
|
variants: {
|
|
16
18
|
variant: {
|
|
17
19
|
primary: {
|
|
18
20
|
root: 'border-primary bg-primary shadow-sm',
|
|
19
21
|
label: 'text-primary-foreground',
|
|
22
|
+
spinner: 'border-primary-foreground/32 border-t-primary-foreground',
|
|
20
23
|
},
|
|
21
24
|
secondary: {
|
|
22
25
|
root: 'bg-secondary',
|
|
23
26
|
label: 'text-secondary-foreground',
|
|
27
|
+
spinner: 'border-secondary-foreground/24 border-t-secondary-foreground',
|
|
24
28
|
},
|
|
25
29
|
outline: {
|
|
26
30
|
root: 'border-input bg-popover shadow-sm',
|
|
27
31
|
label: 'text-foreground',
|
|
32
|
+
spinner: 'border-muted border-t-foreground',
|
|
28
33
|
},
|
|
29
34
|
ghost: {
|
|
30
35
|
root: 'bg-transparent',
|
|
31
36
|
label: 'text-foreground',
|
|
37
|
+
spinner: 'border-muted border-t-foreground',
|
|
32
38
|
},
|
|
33
39
|
destructive: {
|
|
34
40
|
root: 'border-destructive bg-destructive shadow-sm',
|
|
35
41
|
label: 'text-white',
|
|
42
|
+
spinner: 'border-white/32 border-t-white',
|
|
36
43
|
},
|
|
37
44
|
},
|
|
38
45
|
size: {
|
|
@@ -41,6 +48,9 @@ const buttonVariants = tv({
|
|
|
41
48
|
lg: { root: 'h-12 px-6', label: 'text-lg' },
|
|
42
49
|
icon: { root: 'h-11 w-11 px-0' },
|
|
43
50
|
},
|
|
51
|
+
fullWidth: {
|
|
52
|
+
true: { root: 'w-full' },
|
|
53
|
+
},
|
|
44
54
|
disabled: {
|
|
45
55
|
true: { root: 'opacity-[0.64]' },
|
|
46
56
|
},
|
|
@@ -53,38 +63,68 @@ const buttonVariants = tv({
|
|
|
53
63
|
|
|
54
64
|
type ButtonVariantProps = VariantProps<typeof buttonVariants>;
|
|
55
65
|
|
|
66
|
+
const SPINNER_SIZE = { sm: 'sm', md: 'sm', lg: 'md', icon: 'sm' } as const;
|
|
67
|
+
|
|
56
68
|
export interface ButtonProps
|
|
57
69
|
extends Omit<AnimatedPressableProps, 'children' | 'disabled'>,
|
|
58
70
|
Omit<ButtonVariantProps, 'disabled'> {
|
|
59
71
|
children?: ReactNode;
|
|
60
72
|
disabled?: boolean;
|
|
73
|
+
/** Show a spinner and block presses while an action is in flight. */
|
|
74
|
+
loading?: boolean;
|
|
75
|
+
/** Content rendered before the label (replaced by the spinner while loading). */
|
|
76
|
+
startContent?: ReactNode;
|
|
77
|
+
/** Content rendered after the label. */
|
|
78
|
+
endContent?: ReactNode;
|
|
61
79
|
/** Extra classes for the label when children is a string. */
|
|
62
80
|
labelClassName?: string;
|
|
63
81
|
}
|
|
64
82
|
|
|
65
83
|
export const Button = forwardRef<View, ButtonProps>(
|
|
66
84
|
(
|
|
67
|
-
{
|
|
85
|
+
{
|
|
86
|
+
children,
|
|
87
|
+
className,
|
|
88
|
+
labelClassName,
|
|
89
|
+
variant,
|
|
90
|
+
size,
|
|
91
|
+
fullWidth,
|
|
92
|
+
disabled,
|
|
93
|
+
loading = false,
|
|
94
|
+
startContent,
|
|
95
|
+
endContent,
|
|
96
|
+
...props
|
|
97
|
+
},
|
|
68
98
|
ref
|
|
69
99
|
) => {
|
|
70
|
-
const
|
|
100
|
+
const isDisabled = disabled || loading;
|
|
101
|
+
const { root, label, spinner } = buttonVariants({
|
|
102
|
+
variant,
|
|
103
|
+
size,
|
|
104
|
+
fullWidth,
|
|
105
|
+
disabled: isDisabled,
|
|
106
|
+
});
|
|
71
107
|
|
|
72
108
|
return (
|
|
73
109
|
<AnimatedPressable
|
|
74
110
|
ref={ref}
|
|
75
111
|
accessibilityRole="button"
|
|
76
|
-
accessibilityState={{ disabled:
|
|
77
|
-
disabled={
|
|
112
|
+
accessibilityState={{ disabled: isDisabled, busy: loading }}
|
|
113
|
+
disabled={isDisabled}
|
|
78
114
|
className={root({ className })}
|
|
79
115
|
{...props}
|
|
80
116
|
>
|
|
117
|
+
{loading ? (
|
|
118
|
+
<Spinner size={SPINNER_SIZE[size ?? 'md']} className={spinner()} />
|
|
119
|
+
) : (
|
|
120
|
+
startContent
|
|
121
|
+
)}
|
|
81
122
|
{typeof children === 'string' ? (
|
|
82
123
|
<Text className={label({ className: labelClassName })}>{children}</Text>
|
|
83
|
-
) : isValidElement(children) ? (
|
|
84
|
-
children
|
|
85
124
|
) : (
|
|
86
125
|
children
|
|
87
126
|
)}
|
|
127
|
+
{endContent}
|
|
88
128
|
</AnimatedPressable>
|
|
89
129
|
);
|
|
90
130
|
}
|
|
@@ -16,7 +16,9 @@ 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: {
|
|
22
24
|
disabled: {
|
|
@@ -32,10 +34,12 @@ export interface CheckboxProps extends VariantProps<typeof checkboxVariants> {
|
|
|
32
34
|
disabled?: boolean;
|
|
33
35
|
/** Optional label rendered next to the box; pressing it also toggles. */
|
|
34
36
|
label?: string;
|
|
37
|
+
/** Secondary line under the label, for extra context. */
|
|
38
|
+
description?: string;
|
|
35
39
|
}
|
|
36
40
|
|
|
37
41
|
export const Checkbox = forwardRef<View, CheckboxProps>(
|
|
38
|
-
({ className, checked, onCheckedChange, disabled, label }, ref) => {
|
|
42
|
+
({ className, checked, onCheckedChange, disabled, label, description }, ref) => {
|
|
39
43
|
const progress = useSharedValue(checked ? 1 : 0);
|
|
40
44
|
const slots = checkboxVariants({ disabled: !!disabled });
|
|
41
45
|
const checkColor = useCSSVariable('--color-primary-foreground');
|
|
@@ -57,10 +61,13 @@ export const Checkbox = forwardRef<View, CheckboxProps>(
|
|
|
57
61
|
accessibilityRole="checkbox"
|
|
58
62
|
accessibilityState={{ checked, disabled: !!disabled }}
|
|
59
63
|
accessibilityLabel={label}
|
|
64
|
+
accessibilityHint={description}
|
|
60
65
|
disabled={disabled}
|
|
61
66
|
onPress={() => onCheckedChange?.(!checked)}
|
|
62
67
|
hitSlop={8}
|
|
63
|
-
|
|
68
|
+
// A description makes the row two lines tall — align the box to the
|
|
69
|
+
// label rather than centring it against the whole block.
|
|
70
|
+
className={slots.row({ className: description ? `items-start ${className ?? ''}` : className })}
|
|
64
71
|
>
|
|
65
72
|
<View className={slots.box()}>
|
|
66
73
|
<Animated.View style={fillStyle} className={slots.fill()}>
|
|
@@ -70,7 +77,14 @@ export const Checkbox = forwardRef<View, CheckboxProps>(
|
|
|
70
77
|
/>
|
|
71
78
|
</Animated.View>
|
|
72
79
|
</View>
|
|
73
|
-
{label ?
|
|
80
|
+
{label || description ? (
|
|
81
|
+
<View className={slots.content()}>
|
|
82
|
+
{label ? <Text className={slots.label()}>{label}</Text> : null}
|
|
83
|
+
{description ? (
|
|
84
|
+
<Text className={slots.description()}>{description}</Text>
|
|
85
|
+
) : null}
|
|
86
|
+
</View>
|
|
87
|
+
) : null}
|
|
74
88
|
</Pressable>
|
|
75
89
|
);
|
|
76
90
|
}
|
|
@@ -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
|
+
});
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* InputGroup — an Input with leading and/or trailing decorators.
|
|
3
|
+
*
|
|
4
|
+
* Adapted from: heroui-inc/heroui-native src/components/input-group/
|
|
5
|
+
* Prefix and Suffix are absolutely positioned and their measured widths become
|
|
6
|
+
* padding on the Input, so text never runs underneath a decorator regardless
|
|
7
|
+
* of how wide that decorator turns out to be.
|
|
8
|
+
*/
|
|
9
|
+
import {
|
|
10
|
+
createContext,
|
|
11
|
+
forwardRef,
|
|
12
|
+
useCallback,
|
|
13
|
+
useContext,
|
|
14
|
+
useMemo,
|
|
15
|
+
useState,
|
|
16
|
+
type ReactNode,
|
|
17
|
+
} from 'react';
|
|
18
|
+
import {
|
|
19
|
+
View,
|
|
20
|
+
type LayoutChangeEvent,
|
|
21
|
+
type TextInput,
|
|
22
|
+
type ViewProps,
|
|
23
|
+
} from 'react-native';
|
|
24
|
+
import { tv } from 'tailwind-variants';
|
|
25
|
+
import { Input, type InputProps } from '../input';
|
|
26
|
+
|
|
27
|
+
const inputGroupVariants = tv({
|
|
28
|
+
slots: {
|
|
29
|
+
root: 'w-full',
|
|
30
|
+
prefix: 'absolute bottom-0 left-0 top-0 z-10 flex-row items-center justify-center gap-2 px-3',
|
|
31
|
+
suffix: 'absolute bottom-0 right-0 top-0 z-10 flex-row items-center justify-center gap-2 px-3',
|
|
32
|
+
},
|
|
33
|
+
variants: {
|
|
34
|
+
isDisabled: {
|
|
35
|
+
true: { prefix: 'opacity-[0.64]', suffix: 'opacity-[0.64]' },
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
interface InputGroupState {
|
|
41
|
+
isDisabled: boolean;
|
|
42
|
+
prefixWidth: number;
|
|
43
|
+
suffixWidth: number;
|
|
44
|
+
setPrefixWidth: (width: number) => void;
|
|
45
|
+
setSuffixWidth: (width: number) => void;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const InputGroupContext = createContext<InputGroupState | null>(null);
|
|
49
|
+
|
|
50
|
+
function useInputGroup() {
|
|
51
|
+
return useContext(InputGroupContext);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface InputGroupProps extends ViewProps {
|
|
55
|
+
className?: string;
|
|
56
|
+
/** Disables the input and dims both decorators. */
|
|
57
|
+
isDisabled?: boolean;
|
|
58
|
+
children?: ReactNode;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const InputGroupRoot = forwardRef<View, InputGroupProps>(
|
|
62
|
+
({ className, isDisabled = false, children, ...props }, ref) => {
|
|
63
|
+
const { root } = inputGroupVariants();
|
|
64
|
+
const [prefixWidth, setPrefixWidth] = useState(0);
|
|
65
|
+
const [suffixWidth, setSuffixWidth] = useState(0);
|
|
66
|
+
|
|
67
|
+
const value = useMemo<InputGroupState>(
|
|
68
|
+
() => ({ isDisabled, prefixWidth, suffixWidth, setPrefixWidth, setSuffixWidth }),
|
|
69
|
+
[isDisabled, prefixWidth, suffixWidth]
|
|
70
|
+
);
|
|
71
|
+
|
|
72
|
+
return (
|
|
73
|
+
<InputGroupContext.Provider value={value}>
|
|
74
|
+
<View ref={ref} className={root({ className })} {...props}>
|
|
75
|
+
{children}
|
|
76
|
+
</View>
|
|
77
|
+
</InputGroupContext.Provider>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
);
|
|
81
|
+
InputGroupRoot.displayName = 'InputGroup';
|
|
82
|
+
|
|
83
|
+
export interface InputGroupDecoratorProps extends ViewProps {
|
|
84
|
+
className?: string;
|
|
85
|
+
/**
|
|
86
|
+
* Marks the decorator as presentation-only: touches fall through to the
|
|
87
|
+
* Input and screen readers skip it. Leave it off when the decorator holds
|
|
88
|
+
* something interactive, such as a show-password toggle.
|
|
89
|
+
*/
|
|
90
|
+
isDecorative?: boolean;
|
|
91
|
+
children?: ReactNode;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** Builds Prefix/Suffix, which differ only in which side they measure. */
|
|
95
|
+
function createDecorator(side: 'prefix' | 'suffix') {
|
|
96
|
+
const Decorator = forwardRef<View, InputGroupDecoratorProps>(
|
|
97
|
+
({ className, isDecorative = false, onLayout, children, ...props }, ref) => {
|
|
98
|
+
const group = useInputGroup();
|
|
99
|
+
const slots = inputGroupVariants({ isDisabled: group?.isDisabled ?? false });
|
|
100
|
+
const setWidth = side === 'prefix' ? group?.setPrefixWidth : group?.setSuffixWidth;
|
|
101
|
+
|
|
102
|
+
const handleLayout = useCallback(
|
|
103
|
+
(event: LayoutChangeEvent) => {
|
|
104
|
+
setWidth?.(event.nativeEvent.layout.width);
|
|
105
|
+
onLayout?.(event);
|
|
106
|
+
},
|
|
107
|
+
[setWidth, onLayout]
|
|
108
|
+
);
|
|
109
|
+
|
|
110
|
+
return (
|
|
111
|
+
<View
|
|
112
|
+
ref={ref}
|
|
113
|
+
onLayout={handleLayout}
|
|
114
|
+
pointerEvents={isDecorative ? 'none' : 'auto'}
|
|
115
|
+
accessibilityElementsHidden={isDecorative}
|
|
116
|
+
importantForAccessibility={isDecorative ? 'no-hide-descendants' : 'auto'}
|
|
117
|
+
className={slots[side]({ className })}
|
|
118
|
+
{...props}
|
|
119
|
+
>
|
|
120
|
+
{children}
|
|
121
|
+
</View>
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
);
|
|
125
|
+
Decorator.displayName = side === 'prefix' ? 'InputGroup.Prefix' : 'InputGroup.Suffix';
|
|
126
|
+
return Decorator;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const InputGroupPrefix = createDecorator('prefix');
|
|
130
|
+
const InputGroupSuffix = createDecorator('suffix');
|
|
131
|
+
|
|
132
|
+
export type InputGroupInputProps = InputProps;
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* The Input itself. Receives left/right padding matching the measured
|
|
136
|
+
* decorator widths, so its text always clears them.
|
|
137
|
+
*/
|
|
138
|
+
const InputGroupInput = forwardRef<TextInput, InputGroupInputProps>(
|
|
139
|
+
({ style, disabled, ...props }, ref) => {
|
|
140
|
+
const group = useInputGroup();
|
|
141
|
+
|
|
142
|
+
return (
|
|
143
|
+
<Input
|
|
144
|
+
ref={ref}
|
|
145
|
+
disabled={disabled ?? group?.isDisabled}
|
|
146
|
+
style={[
|
|
147
|
+
// Only override padding on the side that actually has a decorator —
|
|
148
|
+
// a 0 here would wipe out the field's default horizontal padding.
|
|
149
|
+
group?.prefixWidth ? { paddingLeft: group.prefixWidth } : null,
|
|
150
|
+
group?.suffixWidth ? { paddingRight: group.suffixWidth } : null,
|
|
151
|
+
style,
|
|
152
|
+
]}
|
|
153
|
+
{...props}
|
|
154
|
+
/>
|
|
155
|
+
);
|
|
156
|
+
}
|
|
157
|
+
);
|
|
158
|
+
InputGroupInput.displayName = 'InputGroup.Input';
|
|
159
|
+
|
|
160
|
+
export const InputGroup = Object.assign(InputGroupRoot, {
|
|
161
|
+
Prefix: InputGroupPrefix,
|
|
162
|
+
Input: InputGroupInput,
|
|
163
|
+
Suffix: InputGroupSuffix,
|
|
164
|
+
});
|