@mrmeg/expo-ui 0.7.0 → 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LLM_USAGE.md CHANGED
@@ -184,7 +184,7 @@ Use this table before creating a new app-local primitive.
184
184
  | `InputOTP` | Verification code entry | Multiple manually managed text inputs | Email codes, SMS codes, MFA, invite codes |
185
185
  | `Label` | Accessible form labels | Plain styled text labels | Required labels, disabled labels, field group labels |
186
186
  | `MaxWidthContainer` | Centered responsive width | Per-screen max-width wrappers | Web pages, tablet layouts, settings forms |
187
- | `Notification` | Global toast surface | Screen-local toast state | Saved/error/sync notifications, bottom-position alerts |
187
+ | `Notification` | Global toast surface | Screen-local toast state | Saved/error/sync notifications, action toasts, bottom-position alerts |
188
188
  | `Popover` | Anchored contextual content | Custom anchored views | Inline help, quick previews, contextual controls |
189
189
  | `Progress` | Determinate or indeterminate progress | Layout-shifting spinners for progress regions | Upload progress, onboarding completion |
190
190
  | `RadioGroup`, `RadioGroupItem` | Mutually exclusive choices | Custom radio rows | Plan interval, visibility choice, survey answer |
@@ -259,5 +259,9 @@ globalUIStore.getState().show({
259
259
  type: "success",
260
260
  title: "Saved",
261
261
  messages: ["Your changes were saved."],
262
+ action: {
263
+ label: "View",
264
+ onPress: openSavedItem,
265
+ },
262
266
  });
263
267
  ```
package/README.md CHANGED
@@ -224,7 +224,7 @@ All components are exported from `@mrmeg/expo-ui/components`; direct imports suc
224
224
  | `InputOTP` | Verification code entry | Multiple manually managed text inputs | Email codes, SMS codes, MFA, invite codes |
225
225
  | `Label` | Accessible form labels | Plain styled text labels | Required labels, disabled labels, field group labels |
226
226
  | `MaxWidthContainer` | Centered responsive width | Per-screen max-width wrappers | Web pages, tablet layouts, settings forms, auth panels |
227
- | `Notification` | Global toast surface | Screen-local toast state | Saved/error/sync notifications, loading toast, bottom-position alerts |
227
+ | `Notification` | Global toast surface | Screen-local toast state | Saved/error/sync notifications, action toasts, loading toast, bottom-position alerts |
228
228
  | `Popover` | Anchored contextual content | Custom anchored views | Inline help, quick previews, contextual controls, small forms |
229
229
  | `Progress` | Determinate or indeterminate progress | Layout-shifting spinners for progress regions | Upload progress, onboarding completion, long-running task state |
230
230
  | `RadioGroup`, `RadioGroupItem` | Mutually exclusive choices | Custom radio rows | Plan interval, visibility choice, survey answer, preference setting |
@@ -332,6 +332,10 @@ globalUIStore.getState().show({
332
332
  type: "success",
333
333
  title: "Saved",
334
334
  messages: ["Your changes were saved."],
335
+ action: {
336
+ label: "View",
337
+ onPress: openSavedItem,
338
+ },
335
339
  });
336
340
  ```
337
341
 
@@ -46,14 +46,29 @@ export const Notification = () => {
46
46
  const position = alert?.position ?? "top";
47
47
  const isBottom = position === "bottom";
48
48
  // Just opacity + translateY — no scale (scale = bouncy feel)
49
- const opacity = useRef(new Animated.Value(0)).current;
50
- const translateY = useRef(new Animated.Value(0)).current;
49
+ const opacityRef = useRef(null);
50
+ if (opacityRef.current === null) {
51
+ opacityRef.current = new Animated.Value(0);
52
+ }
53
+ const opacity = opacityRef.current;
54
+ const translateYRef = useRef(null);
55
+ if (translateYRef.current === null) {
56
+ translateYRef.current = new Animated.Value(0);
57
+ }
58
+ const translateY = translateYRef.current;
51
59
  const wasVisibleRef = useRef(false);
52
60
  const timerRef = useRef(null);
53
61
  const hideNotification = useCallback(() => {
54
62
  hide();
55
63
  }, [hide]);
64
+ const clearAutoDismissTimer = useCallback(() => {
65
+ if (timerRef.current) {
66
+ clearTimeout(timerRef.current);
67
+ timerRef.current = null;
68
+ }
69
+ }, []);
56
70
  const animateOut = useCallback(() => {
71
+ clearAutoDismissTimer();
57
72
  if (reduceMotion) {
58
73
  opacity.setValue(0);
59
74
  hideNotification();
@@ -73,7 +88,18 @@ export const Notification = () => {
73
88
  if (finished)
74
89
  hideNotification();
75
90
  });
76
- }, [reduceMotion, isBottom, opacity, translateY, hideNotification]);
91
+ }, [clearAutoDismissTimer, reduceMotion, isBottom, opacity, translateY, hideNotification]);
92
+ const handleActionPress = useCallback(() => {
93
+ const action = alert?.action;
94
+ if (!action)
95
+ return;
96
+ try {
97
+ action.onPress();
98
+ }
99
+ finally {
100
+ animateOut();
101
+ }
102
+ }, [alert?.action, animateOut]);
77
103
  // The auto-dismiss timer only needs the latest animateOut; wrapping it in an
