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.
Files changed (106) hide show
  1. package/README.md +258 -0
  2. package/lib/module/components/accordion/index.js +312 -0
  3. package/lib/module/components/accordion/index.js.map +1 -0
  4. package/lib/module/components/alert/index.js +106 -13
  5. package/lib/module/components/alert/index.js.map +1 -1
  6. package/lib/module/components/avatar/index.js +58 -17
  7. package/lib/module/components/avatar/index.js.map +1 -1
  8. package/lib/module/components/badge/index.js +29 -8
  9. package/lib/module/components/badge/index.js.map +1 -1
  10. package/lib/module/components/button/index.js +87 -26
  11. package/lib/module/components/button/index.js.map +1 -1
  12. package/lib/module/components/checkbox/index.js +59 -18
  13. package/lib/module/components/checkbox/index.js.map +1 -1
  14. package/lib/module/components/empty-state/index.js +178 -0
  15. package/lib/module/components/empty-state/index.js.map +1 -0
  16. package/lib/module/components/frame/index.js +64 -13
  17. package/lib/module/components/frame/index.js.map +1 -1
  18. package/lib/module/components/input-group/index.js +132 -0
  19. package/lib/module/components/input-group/index.js.map +1 -0
  20. package/lib/module/components/label/index.js +117 -0
  21. package/lib/module/components/label/index.js.map +1 -0
  22. package/lib/module/components/progress/index.js +138 -0
  23. package/lib/module/components/progress/index.js.map +1 -0
  24. package/lib/module/components/steps/index.js +354 -0
  25. package/lib/module/components/steps/index.js.map +1 -0
  26. package/lib/module/components/tabs/index.js +9 -3
  27. package/lib/module/components/tabs/index.js.map +1 -1
  28. package/lib/module/components/timeline/index.js +633 -0
  29. package/lib/module/components/timeline/index.js.map +1 -0
  30. package/lib/module/components/toast/index.js +569 -0
  31. package/lib/module/components/toast/index.js.map +1 -0
  32. package/lib/module/components/toast/toast-store.js +81 -0
  33. package/lib/module/components/toast/toast-store.js.map +1 -0
  34. package/lib/module/components/typography/index.js +126 -0
  35. package/lib/module/components/typography/index.js.map +1 -0
  36. package/lib/module/icons/index.js +590 -8
  37. package/lib/module/icons/index.js.map +1 -1
  38. package/lib/module/index.js +11 -2
  39. package/lib/module/index.js.map +1 -1
  40. package/lib/module/providers/panel-ui-provider.js +2 -1
  41. package/lib/module/providers/panel-ui-provider.js.map +1 -1
  42. package/lib/module/theme/use-theme.js +90 -3
  43. package/lib/module/theme/use-theme.js.map +1 -1
  44. package/lib/typescript/src/components/accordion/index.d.ts +56 -0
  45. package/lib/typescript/src/components/accordion/index.d.ts.map +1 -0
  46. package/lib/typescript/src/components/alert/index.d.ts +56 -4
  47. package/lib/typescript/src/components/alert/index.d.ts.map +1 -1
  48. package/lib/typescript/src/components/avatar/index.d.ts +8 -1
  49. package/lib/typescript/src/components/avatar/index.d.ts.map +1 -1
  50. package/lib/typescript/src/components/badge/index.d.ts +38 -0
  51. package/lib/typescript/src/components/badge/index.d.ts.map +1 -1
  52. package/lib/typescript/src/components/button/index.d.ts +57 -0
  53. package/lib/typescript/src/components/button/index.d.ts.map +1 -1
  54. package/lib/typescript/src/components/checkbox/index.d.ts +50 -0
  55. package/lib/typescript/src/components/checkbox/index.d.ts.map +1 -1
  56. package/lib/typescript/src/components/empty-state/index.d.ts +73 -0
  57. package/lib/typescript/src/components/empty-state/index.d.ts.map +1 -0
  58. package/lib/typescript/src/components/frame/index.d.ts +39 -5
  59. package/lib/typescript/src/components/frame/index.d.ts.map +1 -1
  60. package/lib/typescript/src/components/input-group/index.d.ts +34 -0
  61. package/lib/typescript/src/components/input-group/index.d.ts.map +1 -0
  62. package/lib/typescript/src/components/label/index.d.ts +28 -0
  63. package/lib/typescript/src/components/label/index.d.ts.map +1 -0
  64. package/lib/typescript/src/components/progress/index.d.ts +132 -0
  65. package/lib/typescript/src/components/progress/index.d.ts.map +1 -0
  66. package/lib/typescript/src/components/steps/index.d.ts +62 -0
  67. package/lib/typescript/src/components/steps/index.d.ts.map +1 -0
  68. package/lib/typescript/src/components/tabs/index.d.ts.map +1 -1
  69. package/lib/typescript/src/components/timeline/index.d.ts +96 -0
  70. package/lib/typescript/src/components/timeline/index.d.ts.map +1 -0
  71. package/lib/typescript/src/components/toast/index.d.ts +176 -0
  72. package/lib/typescript/src/components/toast/index.d.ts.map +1 -0
  73. package/lib/typescript/src/components/toast/toast-store.d.ts +65 -0
  74. package/lib/typescript/src/components/toast/toast-store.d.ts.map +1 -0
  75. package/lib/typescript/src/components/typography/index.d.ts +77 -0
  76. package/lib/typescript/src/components/typography/index.d.ts.map +1 -0
  77. package/lib/typescript/src/icons/index.d.ts +60 -0
  78. package/lib/typescript/src/icons/index.d.ts.map +1 -1
  79. package/lib/typescript/src/index.d.ts +13 -4
  80. package/lib/typescript/src/index.d.ts.map +1 -1
  81. package/lib/typescript/src/providers/panel-ui-provider.d.ts.map +1 -1
  82. package/lib/typescript/src/theme/use-theme.d.ts +64 -4
  83. package/lib/typescript/src/theme/use-theme.d.ts.map +1 -1
  84. package/package.json +22 -4
  85. package/src/components/accordion/index.tsx +344 -0
  86. package/src/components/alert/index.tsx +102 -16
  87. package/src/components/avatar/index.tsx +68 -20
  88. package/src/components/badge/index.tsx +38 -9
  89. package/src/components/button/index.tsx +102 -19
  90. package/src/components/checkbox/index.tsx +64 -12
  91. package/src/components/empty-state/index.tsx +136 -0
  92. package/src/components/frame/index.tsx +101 -31
  93. package/src/components/input-group/index.tsx +164 -0
  94. package/src/components/label/index.tsx +128 -0
  95. package/src/components/progress/index.tsx +147 -0
  96. package/src/components/steps/index.tsx +343 -0
  97. package/src/components/tabs/index.tsx +10 -2
  98. package/src/components/timeline/index.tsx +456 -0
  99. package/src/components/toast/index.tsx +568 -0
  100. package/src/components/toast/toast-store.ts +120 -0
  101. package/src/components/typography/index.tsx +121 -0
  102. package/src/icons/index.tsx +423 -7
  103. package/src/index.ts +114 -4
  104. package/src/providers/panel-ui-provider.tsx +3 -0
  105. package/src/theme/use-theme.ts +119 -4
  106. package/theme.css +330 -7
