@medway-ui/native 1.4.0 → 1.6.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 +1 -0
- package/lib/commonjs/components/LoadingOverlay.js +210 -0
- package/lib/commonjs/components/LoadingOverlay.js.map +1 -0
- package/lib/commonjs/components/Tooltip.js +2 -1
- package/lib/commonjs/components/Tooltip.js.map +1 -1
- package/lib/commonjs/hooks/useThemeColor.js +10 -3
- package/lib/commonjs/hooks/useThemeColor.js.map +1 -1
- package/lib/commonjs/index.js +19 -0
- package/lib/commonjs/index.js.map +1 -1
- package/lib/module/components/LoadingOverlay.js +204 -0
- package/lib/module/components/LoadingOverlay.js.map +1 -0
- package/lib/module/components/Tooltip.js +2 -1
- package/lib/module/components/Tooltip.js.map +1 -1
- package/lib/module/hooks/useThemeColor.js +10 -3
- package/lib/module/hooks/useThemeColor.js.map +1 -1
- package/lib/module/index.js +2 -0
- package/lib/module/index.js.map +1 -1
- package/lib/typescript/components/LoadingOverlay.d.ts +49 -0
- package/lib/typescript/components/LoadingOverlay.d.ts.map +1 -0
- package/lib/typescript/components/Tooltip.d.ts.map +1 -1
- package/lib/typescript/hooks/useThemeColor.d.ts.map +1 -1
- package/lib/typescript/index.d.ts +2 -0
- package/lib/typescript/index.d.ts.map +1 -1
- package/package.json +1 -1
- package/setup/babel-brownfield.js +1 -0
- package/setup/metro.js +8 -0
- package/setup/tokens.native.css +103 -0
- package/src/components/LoadingOverlay.tsx +308 -0
- package/src/components/Tooltip.tsx +2 -1
- package/src/hooks/useThemeColor.ts +7 -0
- package/src/index.ts +12 -0
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
import { useEffect, type ReactNode } from "react";
|
|
2
|
+
import { Modal, View, type ModalProps, type ViewProps } from "react-native";
|
|
3
|
+
import Svg, { Circle } from "react-native-svg";
|
|
4
|
+
import Animated, {
|
|
5
|
+
cancelAnimation,
|
|
6
|
+
Easing,
|
|
7
|
+
useAnimatedStyle,
|
|
8
|
+
useSharedValue,
|
|
9
|
+
withRepeat,
|
|
10
|
+
withSequence,
|
|
11
|
+
withTiming,
|
|
12
|
+
} from "react-native-reanimated";
|
|
13
|
+
|
|
14
|
+
import { Text } from "@/components/Text";
|
|
15
|
+
import { useThemeColor, type ThemeColorToken } from "@/hooks/useThemeColor";
|
|
16
|
+
import { cn } from "@/lib/utils";
|
|
17
|
+
|
|
18
|
+
interface LoadingOverlaySpinnerProps {
|
|
19
|
+
/** Icon rendered at the center of the ring, e.g. `<ThemedIcon icon={Brain} />`. */
|
|
20
|
+
icon?: ReactNode;
|
|
21
|
+
/** Ring diameter in px. Defaults to 72. */
|
|
22
|
+
size?: number;
|
|
23
|
+
/** Ring stroke width in px. Defaults to 2. */
|
|
24
|
+
trackWidth?: number;
|
|
25
|
+
/** Theme token for the spinning arc. Defaults to `gray-stroke-muted`. */
|
|
26
|
+
colorToken?: ThemeColorToken;
|
|
27
|
+
/** Explicit arc color. Takes precedence over `colorToken`. */
|
|
28
|
+
color?: string;
|
|
29
|
+
/** Theme token for the track behind the arc. The track is hidden when omitted. */
|
|
30
|
+
trackColorToken?: ThemeColorToken;
|
|
31
|
+
/** Pulses the center icon opacity. Defaults to true. */
|
|
32
|
+
pulse?: boolean;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
interface LoadingOverlayCardProps
|
|
36
|
+
extends LoadingOverlaySpinnerProps,
|
|
37
|
+
Omit<ViewProps, "children"> {
|
|
38
|
+
/** Main label. Defaults to "Carregando..."; pass `null` or `""` to hide it. */
|
|
39
|
+
text?: ReactNode;
|
|
40
|
+
/** Secondary text rendered below the label. */
|
|
41
|
+
description?: ReactNode;
|
|
42
|
+
/** Extra content below the texts, e.g. a cancel button. */
|
|
43
|
+
children?: ReactNode;
|
|
44
|
+
textClassName?: string;
|
|
45
|
+
descriptionClassName?: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
interface LoadingOverlayProps extends LoadingOverlayCardProps {
|
|
49
|
+
/** Controls the overlay visibility. Defaults to true. */
|
|
50
|
+
visible?: boolean;
|
|
51
|
+
/** Called on the Android back button. Defaults to a no-op, so the overlay blocks dismissal. */
|
|
52
|
+
onRequestClose?: ModalProps["onRequestClose"];
|
|
53
|
+
/** Theme token for the backdrop color. Defaults to `backdrop`. */
|
|
54
|
+
overlayColorToken?: ThemeColorToken;
|
|
55
|
+
/** Extra classes for the backdrop. Use `overlayColorToken` to change its color. */
|
|
56
|
+
overlayClassName?: string;
|
|
57
|
+
/** Extra classes for the wrapper that constrains the card width (`w-[90%] max-w-[320px]`). */
|
|
58
|
+
containerClassName?: string;
|
|
59
|
+
/** Props forwarded to the React Native `Modal`. */
|
|
60
|
+
modalProps?: Omit<ModalProps, "visible" | "onRequestClose" | "children">;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Rendered only when a track token is set, so the default case skips the color lookup. */
|
|
64
|
+
function SpinnerTrack({
|
|
65
|
+
size,
|
|
66
|
+
radius,
|
|
67
|
+
strokeWidth,
|
|
68
|
+
colorToken,
|
|
69
|
+
}: {
|
|
70
|
+
size: number;
|
|
71
|
+
radius: number;
|
|
72
|
+
strokeWidth: number;
|
|
73
|
+
colorToken: ThemeColorToken;
|
|
74
|
+
}) {
|
|
75
|
+
const color = useThemeColor(colorToken);
|
|
76
|
+
|
|
77
|
+
return (
|
|
78
|
+
<Circle
|
|
79
|
+
cx={size / 2}
|
|
80
|
+
cy={size / 2}
|
|
81
|
+
r={radius}
|
|
82
|
+
stroke={color}
|
|
83
|
+
strokeWidth={strokeWidth}
|
|
84
|
+
fill="none"
|
|
85
|
+
/>
|
|
86
|
+
);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function LoadingOverlaySpinner({
|
|
90
|
+
icon,
|
|
91
|
+
size = 72,
|
|
92
|
+
trackWidth = 2,
|
|
93
|
+
colorToken = "gray-stroke-muted",
|
|
94
|
+
color,
|
|
95
|
+
trackColorToken,
|
|
96
|
+
pulse = true,
|
|
97
|
+
}: LoadingOverlaySpinnerProps) {
|
|
98
|
+
const themeColor = useThemeColor(colorToken);
|
|
99
|
+
const stroke = color ?? themeColor;
|
|
100
|
+
const hasIcon = Boolean(icon);
|
|
101
|
+
|
|
102
|
+
const rotation = useSharedValue(0);
|
|
103
|
+
const opacity = useSharedValue(1);
|
|
104
|
+
|
|
105
|
+
useEffect(() => {
|
|
106
|
+
rotation.value = withRepeat(
|
|
107
|
+
withTiming(360, { duration: 1000, easing: Easing.linear }),
|
|
108
|
+
-1,
|
|
109
|
+
false
|
|
110
|
+
);
|
|
111
|
+
return () => cancelAnimation(rotation);
|
|
112
|
+
}, [rotation]);
|
|
113
|
+
|
|
114
|
+
useEffect(() => {
|
|
115
|
+
// Nothing to pulse without an icon; skip the infinite loop on the UI thread.
|
|
116
|
+
if (!pulse || !hasIcon) {
|
|
117
|
+
opacity.value = 1;
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
opacity.value = 0.4;
|
|
121
|
+
opacity.value = withRepeat(
|
|
122
|
+
withSequence(
|
|
123
|
+
withTiming(1, { duration: 400 }),
|
|
124
|
+
withTiming(0.4, { duration: 400 })
|
|
125
|
+
),
|
|
126
|
+
-1,
|
|
127
|
+
false
|
|
128
|
+
);
|
|
129
|
+
return () => cancelAnimation(opacity);
|
|
130
|
+
}, [opacity, pulse, hasIcon]);
|
|
131
|
+
|
|
132
|
+
const ringStyle = useAnimatedStyle(() => ({
|
|
133
|
+
transform: [{ rotate: `${rotation.value}deg` }],
|
|
134
|
+
}));
|
|
135
|
+
|
|
136
|
+
const iconStyle = useAnimatedStyle(() => ({
|
|
137
|
+
opacity: opacity.value,
|
|
138
|
+
}));
|
|
139
|
+
|
|
140
|
+
const radius = (size - trackWidth) / 2;
|
|
141
|
+
const circumference = 2 * Math.PI * radius;
|
|
142
|
+
|
|
143
|
+
return (
|
|
144
|
+
<View
|
|
145
|
+
className="items-center justify-center"
|
|
146
|
+
style={{ width: size, height: size }}
|
|
147
|
+
>
|
|
148
|
+
<Animated.View style={[{ position: "absolute" }, ringStyle]}>
|
|
149
|
+
<Svg width={size} height={size} viewBox={`0 0 ${size} ${size}`}>
|
|
150
|
+
{trackColorToken ? (
|
|
151
|
+
<SpinnerTrack
|
|
152
|
+
size={size}
|
|
153
|
+
radius={radius}
|
|
154
|
+
strokeWidth={trackWidth}
|
|
155
|
+
colorToken={trackColorToken}
|
|
156
|
+
/>
|
|
157
|
+
) : null}
|
|
158
|
+
<Circle
|
|
159
|
+
cx={size / 2}
|
|
160
|
+
cy={size / 2}
|
|
161
|
+
r={radius}
|
|
162
|
+
stroke={stroke}
|
|
163
|
+
strokeWidth={trackWidth}
|
|
164
|
+
strokeLinecap="round"
|
|
165
|
+
strokeDasharray={`${circumference * 0.75} ${circumference}`}
|
|
166
|
+
fill="none"
|
|
167
|
+
/>
|
|
168
|
+
</Svg>
|
|
169
|
+
</Animated.View>
|
|
170
|
+
|
|
171
|
+
{icon ? <Animated.View style={iconStyle}>{icon}</Animated.View> : null}
|
|
172
|
+
</View>
|
|
173
|
+
);
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
const DEFAULT_ACCESSIBILITY_LABEL = "Carregando";
|
|
177
|
+
|
|
178
|
+
/** Builds the progressbar label from the string parts of `text` and `description`. */
|
|
179
|
+
function getAccessibilityLabel(text: ReactNode, description: ReactNode): string {
|
|
180
|
+
const parts = [text, description].filter(
|
|
181
|
+
(part): part is string => typeof part === "string" && part.length > 0
|
|
182
|
+
);
|
|
183
|
+
return parts.length > 0 ? parts.join(". ") : DEFAULT_ACCESSIBILITY_LABEL;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function LoadingOverlayCard({
|
|
187
|
+
icon,
|
|
188
|
+
size,
|
|
189
|
+
trackWidth,
|
|
190
|
+
colorToken,
|
|
191
|
+
color,
|
|
192
|
+
trackColorToken,
|
|
193
|
+
pulse,
|
|
194
|
+
text = "Carregando...",
|
|
195
|
+
description,
|
|
196
|
+
children,
|
|
197
|
+
className,
|
|
198
|
+
textClassName,
|
|
199
|
+
descriptionClassName,
|
|
200
|
+
accessibilityLabel,
|
|
201
|
+
...props
|
|
202
|
+
}: LoadingOverlayCardProps) {
|
|
203
|
+
return (
|
|
204
|
+
<View
|
|
205
|
+
className={cn(
|
|
206
|
+
"w-full min-h-[230px] items-center justify-center gap-6 rounded-xl bg-surface-card p-10",
|
|
207
|
+
className
|
|
208
|
+
)}
|
|
209
|
+
{...props}
|
|
210
|
+
>
|
|
211
|
+
{/* Role and live region wrap only the spinner + texts, so `children` (e.g. a
|
|
212
|
+
cancel button) stay focusable and aren't re-announced by TalkBack. */}
|
|
213
|
+
<View
|
|
214
|
+
accessible
|
|
215
|
+
accessibilityRole="progressbar"
|
|
216
|
+
accessibilityLabel={
|
|
217
|
+
accessibilityLabel ?? getAccessibilityLabel(text, description)
|
|
218
|
+
}
|
|
219
|
+
accessibilityState={{ busy: true }}
|
|
220
|
+
accessibilityLiveRegion="polite"
|
|
221
|
+
className="w-full items-center gap-6"
|
|
222
|
+
>
|
|
223
|
+
<LoadingOverlaySpinner
|
|
224
|
+
icon={icon}
|
|
225
|
+
size={size}
|
|
226
|
+
trackWidth={trackWidth}
|
|
227
|
+
colorToken={colorToken}
|
|
228
|
+
color={color}
|
|
229
|
+
trackColorToken={trackColorToken}
|
|
230
|
+
pulse={pulse}
|
|
231
|
+
/>
|
|
232
|
+
|
|
233
|
+
{text || description ? (
|
|
234
|
+
<View className="w-full items-center gap-2">
|
|
235
|
+
{text ? (
|
|
236
|
+
<Text
|
|
237
|
+
className={cn(
|
|
238
|
+
"text-center text-sm font-semibold text-content-title",
|
|
239
|
+
textClassName
|
|
240
|
+
)}
|
|
241
|
+
>
|
|
242
|
+
{text}
|
|
243
|
+
</Text>
|
|
244
|
+
) : null}
|
|
245
|
+
{description ? (
|
|
246
|
+
<Text
|
|
247
|
+
className={cn(
|
|
248
|
+
"text-center text-sm font-medium text-content-paragraph",
|
|
249
|
+
descriptionClassName
|
|
250
|
+
)}
|
|
251
|
+
>
|
|
252
|
+
{description}
|
|
253
|
+
</Text>
|
|
254
|
+
) : null}
|
|
255
|
+
</View>
|
|
256
|
+
) : null}
|
|
257
|
+
</View>
|
|
258
|
+
|
|
259
|
+
{children ? <View className="w-full">{children}</View> : null}
|
|
260
|
+
</View>
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const noop = () => {};
|
|
265
|
+
|
|
266
|
+
function LoadingOverlay({
|
|
267
|
+
visible = true,
|
|
268
|
+
onRequestClose = noop,
|
|
269
|
+
overlayColorToken = "backdrop",
|
|
270
|
+
overlayClassName,
|
|
271
|
+
containerClassName,
|
|
272
|
+
modalProps,
|
|
273
|
+
...cardProps
|
|
274
|
+
}: LoadingOverlayProps) {
|
|
275
|
+
// Inline style: `bg-backdrop` depends on the consumer's Tailwind config having
|
|
276
|
+
// the token, which older `@medway-ui/core` versions don't ship.
|
|
277
|
+
const backdropColor = useThemeColor(overlayColorToken);
|
|
278
|
+
|
|
279
|
+
return (
|
|
280
|
+
<Modal
|
|
281
|
+
transparent
|
|
282
|
+
statusBarTranslucent
|
|
283
|
+
animationType="fade"
|
|
284
|
+
{...modalProps}
|
|
285
|
+
visible={visible}
|
|
286
|
+
onRequestClose={onRequestClose}
|
|
287
|
+
>
|
|
288
|
+
<View
|
|
289
|
+
className={cn(
|
|
290
|
+
"flex-1 items-center justify-center",
|
|
291
|
+
overlayClassName
|
|
292
|
+
)}
|
|
293
|
+
style={{ backgroundColor: backdropColor }}
|
|
294
|
+
>
|
|
295
|
+
<View className={cn("w-[90%] max-w-[320px]", containerClassName)}>
|
|
296
|
+
<LoadingOverlayCard {...cardProps} />
|
|
297
|
+
</View>
|
|
298
|
+
</View>
|
|
299
|
+
</Modal>
|
|
300
|
+
);
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
export { LoadingOverlay, LoadingOverlayCard, LoadingOverlaySpinner };
|
|
304
|
+
export type {
|
|
305
|
+
LoadingOverlayCardProps,
|
|
306
|
+
LoadingOverlayProps,
|
|
307
|
+
LoadingOverlaySpinnerProps,
|
|
308
|
+
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type ReactNode } from "react";
|
|
2
|
+
import { StyleSheet } from "react-native";
|
|
2
3
|
|
|
3
4
|
import { Text } from "@/components/Text";
|
|
4
5
|
import * as TooltipPrimitive from "@rn-primitives/tooltip";
|
|
@@ -28,7 +29,7 @@ function TooltipContent({
|
|
|
28
29
|
}: TooltipContentProps) {
|
|
29
30
|
return (
|
|
30
31
|
<TooltipPrimitive.Portal>
|
|
31
|
-
<TooltipPrimitive.Overlay
|
|
32
|
+
<TooltipPrimitive.Overlay style={StyleSheet.absoluteFill} />
|
|
32
33
|
<TooltipPrimitive.Content
|
|
33
34
|
sideOffset={sideOffset}
|
|
34
35
|
className={cn(
|
|
@@ -25,6 +25,9 @@ const LIGHT_COLOR_ALIAS_MAP: Record<string, string> = {
|
|
|
25
25
|
'prev-background-fill': 'prev',
|
|
26
26
|
'red-foreground': 'red-darken-2',
|
|
27
27
|
'go-foreground': 'go-darken-1',
|
|
28
|
+
// Not a real CSS var: `--color-backdrop` resolves directly, this alias only
|
|
29
|
+
// keys the static fallback below (keep in sync with tokens.native.css).
|
|
30
|
+
backdrop: 'backdrop-light',
|
|
28
31
|
};
|
|
29
32
|
|
|
30
33
|
/** Fallback — dark mode (espelha tokens.css `.dark`). */
|
|
@@ -49,6 +52,8 @@ const DARK_COLOR_ALIAS_MAP: Record<string, string> = {
|
|
|
49
52
|
'prev-background-fill': 'prev',
|
|
50
53
|
'red-foreground': 'red-lighten-3',
|
|
51
54
|
'go-foreground': 'go-lighten-5',
|
|
55
|
+
// Fallback key only, see LIGHT_COLOR_ALIAS_MAP.
|
|
56
|
+
backdrop: 'backdrop-dark',
|
|
52
57
|
};
|
|
53
58
|
|
|
54
59
|
/** Fallback estático quando NativeWind não resolve `var(--color-*)` encadeado. */
|
|
@@ -77,6 +82,8 @@ const THEME_COLOR_FALLBACKS: Record<string, string> = {
|
|
|
77
82
|
'secondary-darken-1': 'rgba(7, 171, 149, 1)',
|
|
78
83
|
'secondary-darken-3': 'rgba(13, 104, 92, 1)',
|
|
79
84
|
'secondary-lighten-1': 'rgba(22, 208, 118, 1)',
|
|
85
|
+
'backdrop-light': 'rgba(16, 19, 24, 0.3)',
|
|
86
|
+
'backdrop-dark': 'rgba(16, 19, 24, 0.5)',
|
|
80
87
|
};
|
|
81
88
|
|
|
82
89
|
const DEFAULT_COLOR = '#3B4958';
|
package/src/index.ts
CHANGED
|
@@ -243,6 +243,18 @@ export type { LabelProps } from "@/components/Label";
|
|
|
243
243
|
export { Loading } from "@/components/Loading";
|
|
244
244
|
export type { LoadingProps } from "@/components/Loading";
|
|
245
245
|
|
|
246
|
+
// LoadingOverlay
|
|
247
|
+
export {
|
|
248
|
+
LoadingOverlay,
|
|
249
|
+
LoadingOverlayCard,
|
|
250
|
+
LoadingOverlaySpinner,
|
|
251
|
+
} from "@/components/LoadingOverlay";
|
|
252
|
+
export type {
|
|
253
|
+
LoadingOverlayCardProps,
|
|
254
|
+
LoadingOverlayProps,
|
|
255
|
+
LoadingOverlaySpinnerProps,
|
|
256
|
+
} from "@/components/LoadingOverlay";
|
|
257
|
+
|
|
246
258
|
// Skeleton
|
|
247
259
|
export { Skeleton } from "@/components/Skeleton";
|
|
248
260
|
export type { SkeletonProps } from "@/components/Skeleton";
|