78
104
  // Effect Event keeps it out of the deps so the effect doesn't re-run (and
79
105
  // restart the timer) every time animateOut's identity changes.
@@ -180,6 +206,7 @@ export const Notification = () => {
180
206
  const message = alert?.messages?.find((item) => item.trim().length > 0);
181
207
  const title = getTitle(message);
182
208
  const hasMessage = !!message;
209
+ const action = alert?.action;
183
210
  if (!alert?.show) {
184
211
  return null;
185
212
  }
@@ -195,7 +222,14 @@ export const Notification = () => {
195
222
  styles.alert,
196
223
  isBottom && styles.alertBottom,
197
224
  getShadowStyle("base"),
198
- ], children: [_jsx(View, { style: [styles.iconBadge, { backgroundColor: iconBgColor }], children: alert?.loading ? (_jsx(ActivityIndicator, { size: "small", color: iconColor })) : (_jsx(Icon, { name: icon, size: 18, color: iconColor })) }), _jsxs(View, { style: styles.alertContent, children: [!!title && (_jsx(StyledText, { selectable: false, style: [styles.alertTitle, { color: theme.colors.foreground }], numberOfLines: 1, children: title })), hasMessage && (_jsx(StyledText, { selectable: false, style: [styles.alertDescription, { color: theme.colors.mutedForeground }], numberOfLines: 2, children: message }))] }), _jsx(Pressable, { style: styles.closeButton, hitSlop: spacing.sm, onPress: animateOut, accessibilityLabel: "Dismiss notification", accessibilityRole: "button", children: _jsx(Icon, { name: "x", size: 16, color: theme.colors.mutedForeground }) })] }) }));
225
+ ], children: [_jsx(View, { style: [styles.iconBadge, { backgroundColor: iconBgColor }], children: alert?.loading ? (_jsx(ActivityIndicator, { size: "small", color: iconColor })) : (_jsx(Icon, { name: icon, size: 18, color: iconColor })) }), _jsxs(View, { style: styles.alertContent, children: [!!title && (_jsx(StyledText, { selectable: false, style: [styles.alertTitle, { color: theme.colors.foreground }], numberOfLines: 1, children: title })), hasMessage && (_jsx(StyledText, { selectable: false, style: [styles.alertDescription, { color: theme.colors.mutedForeground }], numberOfLines: 2, children: message }))] }), action && (_jsx(Pressable, { style: ({ pressed }) => [
226
+ styles.actionButton,
227
+ {
228
+ borderColor: theme.colors.primary + "30",
229
+ backgroundColor: theme.colors.primary + "10",
230
+ },
231
+ pressed && styles.actionButtonPressed,
232
+ ], hitSlop: spacing.xs, onPress: handleActionPress, accessibilityLabel: action.label, accessibilityRole: "button", children: _jsx(StyledText, { selectable: false, style: [styles.actionLabel, { color: theme.colors.primary }], numberOfLines: 1, children: action.label }) })), _jsx(Pressable, { style: styles.closeButton, hitSlop: spacing.sm, onPress: animateOut, accessibilityLabel: "Dismiss notification", accessibilityRole: "button", children: _jsx(Icon, { name: "x", size: 16, color: theme.colors.mutedForeground }) })] }) }));
199
233
  };