@@ -1,5 +1,28 @@
1
- import { forwardRef } from 'react';
2
- import { View, type ViewProps } from 'react-native';
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('gap-2 rounded-2xl border border-border bg-surface py-2', className)}
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
- const FrameHeader = forwardRef<View, FrameProps>(({ className, ...props }, ref) => (
25
- <View ref={ref} className={cn('gap-1 px-5 py-2', className)} {...props} />
26
- ));
27
- FrameHeader.displayName = 'Frame.Header';
42
+ export interface FrameHeaderProps extends FrameProps {
43
+ children?: ReactNode;
44
+ }
28
45
 
29
- const FrameTitle = forwardRef<React.ElementRef<typeof Text>, TextProps>(
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
- <Text
52
+ <View
32
53
  ref={ref}
33
- size="base"
34
- weight="semibold"
35
- className={cn('text-foreground', className)}
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
- const FrameDescription = forwardRef<React.ElementRef<typeof Text>, TextProps>(
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
- * An inset card surface inside a Frame — the raised panel that holds the
51
- * frame's actual content, sitting slightly in from the frame's edges.
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
- const FrameFooter = forwardRef<View, FrameProps>(({ className, ...props }, ref) => (
89
- <View
90
- ref={ref}
91
- className={cn('flex-row items-center gap-2 px-5 py-2', className)}
92
- {...props}
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';