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,5 +1,28 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Frame — a widget shell: a titled surface wrapping an inset panel of rows,
|
|
3
|
+
* with an optional action on the header row and a caption underneath.
|
|
4
|
+
*
|
|
5
|
+
* Layout follows the nested-radius rule (inner radius = outer radius minus the
|
|
6
|
+
* gap between them), which is why the panel is `rounded-xl` inside a
|
|
7
|
+
* `rounded-2xl` shell. Since v0.4.0 the radius scale is theme-scoped, so this
|
|
8
|
+
* relationship holds while the absolute values change per theme.
|
|
9
|
+
*
|
|
10
|
+
* ```tsx
|
|
11
|
+
* <Frame>
|
|
12
|
+
* <Frame.Header>
|
|
13
|
+
* <Frame.Title>Usage Type</Frame.Title>
|
|
14
|
+
* <Frame.Action>Amount</Frame.Action>
|
|
15
|
+
* </Frame.Header>
|
|
16
|
+
* <Frame.Panel>
|
|
17
|
+
* <Frame.Row>…</Frame.Row>
|
|
18
|
+
* <Frame.Row divided>…</Frame.Row>
|
|
19
|
+
* </Frame.Panel>
|
|
20
|
+
* <Frame.Footer>Updated 2 minutes ago</Frame.Footer>
|
|
21
|
+
* </Frame>
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
import { forwardRef, type ReactNode } from 'react';
|
|
25
|
+
import { View, type Text as RNText, type ViewProps } from 'react-native';
|
|
3
26
|
import { Text, type TextProps } from '../../primitives/text';
|
|
4
27
|
import { cn } from '../../utils/cn';
|
|
5
28
|
|
|
@@ -7,39 +30,73 @@ export interface FrameProps extends ViewProps {
|
|
|
7
30
|
className?: string;
|
|
8
31
|
}
|
|
9
32
|
|
|
10
|
-
/**
|
|
11
|
-
* Grouping container (Coss UI's CardFrame): a tinted, rounded surface that
|
|
12
|
-
* holds a header, one or more inset panels, and a footer. Use it to group
|
|
13
|
-
* related settings or list sections under a single title.
|
|
14
|
-
*/
|
|
15
33
|
const FrameRoot = forwardRef<View, FrameProps>(({ className, ...props }, ref) => (
|
|
16
34
|
<View
|
|
17
35
|
ref={ref}
|
|
18
|
-
className={cn('
|
|
36
|
+
className={cn('rounded-2xl border border-border bg-surface p-1.5', className)}
|
|
19
37
|
{...props}
|
|
20
38
|
/>
|
|
21
39
|
));
|
|
22
40
|
FrameRoot.displayName = 'Frame';
|
|
23
41
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
FrameHeader.displayName = 'Frame.Header';
|
|
42
|
+
export interface FrameHeaderProps extends FrameProps {
|
|
43
|
+
children?: ReactNode;
|
|
44
|
+
}
|
|
28
45
|
|
|
29
|
-
|
|
46
|
+
/**
|
|
47
|
+
* The header row: title on the left, `Frame.Action` on the right. Add
|
|
48
|
+
* `className="flex-col items-start"` when you want a description underneath.
|
|
49
|
+
*/
|
|
50
|
+
const FrameHeader = forwardRef<View, FrameHeaderProps>(
|
|
30
51
|
({ className, ...props }, ref) => (
|
|
31
|
-
<
|
|
52
|
+
<View
|
|
32
53
|
ref={ref}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
54
|
+
className={cn(
|
|
55
|
+
'flex-row items-center justify-between gap-3 px-3 pb-2.5 pt-2',
|
|
56
|
+
className
|
|
57
|
+
)}
|
|
36
58
|
{...props}
|
|
37
59
|
/>
|
|
38
60
|
)
|
|
39
61
|
);
|
|
62
|
+
FrameHeader.displayName = 'Frame.Header';
|
|
63
|
+
|
|
64
|
+
const FrameTitle = forwardRef<RNText, TextProps>(({ className, ...props }, ref) => (
|
|
65
|
+
<Text
|
|
66
|
+
ref={ref}
|
|
67
|
+
size="base"
|
|
68
|
+
weight="medium"
|
|
69
|
+
className={cn('text-foreground', className)}
|
|
70
|
+
{...props}
|
|
71
|
+
/>
|
|
72
|
+
));
|
|
40
73
|
FrameTitle.displayName = 'Frame.Title';
|
|
41
74
|
|
|
42
|
-
|
|
75
|
+
export interface FrameActionProps extends FrameProps {
|
|
76
|
+
children?: ReactNode;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Trailing slot on the header row — a column label, a count, a button, a badge.
|
|
81
|
+
* Plain strings render as muted text; anything else renders as-is.
|
|
82
|
+
*/
|
|
83
|
+
const FrameAction = forwardRef<View, FrameActionProps>(
|
|
84
|
+
({ className, children, ...props }, ref) => (
|
|
85
|
+
<View ref={ref} className={cn('flex-row items-center gap-2', className)} {...props}>
|
|
86
|
+
{typeof children === 'string' ? (
|
|
87
|
+
<Text size="base" muted>
|
|
88
|
+
{children}
|
|
89
|
+
</Text>
|
|
90
|
+
) : (
|
|
91
|
+
children
|
|
92
|
+
)}
|
|
93
|
+
</View>
|
|
94
|
+
)
|
|
95
|
+
);
|
|
96
|
+
FrameAction.displayName = 'Frame.Action';
|
|
97
|
+
|
|
98
|
+
/** Secondary line under the title, inside a column-wrapped header. */
|
|
99
|
+
const FrameDescription = forwardRef<RNText, TextProps>(
|
|
43
100
|
({ className, ...props }, ref) => (
|
|
44
101
|
<Text ref={ref} size="sm" muted className={className} {...props} />
|
|
45
102
|
)
|
|
@@ -47,16 +104,13 @@ const FrameDescription = forwardRef<React.ElementRef<typeof Text>, TextProps>(
|
|
|
47
104
|
FrameDescription.displayName = 'Frame.Description';
|
|
48
105
|
|
|
49
106
|
/**
|
|
50
|
-
*
|
|
51
|
-
*
|
|
107
|
+
* The inset card holding the frame's content — the raised surface sitting in
|
|
108
|
+
* from the shell's edges, with its own smaller radius.
|
|
52
109
|
*/
|
|
53
110
|
const FramePanel = forwardRef<View, FrameProps>(({ className, ...props }, ref) => (
|
|
54
111
|
<View
|
|
55
112
|
ref={ref}
|
|
56
|
-
className={cn(
|
|
57
|
-
'mx-1.5 overflow-hidden rounded-xl border border-border bg-card',
|
|
58
|
-
className
|
|
59
|
-
)}
|
|
113
|
+
className={cn('overflow-hidden rounded-xl border border-border bg-card', className)}
|
|
60
114
|
{...props}
|
|
61
115
|
/>
|
|
62
116
|
));
|
|
@@ -85,18 +139,34 @@ const FrameRow = forwardRef<View, FrameRowProps>(
|
|
|
85
139
|
);
|
|
86
140
|
FrameRow.displayName = 'Frame.Row';
|
|
87
141
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
)
|
|
142
|
+
export interface FrameFooterProps extends FrameProps {
|
|
143
|
+
children?: ReactNode;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Muted caption under the panel. Strings are wrapped for you. */
|
|
147
|
+
const FrameFooter = forwardRef<View, FrameFooterProps>(
|
|
148
|
+
({ className, children, ...props }, ref) => (
|
|
149
|
+
<View
|
|
150
|
+
ref={ref}
|
|
151
|
+
className={cn('flex-row items-center gap-2 px-3 pb-1.5 pt-2.5', className)}
|
|
152
|
+
{...props}
|
|
153
|
+
>
|
|
154
|
+
{typeof children === 'string' ? (
|
|
155
|
+
<Text size="sm" muted>
|
|
156
|
+
{children}
|
|
157
|
+
</Text>
|
|
158
|
+
) : (
|
|
159
|
+
children
|
|
160
|
+
)}
|
|
161
|
+
</View>
|
|
162
|
+
)
|
|
163
|
+
);
|
|
95
164
|
FrameFooter.displayName = 'Frame.Footer';
|
|
96
165
|
|
|
97
166
|
export const Frame = Object.assign(FrameRoot, {
|
|
98
167
|
Header: FrameHeader,
|
|
99
168
|
Title: FrameTitle,
|
|
169
|
+
Action: FrameAction,
|
|
100
170
|
Description: FrameDescription,
|
|
101
171
|
Panel: FramePanel,
|
|
102
172
|
Row: FrameRow,
|
|
@@ -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
|
+
});
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Label — text that names a form field.
|
|
3
|
+
*
|
|
4
|
+
* Adapted from: heroui-inc/heroui-native src/components/label/ (root +
|
|
5
|
+
* Label.Text anatomy, string children auto-wrapping, the required asterisk,
|
|
6
|
+
* and the isInvalid / isDisabled recolouring).
|
|
7
|
+
*/
|
|
8
|
+
import {
|
|
9
|
+
createContext,
|
|
10
|
+
forwardRef,
|
|
11
|
+
useContext,
|
|
12
|
+
useMemo,
|
|
13
|
+
type ReactNode,
|
|
14
|
+
} from 'react';
|
|
15
|
+
import { View, type Text as RNText, type ViewProps } from 'react-native';
|
|
16
|
+
import { tv } from 'tailwind-variants';
|
|
17
|
+
import { Text, type TextProps } from '../../primitives/text';
|
|
18
|
+
|
|
19
|
+
const labelVariants = tv({
|
|
20
|
+
slots: {
|
|
21
|
+
root: 'flex-row items-center gap-1',
|
|
22
|
+
text: 'text-sm font-medium text-foreground',
|
|
23
|
+
asterisk: 'text-sm font-medium text-destructive',
|
|
24
|
+
},
|
|
25
|
+
variants: {
|
|
26
|
+
isDisabled: {
|
|
27
|
+
true: {
|
|
28
|
+
root: 'opacity-[0.64]',
|
|
29
|
+
asterisk: 'text-muted-foreground',
|
|
30
|
+
},
|
|
31
|
+
},
|
|
32
|
+
isInvalid: {
|
|
33
|
+
true: { text: 'text-destructive' },
|
|
34
|
+
},
|
|
35
|
+
},
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
interface LabelState {
|
|
39
|
+
isRequired: boolean;
|
|
40
|
+
isInvalid: boolean;
|
|
41
|
+
isDisabled: boolean;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const LabelContext = createContext<LabelState>({
|
|
45
|
+
isRequired: false,
|
|
46
|
+
isInvalid: false,
|
|
47
|
+
isDisabled: false,
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
export interface LabelProps extends ViewProps {
|
|
51
|
+
className?: string;
|
|
52
|
+
/** Appends an asterisk marking the field as required. */
|
|
53
|
+
isRequired?: boolean;
|
|
54
|
+
/** Recolours the label to signal a validation error. */
|
|
55
|
+
isInvalid?: boolean;
|
|
56
|
+
isDisabled?: boolean;
|
|
57
|
+
children?: ReactNode;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const LabelRoot = forwardRef<View, LabelProps>(
|
|
61
|
+
(
|
|
62
|
+
{
|
|
63
|
+
className,
|
|
64
|
+
isRequired = false,
|
|
65
|
+
isInvalid = false,
|
|
66
|
+
isDisabled = false,
|
|
67
|
+
children,
|
|
68
|
+
...props
|
|
69
|
+
},
|
|
70
|
+
ref
|
|
71
|
+
) => {
|
|
72
|
+
const { root } = labelVariants({ isDisabled, isInvalid });
|
|
73
|
+
const state = useMemo(
|
|
74
|
+
() => ({ isRequired, isInvalid, isDisabled }),
|
|
75
|
+
[isRequired, isInvalid, isDisabled]
|
|
76
|
+
);
|
|
77
|
+
|
|
78
|
+
return (
|
|
79
|
+
<LabelContext.Provider value={state}>
|
|
80
|
+
<View
|
|
81
|
+
ref={ref}
|
|
82
|
+
className={root({ className })}
|
|
83
|
+
accessibilityState={{ disabled: isDisabled }}
|
|
84
|
+
{...props}
|
|
85
|
+
>
|
|
86
|
+
{/* Bare strings are the common case — wrap them so callers don't have to. */}
|
|
87
|
+
{typeof children === 'string' ? <LabelText>{children}</LabelText> : children}
|
|
88
|
+
</View>
|
|
89
|
+
</LabelContext.Provider>
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
);
|
|
93
|
+
LabelRoot.displayName = 'Label';
|
|
94
|
+
|
|
95
|
+
export interface LabelTextProps extends TextProps {
|
|
96
|
+
className?: string;
|
|
97
|
+
/** Classes for the required asterisk. */
|
|
98
|
+
asteriskClassName?: string;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const LabelText = forwardRef<RNText, LabelTextProps>(
|
|
102
|
+
({ className, asteriskClassName, children, ...props }, ref) => {
|
|
103
|
+
const { isRequired, isInvalid, isDisabled } = useContext(LabelContext);
|
|
104
|
+
const { text, asterisk } = labelVariants({ isDisabled, isInvalid });
|
|
105
|
+
|
|
106
|
+
return (
|
|
107
|
+
<>
|
|
108
|
+
<Text ref={ref} className={text({ className })} {...props}>
|
|
109
|
+
{children}
|
|
110
|
+
</Text>
|
|
111
|
+
{isRequired ? (
|
|
112
|
+
<Text
|
|
113
|
+
accessibilityElementsHidden
|
|
114
|
+
importantForAccessibility="no-hide-descendants"
|
|
115
|
+
className={asterisk({ className: asteriskClassName })}
|
|
116
|
+
>
|
|
117
|
+
*
|
|
118
|
+
</Text>
|
|
119
|
+
) : null}
|
|
120
|
+
</>
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
);
|
|
124
|
+
LabelText.displayName = 'Label.Text';
|
|
125
|
+
|
|
126
|
+
export const Label = Object.assign(LabelRoot, {
|
|
127
|
+
Text: LabelText,
|
|
128
|
+
});
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { forwardRef, useEffect } from 'react';
|
|
2
|
+
import { View, type LayoutChangeEvent, type ViewProps } from 'react-native';
|
|
3
|
+
import Animated, {
|
|
4
|
+
Easing,
|
|
5
|
+
cancelAnimation,
|
|
6
|
+
interpolate,
|
|
7
|
+
useAnimatedStyle,
|
|
8
|
+
useSharedValue,
|
|
9
|
+
withRepeat,
|
|
10
|
+
withSpring,
|
|
11
|
+
withTiming,
|
|
12
|
+
} from 'react-native-reanimated';
|
|
13
|
+
import { tv, type VariantProps } from 'tailwind-variants';
|
|
14
|
+
|
|
15
|
+
const SPRING = { damping: 20, stiffness: 180, mass: 0.6 } as const;
|
|
16
|
+
/** Fraction of the track covered by the sliding bar in indeterminate mode. */
|
|
17
|
+
const INDETERMINATE_WIDTH = 0.4;
|
|
18
|
+
|
|
19
|
+
const progressVariants = tv({
|
|
20
|
+
slots: {
|
|
21
|
+
track: 'w-full overflow-hidden rounded-full',
|
|
22
|
+
indicator: 'h-full rounded-full',
|
|
23
|
+
},
|
|
24
|
+
variants: {
|
|
25
|
+
color: {
|
|
26
|
+
primary: { track: 'bg-primary/16', indicator: 'bg-primary' },
|
|
27
|
+
success: { track: 'bg-success/16', indicator: 'bg-success' },
|
|
28
|
+
warning: { track: 'bg-warning/16', indicator: 'bg-warning' },
|
|
29
|
+
destructive: { track: 'bg-destructive/16', indicator: 'bg-destructive' },
|
|
30
|
+
info: { track: 'bg-info/16', indicator: 'bg-info' },
|
|
31
|
+
},
|
|
32
|
+
size: {
|
|
33
|
+
sm: { track: 'h-1.5' },
|
|
34
|
+
md: { track: 'h-2' },
|
|
35
|
+
lg: { track: 'h-3' },
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
defaultVariants: {
|
|
39
|
+
color: 'primary',
|
|
40
|
+
size: 'md',
|
|
41
|
+
},
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
type ProgressVariantProps = VariantProps<typeof progressVariants>;
|
|
45
|
+
|
|
46
|
+
export interface ProgressProps
|
|
47
|
+
extends Omit<ViewProps, 'children'>,
|
|
48
|
+
ProgressVariantProps {
|
|
49
|
+
className?: string;
|
|
50
|
+
/** Progress value, 0–100. Ignored when `indeterminate` is set. */
|
|
51
|
+
value?: number;
|
|
52
|
+
/** Show a looping animation for unknown-duration work. */
|
|
53
|
+
indeterminate?: boolean;
|
|
54
|
+
/** Extra classes for the moving indicator. */
|
|
55
|
+
indicatorClassName?: string;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function clamp(value: number) {
|
|
59
|
+
'worklet';
|
|
60
|
+
if (value < 0) return 0;
|
|
61
|
+
if (value > 100) return 100;
|
|
62
|
+
return value;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Determinate or indeterminate progress bar. The fill width (determinate) and
|
|
67
|
+
* the sliding bar (indeterminate) are both driven on the UI thread, so updates
|
|
68
|
+
* never re-render past the value change itself.
|
|
69
|
+
*/
|
|
70
|
+
export const Progress = forwardRef<View, ProgressProps>(
|
|
71
|
+
(
|
|
72
|
+
{
|
|
73
|
+
className,
|
|
74
|
+
indicatorClassName,
|
|
75
|
+
value = 0,
|
|
76
|
+
indeterminate = false,
|
|
77
|
+
color,
|
|
78
|
+
size,
|
|
79
|
+
...props
|
|
80
|
+
},
|
|
81
|
+
ref
|
|
82
|
+
) => {
|
|
83
|
+
const slots = progressVariants({ color, size });
|
|
84
|
+
const trackWidth = useSharedValue(0);
|
|
85
|
+
const progress = useSharedValue(clamp(value) / 100);
|
|
86
|
+
const slide = useSharedValue(0);
|
|
87
|
+
|
|
88
|
+
const target = clamp(value) / 100;
|
|
89
|
+
|
|
90
|
+
useEffect(() => {
|
|
91
|
+
if (indeterminate) {
|
|
92
|
+
slide.value = withRepeat(
|
|
93
|
+
withTiming(1, { duration: 1100, easing: Easing.inOut(Easing.ease) }),
|
|
94
|
+
-1,
|
|
95
|
+
false
|
|
96
|
+
);
|
|
97
|
+
return () => cancelAnimation(slide);
|
|
98
|
+
}
|
|
99
|
+
progress.value = withSpring(target, SPRING);
|
|
100
|
+
return undefined;
|
|
101
|
+
}, [indeterminate, target, progress, slide]);
|
|
102
|
+
|
|
103
|
+
const onLayout = (event: LayoutChangeEvent) => {
|
|
104
|
+
trackWidth.value = event.nativeEvent.layout.width;
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const determinateStyle = useAnimatedStyle(() => ({
|
|
108
|
+
width: trackWidth.value * progress.value,
|
|
109
|
+
}));
|
|
110
|
+
|
|
111
|
+
const indeterminateStyle = useAnimatedStyle(() => {
|
|
112
|
+
const barWidth = trackWidth.value * INDETERMINATE_WIDTH;
|
|
113
|
+
return {
|
|
114
|
+
width: barWidth,
|
|
115
|
+
transform: [
|
|
116
|
+
{
|
|
117
|
+
translateX: interpolate(
|
|
118
|
+
slide.value,
|
|
119
|
+
[0, 1],
|
|
120
|
+
[-barWidth, trackWidth.value]
|
|
121
|
+
),
|
|
122
|
+
},
|
|
123
|
+
],
|
|
124
|
+
};
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
return (
|
|
128
|
+
<View
|
|
129
|
+
ref={ref}
|
|
130
|
+
accessibilityRole="progressbar"
|
|
131
|
+
accessibilityValue={
|
|
132
|
+
indeterminate ? undefined : { min: 0, max: 100, now: clamp(value) }
|
|
133
|
+
}
|
|
134
|
+
className={slots.track({ className })}
|
|
135
|
+
onLayout={onLayout}
|
|
136
|
+
{...props}
|
|
137
|
+
>
|
|
138
|
+
<Animated.View
|
|
139
|
+
style={indeterminate ? indeterminateStyle : determinateStyle}
|
|
140
|
+
className={slots.indicator({ className: indicatorClassName })}
|
|
141
|
+
/>
|
|
142
|
+
</View>
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
Progress.displayName = 'Progress';
|