200
234
  const createStyles = (theme) => StyleSheet.create({
201
235
  container: {
@@ -248,6 +282,26 @@ const createStyles = (theme) => StyleSheet.create({
248
282
  fontSize: 13,
249
283
  lineHeight: 18,
250
284
  },
285
+ actionButton: {
286
+ minHeight: 28,
287
+ maxWidth: 140,
288
+ paddingHorizontal: spacing.sm,
289
+ borderRadius: spacing.radiusSm,
290
+ borderWidth: 1,
291
+ justifyContent: "center",
292
+ alignItems: "center",
293
+ flexShrink: 0,
294
+ ...(Platform.OS === "web" && { cursor: "pointer" }),
295
+ },
296
+ actionButtonPressed: {
297
+ opacity: 0.75,
298
+ },
299
+ actionLabel: {
300
+ fontFamily: fontFamilies.sansSerif.regular,
301
+ fontWeight: "600",
302
+ fontSize: 13,
303
+ lineHeight: 18,
304
+ },
251
305
  closeButton: {
252
306
  position: "absolute",
253
307
  top: spacing.sm,
@@ -5,26 +5,33 @@
5
5
  * Primarily used to trigger and dismiss the `Notification` component globally.
6
6
  *
7
7
  * Methods:
8
- * - show({ type, title, messages, duration, loading }): displays a notification
8
+ * - show({ type, title, messages, duration, loading, action }): displays a notification
9
9
  * - hide(): hides the current notification
10
10
  *
11
11
  * Recommended: wrap in hooks or utility functions for cleaner usage across components.
12
12
  */
13
- type State = {
14
- alert: {
15
- show: boolean;
16
- type: "error" | "success" | "info" | "warning";
17
- messages?: string[];
18
- title?: string;
19
- duration?: number;
20
- loading?: boolean;
21
- /** Where to display the notification */
22
- position?: "top" | "bottom";
23
- } | null;
13
+ export type GlobalNotificationType = "error" | "success" | "info" | "warning";
14
+ export type GlobalNotificationPosition = "top" | "bottom";
15
+ export type GlobalNotificationAction = {
16
+ label: string;
17
+ onPress: () => void;
24
18
  };
25
- type Actions = {
26
- show: (alert: Omit<NonNullable<State["alert"]>, "show">) => void;
19
+ export type GlobalNotificationAlert = {
20
+ show: boolean;
21
+ type: GlobalNotificationType;
22
+ title?: string;
23
+ messages?: string[];
24
+ duration?: number;
25
+ loading?: boolean;
26
+ /** Where to display the notification */
27
+ position?: GlobalNotificationPosition;
28
+ action?: GlobalNotificationAction;
29
+ };
30
+ export type GlobalUIState = {
31
+ alert: GlobalNotificationAlert | null;
32
+ };
33
+ export type GlobalUIActions = {
34
+ show: (alert: Omit<GlobalNotificationAlert, "show">) => void;
27
35
  hide: () => void;
28
36
  };
29
- export declare const globalUIStore: import("zustand").UseBoundStore<import("zustand").StoreApi<State & Actions>>;
30
- export {};
37
+ export declare const globalUIStore: import("zustand").UseBoundStore<import("zustand").StoreApi<GlobalUIState & GlobalUIActions>>;
package/llms-full.md CHANGED
@@ -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`. |
104
+ | `Notification` | `@mrmeg/expo-ui/components` | Global toast surface | Trigger through `globalUIStore` 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. |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrmeg/expo-ui",
3
- "version": "0.7.0",
3
+ "version": "0.7.1",
4
4
  "private": false,
5
5
  "description": "Reusable Expo and React Native UI primitives for MrMeg projects.",
6
6
  "keywords": [
@@ -69,6 +69,10 @@
69
69
  "types": "./dist/state/index.d.ts",
70
70
  "default": "./dist/state/index.js"
71
71
  },
72
+ "./state/*": {
73
+ "types": "./dist/state/*.d.ts",
74
+ "default": "./dist/state/*.js"
75
+ },
72
76
  "./lib": {
73
77
  "types": "./dist/lib/index.d.ts",
74
78
  "default": "./dist/lib/index.js"
@@ -76,8 +80,9 @@
76
80
  },
77
81
  "scripts": {
78
82
  "typecheck": "tsc --noEmit -p tsconfig.json",
79
- "test": "jest --config ../../jest.config.js packages/ui/src --runInBand --watchman=false",
80
- "build": "rm -rf dist && tsc -p tsconfig.build.json && node ../../scripts/fix-ui-package-esm.mjs",
83
+ "test": "jest --config ../../jest.config.js packages/ui/src --runInBand --watchman=false && bun run check:forbidden-imports",
84
+ "build": "rm -rf dist && tsc -p tsconfig.build.json && node ../../scripts/fix-ui-package-esm.mjs && bun run check:forbidden-imports",
85
+ "check:forbidden-imports": "node ../../scripts/check-ui-forbidden-imports.mjs",
81
86
  "publish:dry-run": "bun pm pack --dry-run"
82
87
  },
83
88
  "dependencies": {