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
|
@@ -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';
|
|
@@ -0,0 +1,343 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Steps — a stepper for multi-step flows.
|
|
3
|
+
*
|
|
4
|
+
* Adapted from: origin-space/originui apps/origin/registry/default/ui/stepper.tsx
|
|
5
|
+
* (the Stepper / Item / Trigger / Indicator / Title / Description / Separator
|
|
6
|
+
* anatomy, and the completed | active | inactive | loading state resolution).
|
|
7
|
+
*
|
|
8
|
+
* Upstream drives all styling through `data-*` attributes, which React Native
|
|
9
|
+
* has no equivalent for. Here the state is resolved in JS and passed down
|
|
10
|
+
* through context into `tv()` variants — the same approach Alert uses.
|
|
11
|
+
*
|
|
12
|
+
* Steps does not own your flow: it reflects whatever step your app says is
|
|
13
|
+
* active. Pass `value` to control it, or `defaultValue` to let it manage
|
|
14
|
+
* its own.
|
|
15
|
+
*/
|
|
16
|
+
import {
|
|
17
|
+
createContext,
|
|
18
|
+
forwardRef,
|
|
19
|
+
useCallback,
|
|
20
|
+
useContext,
|
|
21
|
+
useMemo,
|
|
22
|
+
useState,
|
|
23
|
+
type ReactNode,
|
|
24
|
+
} from 'react';
|
|
25
|
+
import { Pressable, View, type Text as RNText, type ViewProps } from 'react-native';
|
|
26
|
+
import { tv } from 'tailwind-variants';
|
|
27
|
+
import { useCSSVariable } from 'uniwind';
|
|
28
|
+
import { CheckIcon } from '../../icons';
|
|
29
|
+
import { Text, type TextProps } from '../../primitives/text';
|
|
30
|
+
import { Spinner } from '../spinner';
|
|
31
|
+
|
|
32
|
+
export type StepState = 'active' | 'completed' | 'inactive' | 'loading';
|
|
33
|
+
export type StepsOrientation = 'horizontal' | 'vertical';
|
|
34
|
+
|
|
35
|
+
const stepsVariants = tv({
|
|
36
|
+
slots: {
|
|
37
|
+
root: '',
|
|
38
|
+
item: 'items-center',
|
|
39
|
+
trigger: 'flex-row items-center gap-3',
|
|
40
|
+
indicator: 'h-7 w-7 shrink-0 items-center justify-center rounded-full',
|
|
41
|
+
indicatorLabel: 'text-xs font-medium',
|
|
42
|
+
title: 'text-sm font-medium text-foreground',
|
|
43
|
+
description: 'text-sm text-muted-foreground',
|
|
44
|
+
separator: 'bg-muted',
|
|
45
|
+
},
|
|
46
|
+
variants: {
|
|
47
|
+
orientation: {
|
|
48
|
+
horizontal: {
|
|
49
|
+
root: 'w-full flex-row items-center',
|
|
50
|
+
item: 'flex-row',
|
|
51
|
+
separator: 'h-0.5 flex-1',
|
|
52
|
+
},
|
|
53
|
+
vertical: {
|
|
54
|
+
root: 'flex-col',
|
|
55
|
+
item: 'flex-col items-start',
|
|
56
|
+
separator: 'ml-3.5 h-8 w-0.5',
|
|
57
|
+
},
|
|
58
|
+
},
|
|
59
|
+
state: {
|
|
60
|
+
inactive: {
|
|
61
|
+
indicator: 'bg-muted',
|
|
62
|
+
indicatorLabel: 'text-muted-foreground',
|
|
63
|
+
},
|
|
64
|
+
active: {
|
|
65
|
+
indicator: 'bg-primary',
|
|
66
|
+
indicatorLabel: 'text-primary-foreground',
|
|
67
|
+
},
|
|
68
|
+
completed: {
|
|
69
|
+
indicator: 'bg-primary',
|
|
70
|
+
indicatorLabel: 'text-primary-foreground',
|
|
71
|
+
separator: 'bg-primary',
|
|
72
|
+
},
|
|
73
|
+
loading: {
|
|
74
|
+
indicator: 'bg-muted',
|
|
75
|
+
indicatorLabel: 'text-muted-foreground',
|
|
76
|
+
},
|
|
77
|
+
},
|
|
78
|
+
isDisabled: {
|
|
79
|
+
true: { trigger: 'opacity-[0.64]' },
|
|
80
|
+
},
|
|
81
|
+
},
|
|
82
|
+
defaultVariants: {
|
|
83
|
+
orientation: 'horizontal',
|
|
84
|
+
state: 'inactive',
|
|
85
|
+
},
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
interface StepsContextValue {
|
|
89
|
+
activeStep: number;
|
|
90
|
+
setActiveStep: (step: number) => void;
|
|
91
|
+
orientation: StepsOrientation;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
interface StepItemContextValue {
|
|
95
|
+
step: number;
|
|
96
|
+
state: StepState;
|
|
97
|
+
isDisabled: boolean;
|
|
98
|
+
isLoading: boolean;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const StepsContext = createContext<StepsContextValue | null>(null);
|
|
102
|
+
const StepItemContext = createContext<StepItemContextValue | null>(null);
|
|
103
|
+
|
|
104
|
+
function useSteps(component: string): StepsContextValue {
|
|
105
|
+
const context = useContext(StepsContext);
|
|
106
|
+
if (!context) {
|
|
107
|
+
throw new Error(`${component} must be used within a <Steps>`);
|
|
108
|
+
}
|
|
109
|
+
return context;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function useStepItem(component: string): StepItemContextValue {
|
|
113
|
+
const context = useContext(StepItemContext);
|
|
114
|
+
if (!context) {
|
|
115
|
+
throw new Error(`${component} must be used within a <Steps.Item>`);
|
|
116
|
+
}
|
|
117
|
+
return context;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export interface StepsProps extends ViewProps {
|
|
121
|
+
className?: string;
|
|
122
|
+
/** Active step when uncontrolled. */
|
|
123
|
+
defaultValue?: number;
|
|
124
|
+
/** Active step, controlled. */
|
|
125
|
+
value?: number;
|
|
126
|
+
onValueChange?: (value: number) => void;
|
|
127
|
+
orientation?: StepsOrientation;
|
|
128
|
+
children?: ReactNode;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const StepsRoot = forwardRef<View, StepsProps>(
|
|
132
|
+
(
|
|
133
|
+
{
|
|
134
|
+
className,
|
|
135
|
+
defaultValue = 0,
|
|
136
|
+
value,
|
|
137
|
+
onValueChange,
|
|
138
|
+
orientation = 'horizontal',
|
|
139
|
+
children,
|
|
140
|
+
...props
|
|
141
|
+
},
|
|
142
|
+
ref
|
|
143
|
+
) => {
|
|
144
|
+
const [internalStep, setInternalStep] = useState(defaultValue);
|
|
145
|
+
const isControlled = value !== undefined;
|
|
146
|
+
const activeStep = isControlled ? value : internalStep;
|
|
147
|
+
|
|
148
|
+
const setActiveStep = useCallback(
|
|
149
|
+
(step: number) => {
|
|
150
|
+
if (!isControlled) setInternalStep(step);
|
|
151
|
+
onValueChange?.(step);
|
|
152
|
+
},
|
|
153
|
+
[isControlled, onValueChange]
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
const context = useMemo(
|
|
157
|
+
() => ({ activeStep, setActiveStep, orientation }),
|
|
158
|
+
[activeStep, setActiveStep, orientation]
|
|
159
|
+
);
|
|
160
|
+
|
|
161
|
+
const { root } = stepsVariants({ orientation });
|
|
162
|
+
|
|
163
|
+
return (
|
|
164
|
+
<StepsContext.Provider value={context}>
|
|
165
|
+
<View ref={ref} className={root({ className })} {...props}>
|
|
166
|
+
{children}
|
|
167
|
+
</View>
|
|
168
|
+
</StepsContext.Provider>
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
);
|
|
172
|
+
StepsRoot.displayName = 'Steps';
|
|
173
|
+
|
|
174
|
+
export interface StepsItemProps extends ViewProps {
|
|
175
|
+
className?: string;
|
|
176
|
+
/** This item's position in the flow, zero-based by convention. */
|
|
177
|
+
step: number;
|
|
178
|
+
/** Force the completed state, regardless of the active step. */
|
|
179
|
+
completed?: boolean;
|
|
180
|
+
disabled?: boolean;
|
|
181
|
+
/** Shows a spinner in place of the number while this step is active. */
|
|
182
|
+
loading?: boolean;
|
|
183
|
+
children?: ReactNode;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const StepsItem = forwardRef<View, StepsItemProps>(
|
|
187
|
+
(
|
|
188
|
+
{ className, step, completed = false, disabled = false, loading = false, children, ...props },
|
|
189
|
+
ref
|
|
190
|
+
) => {
|
|
191
|
+
const { activeStep, orientation } = useSteps('Steps.Item');
|
|
192
|
+
|
|
193
|
+
const isLoading = loading && step === activeStep;
|
|
194
|
+
const state: StepState =
|
|
195
|
+
completed || step < activeStep
|
|
196
|
+
? 'completed'
|
|
197
|
+
: step === activeStep
|
|
198
|
+
? isLoading
|
|
199
|
+
? 'loading'
|
|
200
|
+
: 'active'
|
|
201
|
+
: 'inactive';
|
|
202
|
+
|
|
203
|
+
const context = useMemo(
|
|
204
|
+
() => ({ step, state, isDisabled: disabled, isLoading }),
|
|
205
|
+
[step, state, disabled, isLoading]
|
|
206
|
+
);
|
|
207
|
+
|
|
208
|
+
const { item } = stepsVariants({ orientation, state });
|
|
209
|
+
|
|
210
|
+
return (
|
|
211
|
+
<StepItemContext.Provider value={context}>
|
|
212
|
+
<View ref={ref} className={item({ className })} {...props}>
|
|
213
|
+
{children}
|
|
214
|
+
</View>
|
|
215
|
+
</StepItemContext.Provider>
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
);
|
|
219
|
+
StepsItem.displayName = 'Steps.Item';
|
|
220
|
+
|
|
221
|
+
export interface StepsTriggerProps extends ViewProps {
|
|
222
|
+
className?: string;
|
|
223
|
+
children?: ReactNode;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/** Makes its item selectable. Omit it for a read-only stepper. */
|
|
227
|
+
const StepsTrigger = forwardRef<View, StepsTriggerProps>(
|
|
228
|
+
({ className, children, ...props }, ref) => {
|
|
229
|
+
const { setActiveStep, orientation } = useSteps('Steps.Trigger');
|
|
230
|
+
const { step, state, isDisabled } = useStepItem('Steps.Trigger');
|
|
231
|
+
const { trigger } = stepsVariants({ orientation, state, isDisabled });
|
|
232
|
+
|
|
233
|
+
return (
|
|
234
|
+
<Pressable
|
|
235
|
+
ref={ref}
|
|
236
|
+
accessibilityRole="button"
|
|
237
|
+
accessibilityState={{ disabled: isDisabled, selected: state === 'active' }}
|
|
238
|
+
disabled={isDisabled}
|
|
239
|
+
onPress={() => setActiveStep(step)}
|
|
240
|
+
className={trigger({ className })}
|
|
241
|
+
{...props}
|
|
242
|
+
>
|
|
243
|
+
{children}
|
|
244
|
+
</Pressable>
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
);
|
|
248
|
+
StepsTrigger.displayName = 'Steps.Trigger';
|
|
249
|
+
|
|
250
|
+
export interface StepsIndicatorProps extends ViewProps {
|
|
251
|
+
className?: string;
|
|
252
|
+
/** Replaces the number / check / spinner entirely. */
|
|
253
|
+
children?: ReactNode;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* The circle. Shows the step number, a check once completed, or a spinner
|
|
258
|
+
* while loading.
|
|
259
|
+
*/
|
|
260
|
+
const StepsIndicator = forwardRef<View, StepsIndicatorProps>(
|
|
261
|
+
({ className, children, ...props }, ref) => {
|
|
262
|
+
const { orientation } = useSteps('Steps.Indicator');
|
|
263
|
+
const { step, state, isLoading } = useStepItem('Steps.Indicator');
|
|
264
|
+
const { indicator, indicatorLabel } = stepsVariants({ orientation, state });
|
|
265
|
+
const checkColor = useCSSVariable('--color-primary-foreground');
|
|
266
|
+
|
|
267
|
+
return (
|
|
268
|
+
<View ref={ref} className={indicator({ className })} {...props}>
|
|
269
|
+
{children ?? (
|
|
270
|
+
<>
|
|
271
|
+
{isLoading ? (
|
|
272
|
+
<Spinner size="sm" />
|
|
273
|
+
) : state === 'completed' ? (
|
|
274
|
+
<CheckIcon
|
|
275
|
+
size={14}
|
|
276
|
+
color={typeof checkColor === 'string' ? checkColor : '#fff'}
|
|
277
|
+
/>
|
|
278
|
+
) : (
|
|
279
|
+
// Steps are zero-based internally but read as 1, 2, 3.
|
|
280
|
+
<Text className={indicatorLabel()}>{step + 1}</Text>
|
|
281
|
+
)}
|
|
282
|
+
</>
|
|
283
|
+
)}
|
|
284
|
+
</View>
|
|
285
|
+
);
|
|
286
|
+
}
|
|
287
|
+
);
|
|
288
|
+
StepsIndicator.displayName = 'Steps.Indicator';
|
|
289
|
+
|
|
290
|
+
const StepsTitle = forwardRef<RNText, TextProps>(({ className, ...props }, ref) => {
|
|
291
|
+
const { orientation } = useSteps('Steps.Title');
|
|
292
|
+
const { state } = useStepItem('Steps.Title');
|
|
293
|
+
const { title } = stepsVariants({ orientation, state });
|
|
294
|
+
return <Text ref={ref} className={title({ className })} {...props} />;
|
|
295
|
+
});
|
|
296
|
+
StepsTitle.displayName = 'Steps.Title';
|
|
297
|
+
|
|
298
|
+
const StepsDescription = forwardRef<RNText, TextProps>(({ className, ...props }, ref) => {
|
|
299
|
+
const { orientation } = useSteps('Steps.Description');
|
|
300
|
+
const { state } = useStepItem('Steps.Description');
|
|
301
|
+
const { description } = stepsVariants({ orientation, state });
|
|
302
|
+
return <Text ref={ref} className={description({ className })} {...props} />;
|
|
303
|
+
});
|
|
304
|
+
StepsDescription.displayName = 'Steps.Description';
|
|
305
|
+
|
|
306
|
+
export interface StepsSeparatorProps extends ViewProps {
|
|
307
|
+
className?: string;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* The connector between two steps. Fills with the primary colour once the
|
|
312
|
+
* step before it is complete.
|
|
313
|
+
*
|
|
314
|
+
* Place it inside a Steps.Item so it can read that item's state — the
|
|
315
|
+
* separator after step 1 goes solid when step 1 is done.
|
|
316
|
+
*/
|
|
317
|
+
const StepsSeparator = forwardRef<View, StepsSeparatorProps>(
|
|
318
|
+
({ className, ...props }, ref) => {
|
|
319
|
+
const { orientation } = useSteps('Steps.Separator');
|
|
320
|
+
const { state } = useStepItem('Steps.Separator');
|
|
321
|
+
const { separator } = stepsVariants({ orientation, state });
|
|
322
|
+
|
|
323
|
+
return (
|
|
324
|
+
<View
|
|
325
|
+
ref={ref}
|
|
326
|
+
accessibilityElementsHidden
|
|
327
|
+
importantForAccessibility="no-hide-descendants"
|
|
328
|
+
className={separator({ className })}
|
|
329
|
+
{...props}
|
|
330
|
+
/>
|
|
331
|
+
);
|
|
332
|
+
}
|
|
333
|
+
);
|
|
334
|
+
StepsSeparator.displayName = 'Steps.Separator';
|
|
335
|
+
|
|
336
|
+
export const Steps = Object.assign(StepsRoot, {
|
|
337
|
+
Item: StepsItem,
|
|
338
|
+
Trigger: StepsTrigger,
|
|
339
|
+
Indicator: StepsIndicator,
|
|
340
|
+
Title: StepsTitle,
|
|
341
|
+
Description: StepsDescription,
|
|
342
|
+
Separator: StepsSeparator,
|
|
343
|
+
});
|
|
@@ -2,6 +2,7 @@ import {
|
|
|
2
2
|
createContext,
|
|
3
3
|
useCallback,
|
|
4
4
|
useContext,
|
|
5
|
+
useEffect,
|
|
5
6
|
useMemo,
|
|
6
7
|
useState,
|
|
7
8
|
type ReactNode,
|
|
@@ -100,8 +101,15 @@ function TabsIndicator() {
|
|
|
100
101
|
const initialized = useSharedValue(0);
|
|
101
102
|
|
|
102
103
|
const layout = layouts[value];
|
|
103
|
-
|
|
104
|
+
|
|
105
|
+
// In an effect, not the render body: touching a shared value during render
|
|
106
|
+
// is a Reanimated strict-mode violation, and the write can be lost or
|
|
107
|
+
// duplicated when React re-renders.
|
|
108
|
+
useEffect(() => {
|
|
109
|
+
if (!layout) return;
|
|
110
|
+
|
|
104
111
|
if (initialized.value === 0) {
|
|
112
|
+
// First measurement snaps into place; there is nothing to animate from.
|
|
105
113
|
x.value = layout.x;
|
|
106
114
|
width.value = layout.width;
|
|
107
115
|
initialized.value = 1;
|
|
@@ -109,7 +117,7 @@ function TabsIndicator() {
|
|
|
109
117
|
x.value = withSpring(layout.x, SPRING);
|
|
110
118
|
width.value = withSpring(layout.width, SPRING);
|
|
111
119
|
}
|
|
112
|
-
}
|
|
120
|
+
}, [layout?.x, layout?.width, x, width, initialized, layout]);
|
|
113
121
|
|
|
114
122
|
const style = useAnimatedStyle(() => ({
|
|
115
123
|
opacity: initialized.value,
|