@mrmeg/expo-ui 0.7.2 → 0.8.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/LLM_USAGE.md +21 -11
- package/README.md +8 -10
- package/dist/components/Accordion.d.ts +4 -4
- package/dist/components/AnimatedView.d.ts +1 -1
- package/dist/components/Badge.d.ts +1 -1
- package/dist/components/BottomSheet.d.ts +96 -20
- package/dist/components/BottomSheet.js +203 -444
- package/dist/components/Button.d.ts +3 -3
- package/dist/components/Button.js +17 -1
- package/dist/components/Card.d.ts +6 -6
- package/dist/components/Checkbox.d.ts +2 -1
- package/dist/components/Collapsible.d.ts +4 -3
- package/dist/components/Dialog.d.ts +10 -10
- package/dist/components/Dialog.js +16 -8
- package/dist/components/DismissKeyboard.d.ts +1 -1
- package/dist/components/Drawer.d.ts +7 -7
- package/dist/components/DropdownMenu.d.ts +10 -10
- package/dist/components/EmptyState.d.ts +1 -1
- package/dist/components/ErrorBoundary.d.ts +1 -1
- package/dist/components/Icon.d.ts +1 -1
- package/dist/components/InputOTP.d.ts +2 -1
- package/dist/components/Label.d.ts +1 -1
- package/dist/components/MaxWidthContainer.d.ts +1 -1
- package/dist/components/Notification.d.ts +4 -10
- package/dist/components/Notification.js +12 -13
- package/dist/components/Popover.d.ts +4 -4
- package/dist/components/Progress.d.ts +2 -1
- package/dist/components/RadioGroup.d.ts +3 -2
- package/dist/components/SegmentedControl.d.ts +53 -0
- package/dist/components/SegmentedControl.js +25 -0
- package/dist/components/Select.d.ts +7 -7
- package/dist/components/Separator.d.ts +2 -1
- package/dist/components/Skeleton.d.ts +5 -4
- package/dist/components/Slider.d.ts +24 -3
- package/dist/components/Slider.js +26 -147
- package/dist/components/StatusBar.d.ts +1 -1
- package/dist/components/StyledText.d.ts +12 -12
- package/dist/components/Switch.d.ts +2 -1
- package/dist/components/Tabs.d.ts +5 -5
- package/dist/components/Tabs.js +10 -2
- package/dist/components/TextInput.d.ts +1 -1
- package/dist/components/TextInput.js +129 -2
- package/dist/components/Toggle.d.ts +3 -2
- package/dist/components/ToggleGroup.d.ts +4 -3
- package/dist/components/Tooltip.d.ts +3 -3
- package/dist/components/UIProvider.d.ts +1 -1
- package/dist/components/index.d.ts +1 -0
- package/dist/components/index.js +1 -0
- package/dist/state/globalUIStore.d.ts +9 -1
- package/dist/state/globalUIStore.js +9 -1
- package/dist/state/index.d.ts +1 -0
- package/dist/state/index.js +1 -0
- package/dist/state/notify.d.ts +50 -0
- package/dist/state/notify.js +31 -0
- package/dist/state/themeColorScope.d.ts +1 -1
- package/llms-full.md +34 -3
- package/package.json +3 -2
- package/dist/components/BottomSheetKeyboard.d.ts +0 -7
- package/dist/components/BottomSheetKeyboard.js +0 -39
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useMemo, useState } from "react";
|
|
2
|
+
import { useEffect, useImperativeHandle, useMemo, useRef, useState, } from "react";
|
|
3
3
|
import { StyleSheet, TextInput as RNTextInput, Platform, View, Pressable, } from "react-native";
|
|
4
|
+
import { Host, TextInput as ExpoTextInput, useNativeState, } from "@expo/ui";
|
|
4
5
|
import { useTheme } from "../hooks/useTheme.js";
|
|
5
6
|
import { spacing } from "../constants/spacing.js";
|
|
6
7
|
import { fontFamilies } from "../constants/fonts.js";
|
|
@@ -70,7 +71,21 @@ const SIZE_CONFIGS = {
|
|
|
70
71
|
* />
|
|
71
72
|
* ```
|
|
72
73
|
*/
|
|
73
|
-
export function TextInput(
|
|
74
|
+
export function TextInput(props) {
|
|
75
|
+
// On iOS/Android, route to the native @expo/ui field for flicker-free,
|
|
76
|
+
// platform-native text editing. Web keeps the full-featured RN implementation
|
|
77
|
+
// (no flicker problem there, and it preserves every in-field affordance).
|
|
78
|
+
if (Platform.OS !== "web") {
|
|
79
|
+
return _jsx(NativeTextInput, { ...props });
|
|
80
|
+
}
|
|
81
|
+
return _jsx(WebTextInput, { ...props });
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Web / fallback implementation — the original React Native TextInput with the
|
|
85
|
+
* complete chrome (variants, sizes, overlays, password toggle, clear button,
|
|
86
|
+
* error icon). Unchanged from the pre-native version.
|
|
87
|
+
*/
|
|
88
|
+
function WebTextInput({ variant = "outline", size = "md", label, helperText, errorText, error, required, rows, showSecureEntryToggle, leftElement, rightElement, clearable = false, wrapperStyle, focusedStyle, forceLight, secureTextEntry, inputMode, style, onChangeText, onFocus, onBlur, value, multiline, editable = true, ref, ...rest }) {
|
|
74
89
|
const { theme, getContrastingColor, getFocusRingStyle } = useTheme();
|
|
75
90
|
const styles = useMemo(() => createStyles(theme, variant, size), [theme, variant, size]);
|
|
76
91
|
const [focused, setFocused] = useState(false);
|
|
@@ -157,7 +172,119 @@ export function TextInput({ variant = "outline", size = "md", label, helperText,
|
|
|
157
172
|
hasError && styles.errorText,
|
|
158
173
|
], children: errorText || helperText }))] }));
|
|
159
174
|
}
|
|
175
|
+
/**
|
|
176
|
+
* Native (iOS / Android) implementation backed by `@expo/ui`'s TextInput, which
|
|
177
|
+
* bridges to SwiftUI's `TextField`/`SecureField` and Jetpack Compose's
|
|
178
|
+
* `TextField`. The text buffer lives natively (via `useNativeState`), so typing
|
|
179
|
+
* never round-trips through React state — eliminating the cursor flicker seen on
|
|
180
|
+
* controlled RN inputs.
|
|
181
|
+
*
|
|
182
|
+
* By design (reliability over feature-parity) this path renders the field plus
|
|
183
|
+
* sibling label / helper / error text only. The in-field overlays from the web
|
|
184
|
+
* implementation — password visibility toggle, clear button, left/right
|
|
185
|
+
* elements, and error icon — are intentionally omitted on native to avoid
|
|
186
|
+
* layering RN views over the native host view.
|
|
187
|
+
*/
|
|
188
|
+
function NativeTextInput({ variant = "outline", size = "md", label, helperText, errorText, error, required, rows, forceLight, inputMode, onChangeText, value, defaultValue, editable = true, multiline, secureTextEntry, showSecureEntryToggle, ref, wrapperStyle,
|
|
189
|
+
// Web-only affordances. Destructured out of `rest` so they're NOT forwarded to
|
|
190
|
+
// the native field; intentionally unused on this path (see doc comment above).
|
|
191
|
+
// `style` (an RN TextStyle) is likewise dropped — it doesn't map to
|
|
192
|
+
// UniversalStyle and is replaced by the `boxStyle`/`textStyle` computed below.
|
|
193
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
194
|
+
leftElement, rightElement, clearable, focusedStyle, style, ...rest }) {
|
|
195
|
+
const { theme, getContrastingColor } = useTheme();
|
|
196
|
+
const styles = useMemo(() => createStyles(theme, variant, size), [theme, variant, size]);
|
|
197
|
+
const [passwordVisible, setPasswordVisible] = useState(false);
|
|
198
|
+
const hasError = error || !!errorText;
|
|
199
|
+
const sizeConfig = SIZE_CONFIGS[size];
|
|
200
|
+
// Password visibility toggle. Flipping `secureTextEntry` swaps SwiftUI's
|
|
201
|
+
// SecureField <-> TextField on iOS and toggles Compose's visualTransformation
|
|
202
|
+
// on Android; both bind the same `state` observable, so the text survives.
|
|
203
|
+
const hasSecureToggle = !!(secureTextEntry && showSecureEntryToggle);
|
|
204
|
+
const effectiveSecureTextEntry = secureTextEntry && !passwordVisible;
|
|
205
|
+
// Native text buffer. Seeded once; `value` changes are reconciled below.
|
|
206
|
+
const state = useNativeState(value ?? defaultValue ?? "");
|
|
207
|
+
// Reconcile controlled `value` -> native buffer WITHOUT echoing keystrokes.
|
|
208
|
+
// Only write when the parent's value genuinely diverges (resets, clears,
|
|
209
|
+
// programmatic sets); typing already updated `state` natively.
|
|
210
|
+
useEffect(() => {
|
|
211
|
+
if (value !== undefined && value !== state.value) {
|
|
212
|
+
state.value = value;
|
|
213
|
+
}
|
|
214
|
+
}, [value, state]);
|
|
215
|
+
// The inner ref is the @expo/ui handle; the outward ref is typed as
|
|
216
|
+
// RNTextInput because that's what the public TextInputCustomProps declares.
|
|
217
|
+
// We expose the subset consumers use, plus a `setNativeProps` shim so the
|
|
218
|
+
// uncontrolled AuthTextField can push corrected text into the native buffer.
|
|
219
|
+
const innerRef = useRef(null);
|
|
220
|
+
useImperativeHandle(ref, () => ({
|
|
221
|
+
focus: () => innerRef.current?.focus(),
|
|
222
|
+
blur: () => innerRef.current?.blur(),
|
|
223
|
+
clear: () => {
|
|
224
|
+
state.value = "";
|
|
225
|
+
},
|
|
226
|
+
isFocused: () => innerRef.current?.isFocused() ?? false,
|
|
227
|
+
setNativeProps: (props) => {
|
|
228
|
+
if (typeof props?.text === "string") {
|
|
229
|
+
state.value = props.text;
|
|
230
|
+
}
|
|
231
|
+
},
|
|
232
|
+
}), [state]);
|
|
233
|
+
const backgroundColor = forceLight
|
|
234
|
+
? palette.white
|
|
235
|
+
: variant === "filled"
|
|
236
|
+
? theme.colors.card
|
|
237
|
+
: "transparent";
|
|
238
|
+
const borderColor = hasError
|
|
239
|
+
? theme.colors.destructive
|
|
240
|
+
: forceLight
|
|
241
|
+
? "#d1d5db"
|
|
242
|
+
: theme.colors.input;
|
|
243
|
+
const textColor = forceLight
|
|
244
|
+
? "#1f2937"
|
|
245
|
+
: getContrastingColor(backgroundColor === "transparent" ? theme.colors.background : backgroundColor, theme.colors.text, palette.white);
|
|
246
|
+
// Map variant/size to @expo/ui's UniversalStyle (translated to SwiftUI /
|
|
247
|
+
// Compose modifiers natively).
|
|
248
|
+
const boxStyle = {
|
|
249
|
+
backgroundColor,
|
|
250
|
+
borderColor,
|
|
251
|
+
borderRadius: variant === "underlined" ? 0 : spacing.radiusMd,
|
|
252
|
+
borderWidth: variant === "outline" ? 1 : 0,
|
|
253
|
+
paddingHorizontal: sizeConfig.paddingHorizontal,
|
|
254
|
+
paddingVertical: sizeConfig.paddingVertical,
|
|
255
|
+
opacity: editable === false ? 0.6 : 1,
|
|
256
|
+
...(multiline ? null : { height: sizeConfig.height }),
|
|
257
|
+
};
|
|
258
|
+
// "System" is an RN-only sentinel (RCTFont resolves it to the system font).
|
|
259
|
+
// @expo/ui passes the family verbatim to SwiftUI's Font.custom / Compose,
|
|
260
|
+
// where no such font exists — the fallback ignores fontSize and renders at
|
|
261
|
+
// the 17pt default, blowing up text and secure-entry dots. Omit the family
|
|
262
|
+
// so the native side uses the system font at our requested size.
|
|
263
|
+
const nativeFontFamily = fontFamilies.sansSerif.regular === "System"
|
|
264
|
+
? undefined
|
|
265
|
+
: fontFamilies.sansSerif.regular;
|
|
266
|
+
const textStyle = {
|
|
267
|
+
color: textColor,
|
|
268
|
+
fontSize: sizeConfig.fontSize,
|
|
269
|
+
...(nativeFontFamily ? { fontFamily: nativeFontFamily } : null),
|
|
270
|
+
};
|
|
271
|
+
return (_jsxs(View, { style: wrapperStyle, children: [!!label && (_jsx(View, { style: styles.labelContainer, children: _jsxs(StyledText, { selectable: false, style: styles.label, children: [label, required && _jsx(StyledText, { selectable: false, style: styles.required, children: " *" })] }) })), _jsxs(View, { style: hasSecureToggle ? styles.nativeRow : undefined, children: [_jsx(Host, { matchContents: { vertical: true }, style: hasSecureToggle ? styles.nativeHostFlex : styles.nativeHost, children: _jsx(ExpoTextInput, { ...rest, ref: innerRef, value: state, defaultValue: defaultValue, onChangeText: onChangeText, editable: editable, multiline: multiline, rows: rows, inputMode: inputMode, secureTextEntry: effectiveSecureTextEntry, placeholderTextColor: theme.colors.textDim, style: boxStyle, textStyle: textStyle }) }), hasSecureToggle && (_jsx(Pressable, { style: styles.nativePasswordToggle, onPress: () => setPasswordVisible((v) => !v), accessibilityLabel: passwordVisible ? "Hide password" : "Show password", accessibilityRole: "button", children: _jsx(Icon, { name: passwordVisible ? "eye-off" : "eye", size: spacing.iconSm + 4, color: "textDim" }) }))] }), !!(helperText || errorText) && (_jsx(StyledText, { selectable: false, style: [styles.helperText, hasError && styles.errorText], children: errorText || helperText }))] }));
|
|
272
|
+
}
|
|
160
273
|
const createStyles = (theme, variant, size) => StyleSheet.create({
|
|
274
|
+
nativeHost: {
|
|
275
|
+
width: "100%",
|
|
276
|
+
},
|
|
277
|
+
nativeRow: {
|
|
278
|
+
flexDirection: "row",
|
|
279
|
+
alignItems: "center",
|
|
280
|
+
gap: spacing.xs,
|
|
281
|
+
},
|
|
282
|
+
nativeHostFlex: {
|
|
283
|
+
flex: 1,
|
|
284
|
+
},
|
|
285
|
+
nativePasswordToggle: {
|
|
286
|
+
paddingHorizontal: spacing.xs,
|
|
287
|
+
},
|
|
161
288
|
wrapper: {
|
|
162
289
|
width: "100%",
|
|
163
290
|
position: "relative",
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import React from "react";
|
|
1
2
|
import * as TogglePrimitive from "@rn-primitives/toggle";
|
|
2
3
|
import { ViewStyle, StyleProp } from "react-native";
|
|
3
4
|
import type { IconName } from "./Icon";
|
|
@@ -83,7 +84,7 @@ interface ToggleProps extends Omit<TogglePrimitive.RootProps, "style"> {
|
|
|
83
84
|
* </Toggle>
|
|
84
85
|
* ```
|
|
85
86
|
*/
|
|
86
|
-
declare function Toggle({ variant, size, shape, loading, iconOnly, style: styleOverride, ...props }: ToggleProps):
|
|
87
|
+
declare function Toggle({ variant, size, shape, loading, iconOnly, style: styleOverride, ...props }: ToggleProps): React.JSX.Element;
|
|
87
88
|
/**
|
|
88
89
|
* ToggleIcon Component
|
|
89
90
|
* Icon wrapper for use inside Toggle buttons
|
|
@@ -101,6 +102,6 @@ interface ToggleIconProps {
|
|
|
101
102
|
size?: number;
|
|
102
103
|
color?: string;
|
|
103
104
|
}
|
|
104
|
-
declare function ToggleIcon({ name, size, color }: ToggleIconProps):
|
|
105
|
+
declare function ToggleIcon({ name, size, color }: ToggleIconProps): React.JSX.Element;
|
|
105
106
|
export { Toggle, ToggleIcon };
|
|
106
107
|
export type { ToggleProps };
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { IconName } from "./Icon";
|
|
2
2
|
import * as ToggleGroupPrimitive from "@rn-primitives/toggle-group";
|
|
3
|
+
import * as React from "react";
|
|
3
4
|
type ToggleGroupVariant = "default" | "outline";
|
|
4
5
|
type ToggleGroupSize = "sm" | "default" | "lg";
|
|
5
6
|
type ToggleGroupProps = ToggleGroupPrimitive.RootProps & {
|
|
@@ -41,7 +42,7 @@ type ToggleGroupProps = ToggleGroupPrimitive.RootProps & {
|
|
|
41
42
|
* </ToggleGroup>
|
|
42
43
|
* ```
|
|
43
44
|
*/
|
|
44
|
-
declare function ToggleGroup({ variant, size, children, ...props }: ToggleGroupProps):
|
|
45
|
+
declare function ToggleGroup({ variant, size, children, ...props }: ToggleGroupProps): React.JSX.Element;
|
|
45
46
|
type ToggleGroupItemProps = ToggleGroupPrimitive.ItemProps & {
|
|
46
47
|
/**
|
|
47
48
|
* Automatically set by ToggleGroup parent - don't set manually
|
|
@@ -57,7 +58,7 @@ type ToggleGroupItemProps = ToggleGroupPrimitive.ItemProps & {
|
|
|
57
58
|
* Individual toggle button within a ToggleGroup
|
|
58
59
|
* Position (first/last) is automatically detected for rounded corners
|
|
59
60
|
*/
|
|
60
|
-
declare function ToggleGroupItem({ isFirst, isLast, children, ...props }: ToggleGroupItemProps):
|
|
61
|
+
declare function ToggleGroupItem({ isFirst, isLast, children, ...props }: ToggleGroupItemProps): React.JSX.Element;
|
|
61
62
|
/**
|
|
62
63
|
* ToggleGroupIcon Component
|
|
63
64
|
* Icon wrapper for use inside ToggleGroup items
|
|
@@ -75,6 +76,6 @@ interface ToggleGroupIconProps {
|
|
|
75
76
|
size?: number;
|
|
76
77
|
color?: string;
|
|
77
78
|
}
|
|
78
|
-
declare function ToggleGroupIcon({ name, size, color }: ToggleGroupIconProps):
|
|
79
|
+
declare function ToggleGroupIcon({ name, size, color }: ToggleGroupIconProps): React.JSX.Element;
|
|
79
80
|
export { ToggleGroup, ToggleGroupIcon, ToggleGroupItem };
|
|
80
81
|
export type { ToggleGroupProps, ToggleGroupSize, ToggleGroupVariant };
|
|
@@ -41,7 +41,7 @@ interface TooltipContentProps extends TooltipPrimitive.ContentProps {
|
|
|
41
41
|
* - Smooth animations
|
|
42
42
|
* - Portal-based rendering for proper z-index
|
|
43
43
|
*/
|
|
44
|
-
declare function TooltipContent({ side, align, sideOffset, portalHost, variant, ...props }: TooltipContentProps):
|
|
44
|
+
declare function TooltipContent({ side, align, sideOffset, portalHost, variant, ...props }: TooltipContentProps): React.JSX.Element;
|
|
45
45
|
/**
|
|
46
46
|
* Tooltip Body Component
|
|
47
47
|
* Simple wrapper for tooltip content with padding
|
|
@@ -49,7 +49,7 @@ declare function TooltipContent({ side, align, sideOffset, portalHost, variant,
|
|
|
49
49
|
interface TooltipBodyProps extends ViewProps {
|
|
50
50
|
children: React.ReactNode;
|
|
51
51
|
}
|
|
52
|
-
declare function TooltipBody({ children, style, ...props }: TooltipBodyProps):
|
|
52
|
+
declare function TooltipBody({ children, style, ...props }: TooltipBodyProps): React.JSX.Element;
|
|
53
53
|
interface TooltipProps extends TooltipPrimitive.RootProps {
|
|
54
54
|
/**
|
|
55
55
|
* Time to wait before showing tooltip (web only)
|
|
@@ -99,7 +99,7 @@ interface TooltipProps extends TooltipPrimitive.RootProps {
|
|
|
99
99
|
* </Tooltip>
|
|
100
100
|
* ```
|
|
101
101
|
*/
|
|
102
|
-
declare function Tooltip({ delayDuration, skipDelayDuration, ...props }: TooltipProps):
|
|
102
|
+
declare function Tooltip({ delayDuration, skipDelayDuration, ...props }: TooltipProps): React.JSX.Element;
|
|
103
103
|
/**
|
|
104
104
|
* Tooltip Component with Sub-components
|
|
105
105
|
* Properly typed interface for dot notation access (e.g., Tooltip.Trigger)
|
|
@@ -20,4 +20,4 @@ export interface UIProviderProps {
|
|
|
20
20
|
*/
|
|
21
21
|
statusBar?: boolean;
|
|
22
22
|
}
|
|
23
|
-
export declare function UIProvider({ children, notification, portalHost, statusBar, }: UIProviderProps):
|
|
23
|
+
export declare function UIProvider({ children, notification, portalHost, statusBar, }: UIProviderProps): React.JSX.Element;
|
package/dist/components/index.js
CHANGED
|
@@ -21,6 +21,7 @@ export * from "./Notification.js";
|
|
|
21
21
|
export * from "./Popover.js";
|
|
22
22
|
export * from "./Progress.js";
|
|
23
23
|
export * from "./RadioGroup.js";
|
|
24
|
+
export * from "./SegmentedControl.js";
|
|
24
25
|
export * from "./Select.js";
|
|
25
26
|
export * from "./Separator.js";
|
|
26
27
|
export * from "./Skeleton.js";
|
|
@@ -8,9 +8,16 @@
|
|
|
8
8
|
* - show({ type, title, messages, duration, loading, action }): displays a notification
|
|
9
9
|
* - hide(): hides the current notification
|
|
10
10
|
*
|
|
11
|
-
*
|
|
11
|
+
* Notifications auto-dismiss after `DEFAULT_NOTIFICATION_DURATION` unless a
|
|
12
|
+
* `duration` is given. Pass `duration: 0` to keep one up until dismissed;
|
|
13
|
+
* loading notifications never auto-dismiss.
|
|
14
|
+
*
|
|
15
|
+
* Prefer the `notify` helpers (see ./notify) for triggering notifications from
|
|
16
|
+
* app code; use this store directly for reactive subscription (selectors) and tests.
|
|
12
17
|
*/
|
|
13
18
|
export type GlobalNotificationType = "error" | "success" | "info" | "warning";
|
|
19
|
+
/** Auto-dismiss delay applied when `show()` is called without a `duration`. */
|
|
20
|
+
export declare const DEFAULT_NOTIFICATION_DURATION = 4000;
|
|
14
21
|
export type GlobalNotificationPosition = "top" | "bottom";
|
|
15
22
|
export type GlobalNotificationAction = {
|
|
16
23
|
label: string;
|
|
@@ -21,6 +28,7 @@ export type GlobalNotificationAlert = {
|
|
|
21
28
|
type: GlobalNotificationType;
|
|
22
29
|
title?: string;
|
|
23
30
|
messages?: string[];
|
|
31
|
+
/** Auto-dismiss delay in ms. Defaults to `DEFAULT_NOTIFICATION_DURATION`; 0 = stays until dismissed. */
|
|
24
32
|
duration?: number;
|
|
25
33
|
loading?: boolean;
|
|
26
34
|
/** Where to display the notification */
|
|
@@ -1,8 +1,16 @@
|
|
|
1
1
|
import { create } from "zustand";
|
|
2
|
+
/** Auto-dismiss delay applied when `show()` is called without a `duration`. */
|
|
3
|
+
export const DEFAULT_NOTIFICATION_DURATION = 4000;
|
|
2
4
|
export const globalUIStore = create((set) => ({
|
|
3
5
|
alert: null,
|
|
4
6
|
show: (alert) => set({
|
|
5
|
-
alert: {
|
|
7
|
+
alert: {
|
|
8
|
+
...alert,
|
|
9
|
+
// Loading notifications stay up until replaced or hidden (e.g. by
|
|
10
|
+
// notify.promise); everything else falls back to the default timeout.
|
|
11
|
+
duration: alert.duration ?? (alert.loading ? undefined : DEFAULT_NOTIFICATION_DURATION),
|
|
12
|
+
show: true,
|
|
13
|
+
},
|
|
6
14
|
}),
|
|
7
15
|
hide: () => set({ alert: null }),
|
|
8
16
|
}));
|
package/dist/state/index.d.ts
CHANGED
package/dist/state/index.js
CHANGED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import type { GlobalNotificationAlert } from "./globalUIStore";
|
|
2
|
+
/**
|
|
3
|
+
* notify
|
|
4
|
+
*
|
|
5
|
+
* Imperative notification API backed by `globalUIStore`. This is the
|
|
6
|
+
* recommended way to trigger the `Notification` component from app code.
|
|
7
|
+
*
|
|
8
|
+
* Notifications auto-dismiss after `DEFAULT_NOTIFICATION_DURATION` (4s) by
|
|
9
|
+
* default. Pass `duration: 0` to keep one up until dismissed; `notify.loading`
|
|
10
|
+
* is always persistent.
|
|
11
|
+
*
|
|
12
|
+
* Usage:
|
|
13
|
+
* ```ts
|
|
14
|
+
* notify.success("Saved", { messages: ["Your changes have been saved."] });
|
|
15
|
+
* notify.error("Upload failed");
|
|
16
|
+
* notify.loading("Uploading…");
|
|
17
|
+
* notify.hide();
|
|
18
|
+
*
|
|
19
|
+
* // Full control (same payload as globalUIStore show())
|
|
20
|
+
* notify({ type: "info", title: "Copied", duration: 2000, position: "bottom" });
|
|
21
|
+
*
|
|
22
|
+
* // Loading → success/error around a promise
|
|
23
|
+
* await notify.promise(saveProfile(), {
|
|
24
|
+
* loading: "Saving…",
|
|
25
|
+
* success: "Profile saved",
|
|
26
|
+
* error: "Could not save profile",
|
|
27
|
+
* });
|
|
28
|
+
* ```
|
|
29
|
+
*/
|
|
30
|
+
export type NotifyOptions = Omit<GlobalNotificationAlert, "show" | "type" | "title">;
|
|
31
|
+
export type NotifyPromiseMessages<T> = {
|
|
32
|
+
loading: string;
|
|
33
|
+
success: string | ((value: T) => string);
|
|
34
|
+
error: string | ((error: unknown) => string);
|
|
35
|
+
};
|
|
36
|
+
export declare const notify: ((alert: Omit<GlobalNotificationAlert, "show">) => void) & {
|
|
37
|
+
success: (title: string, options?: NotifyOptions) => void;
|
|
38
|
+
error: (title: string, options?: NotifyOptions) => void;
|
|
39
|
+
info: (title: string, options?: NotifyOptions) => void;
|
|
40
|
+
warning: (title: string, options?: NotifyOptions) => void;
|
|
41
|
+
/** Persistent spinner notification; stays visible until replaced or hidden. */
|
|
42
|
+
loading: (title: string, options?: NotifyOptions) => void;
|
|
43
|
+
/**
|
|
44
|
+
* Shows a loading notification while the promise is pending, then a
|
|
45
|
+
* success or error notification. Rethrows on rejection and returns the
|
|
46
|
+
* resolved value so it can wrap existing async flows transparently.
|
|
47
|
+
*/
|
|
48
|
+
promise: <T>(promise: Promise<T>, messages: NotifyPromiseMessages<T>) => Promise<T>;
|
|
49
|
+
hide: () => void;
|
|
50
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { globalUIStore } from "./globalUIStore.js";
|
|
2
|
+
const show = (alert) => globalUIStore.getState().show(alert);
|
|
3
|
+
const showType = (type) => (title, options) => show({ type, title, ...options });
|
|
4
|
+
export const notify = Object.assign(show, {
|
|
5
|
+
success: showType("success"),
|
|
6
|
+
error: showType("error"),
|
|
7
|
+
info: showType("info"),
|
|
8
|
+
warning: showType("warning"),
|
|
9
|
+
/** Persistent spinner notification; stays visible until replaced or hidden. */
|
|
10
|
+
loading: (title, options) => show({ type: "info", title, loading: true, ...options }),
|
|
11
|
+
/**
|
|
12
|
+
* Shows a loading notification while the promise is pending, then a
|
|
13
|
+
* success or error notification. Rethrows on rejection and returns the
|
|
14
|
+
* resolved value so it can wrap existing async flows transparently.
|
|
15
|
+
*/
|
|
16
|
+
promise: async (promise, messages) => {
|
|
17
|
+
notify.loading(messages.loading);
|
|
18
|
+
try {
|
|
19
|
+
const value = await promise;
|
|
20
|
+
const title = typeof messages.success === "function" ? messages.success(value) : messages.success;
|
|
21
|
+
notify.success(title);
|
|
22
|
+
return value;
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
const title = typeof messages.error === "function" ? messages.error(error) : messages.error;
|
|
26
|
+
notify.error(title);
|
|
27
|
+
throw error;
|
|
28
|
+
}
|
|
29
|
+
},
|
|
30
|
+
hide: () => globalUIStore.getState().hide(),
|
|
31
|
+
});
|
package/llms-full.md
CHANGED
|
@@ -34,7 +34,7 @@ the root when the app uses package feedback or overlay components.
|
|
|
34
34
|
`UIProvider` owns the package `Notification`, `StatusBar`, and default
|
|
35
35
|
`@rn-primitives` portal host. Mount it before using `Dialog`, `AlertDialog`,
|
|
36
36
|
`BottomSheet`, `Drawer`, `DropdownMenu`, `Popover`, `SelectContent`,
|
|
37
|
-
`Tooltip`, or `globalUIStore` notifications.
|
|
37
|
+
`Tooltip`, or `notify` / `globalUIStore` notifications.
|
|
38
38
|
|
|
39
39
|
On native, `BottomSheet.Content` composes its sheet transform with React Native
|
|
40
40
|
keyboard event values. Pass `avoidKeyboard={false}` for sheets that should not
|
|
@@ -66,7 +66,7 @@ import { Button, StyledText, UIProvider } from "@mrmeg/expo-ui/components";
|
|
|
66
66
|
import { Button as ButtonDirect } from "@mrmeg/expo-ui/components/Button";
|
|
67
67
|
import { colors, spacing, typography } from "@mrmeg/expo-ui/constants";
|
|
68
68
|
import { useResources, useTheme } from "@mrmeg/expo-ui/hooks";
|
|
69
|
-
import { globalUIStore, useThemeStore } from "@mrmeg/expo-ui/state";
|
|
69
|
+
import { globalUIStore, notify, useThemeStore } from "@mrmeg/expo-ui/state";
|
|
70
70
|
import { configureExpoUiI18n, hapticLight } from "@mrmeg/expo-ui/lib";
|
|
71
71
|
```
|
|
72
72
|
|
|
@@ -101,7 +101,7 @@ Use this catalog before creating a new app-local primitive.
|
|
|
101
101
|
| `InputOTP` | `@mrmeg/expo-ui/components` | Verification code entry | Prefer over manually managed text input groups. |
|
|
102
102
|
| `Label` | `@mrmeg/expo-ui/components` | Accessible form labels | Use with package form controls. |
|
|
103
103
|
| `MaxWidthContainer` | `@mrmeg/expo-ui/components` | Centered responsive width | Use for web and tablet constrained layouts. |
|
|
104
|
-
| `Notification` | `@mrmeg/expo-ui/components` | Global toast surface | Trigger through `globalUIStore` with root `UIProvider`; optional actions dismiss after press. |
|
|
104
|
+
| `Notification` | `@mrmeg/expo-ui/components` | Global toast surface | Trigger through `notify` (or `globalUIStore` for subscriptions/tests) with root `UIProvider`; optional actions dismiss after press. |
|
|
105
105
|
| `Popover` | `@mrmeg/expo-ui/components` | Anchored contextual content | Requires root `UIProvider` portal setup. |
|
|
106
106
|
| `Progress` | `@mrmeg/expo-ui/components` | Determinate or indeterminate progress | Prefer over layout-shifting spinners for progress regions. |
|
|
107
107
|
| `RadioGroup` | `@mrmeg/expo-ui/components` | Small mutually exclusive choices | Use `Select` for longer option sets. |
|
|
@@ -134,6 +134,37 @@ full page sections. Use `EmptyState` for no-data or recoverable error regions,
|
|
|
134
134
|
`Skeleton` for loading content with stable layout, and `Progress` for real
|
|
135
135
|
progress or indeterminate long-running work.
|
|
136
136
|
|
|
137
|
+
## Notifications
|
|
138
|
+
|
|
139
|
+
`notify` is the primary imperative API for triggering the `Notification` component. Import from `@mrmeg/expo-ui/state` (also re-exported from the package root).
|
|
140
|
+
|
|
141
|
+
Notifications auto-dismiss after 4s (`DEFAULT_NOTIFICATION_DURATION`) unless a `duration` is given; pass `duration: 0` to keep one up until dismissed. `notify.loading` never auto-dismisses.
|
|
142
|
+
|
|
143
|
+
```ts
|
|
144
|
+
import { notify } from "@mrmeg/expo-ui/state";
|
|
145
|
+
|
|
146
|
+
notify.success("Saved", { messages: ["Your changes were saved."] });
|
|
147
|
+
notify.error("Upload failed");
|
|
148
|
+
notify.warning("Connection slow");
|
|
149
|
+
notify.info("Copied to clipboard");
|
|
150
|
+
|
|
151
|
+
// Loading spinner — persists until replaced or hidden (no auto-dismiss)
|
|
152
|
+
notify.loading("Uploading…");
|
|
153
|
+
notify.hide();
|
|
154
|
+
|
|
155
|
+
// Full control (same payload as globalUIStore show())
|
|
156
|
+
notify({ type: "success", title: "Saved", duration: 3000, position: "bottom" });
|
|
157
|
+
|
|
158
|
+
// Loading → success/error around a promise; rethrows on rejection
|
|
159
|
+
await notify.promise(saveProfile(), {
|
|
160
|
+
loading: "Saving…",
|
|
161
|
+
success: "Profile saved", // or (value) => `Saved ${value.name}`
|
|
162
|
+
error: "Could not save profile", // or (err) => err.message
|
|
163
|
+
});
|
|
164
|
+
```
|
|
165
|
+
|
|
166
|
+
`globalUIStore` (the underlying zustand store) remains available for reactive selectors and tests. Use `notify` for all imperative triggers in app code.
|
|
167
|
+
|
|
137
168
|
## Validation
|
|
138
169
|
|
|
139
170
|
Run the UI package gates when changing package code or shipped docs:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mrmeg/expo-ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Reusable Expo and React Native UI primitives for MrMeg projects.",
|
|
6
6
|
"keywords": [
|
|
@@ -108,6 +108,7 @@
|
|
|
108
108
|
"@rn-primitives/tooltip": "~1.4.0"
|
|
109
109
|
},
|
|
110
110
|
"peerDependencies": {
|
|
111
|
+
"@expo/ui": ">=56.0.0 <57.0.0",
|
|
111
112
|
"@react-native-async-storage/async-storage": ">=2.2.0 <2.3.0",
|
|
112
113
|
"expo": ">=55.0.0 <57.0.0",
|
|
113
114
|
"expo-font": ">=55.0.0 <57.0.0",
|
|
@@ -121,7 +122,7 @@
|
|
|
121
122
|
"zustand": ">=5.0.0 <6.0.0"
|
|
122
123
|
},
|
|
123
124
|
"devDependencies": {
|
|
124
|
-
"@types/react": "~19.2.
|
|
125
|
+
"@types/react": "~19.2.17",
|
|
125
126
|
"typescript": "~6.0.3"
|
|
126
127
|
}
|
|
127
128
|
}
|
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
import { useEffect, useRef } from "react";
|
|
2
|
-
import { Animated, Keyboard, Platform } from "react-native";
|
|
3
|
-
// This value drives layout props (a sheet's `bottom`/`height`), which the
|
|
4
|
-
// native animation driver can't touch — so timings here must stay on the JS
|
|
5
|
-
// driver. It also means the value is a positive keyboard height (0 when
|
|
6
|
-
// hidden), letting consumers both lift and shrink the sheet from one source.
|
|
7
|
-
function animateKeyboardOffset(value, toValue, duration = 180) {
|
|
8
|
-
Animated.timing(value, {
|
|
9
|
-
toValue,
|
|
10
|
-
duration,
|
|
11
|
-
useNativeDriver: false,
|
|
12
|
-
}).start();
|
|
13
|
-
}
|
|
14
|
-
export function useBottomSheetKeyboardAnimation() {
|
|
15
|
-
const keyboardHeight = useRef(new Animated.Value(0)).current;
|
|
16
|
-
useEffect(() => {
|
|
17
|
-
if (Platform.OS === "web") {
|
|
18
|
-
return;
|
|
19
|
-
}
|
|
20
|
-
const showEvent = Platform.OS === "ios" ? "keyboardWillShow" : "keyboardDidShow";
|
|
21
|
-
const hideEvent = Platform.OS === "ios" ? "keyboardWillHide" : "keyboardDidHide";
|
|
22
|
-
const showSubscription = Keyboard.addListener(showEvent, (event) => {
|
|
23
|
-
animateKeyboardOffset(keyboardHeight, event.endCoordinates.height, event.duration || 180);
|
|
24
|
-
});
|
|
25
|
-
const hideSubscription = Keyboard.addListener(hideEvent, (event) => {
|
|
26
|
-
animateKeyboardOffset(keyboardHeight, 0, event.duration || 160);
|
|
27
|
-
});
|
|
28
|
-
return () => {
|
|
29
|
-
showSubscription.remove();
|
|
30
|
-
hideSubscription.remove();
|
|
31
|
-
};
|
|
32
|
-
}, [keyboardHeight]);
|
|
33
|
-
return { height: keyboardHeight };
|
|
34
|
-
}
|
|
35
|
-
export const BottomSheetKeyboardController = {
|
|
36
|
-
dismiss() {
|
|
37
|
-
Keyboard.dismiss();
|
|
38
|
-
},
|
|
39
|
-
};
|