@mrmeg/expo-ui 0.7.3 → 0.9.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.
Files changed (53) hide show
  1. package/LLM_USAGE.md +24 -13
  2. package/README.md +8 -10
  3. package/dist/components/Accordion.d.ts +4 -4
  4. package/dist/components/AnimatedView.d.ts +1 -1
  5. package/dist/components/Badge.d.ts +1 -1
  6. package/dist/components/BottomSheet.d.ts +7 -7
  7. package/dist/components/Button.d.ts +3 -3
  8. package/dist/components/Button.js +17 -1
  9. package/dist/components/Card.d.ts +6 -6
  10. package/dist/components/Checkbox.d.ts +2 -1
  11. package/dist/components/Collapsible.d.ts +4 -3
  12. package/dist/components/Dialog.d.ts +10 -10
  13. package/dist/components/DismissKeyboard.d.ts +1 -1
  14. package/dist/components/Drawer.d.ts +7 -7
  15. package/dist/components/DropdownMenu.d.ts +10 -10
  16. package/dist/components/EmptyState.d.ts +1 -1
  17. package/dist/components/ErrorBoundary.d.ts +1 -1
  18. package/dist/components/Icon.d.ts +1 -1
  19. package/dist/components/InputOTP.d.ts +2 -1
  20. package/dist/components/Label.d.ts +1 -1
  21. package/dist/components/MaxWidthContainer.d.ts +1 -1
  22. package/dist/components/Notification.d.ts +4 -10
  23. package/dist/components/Notification.js +12 -13
  24. package/dist/components/Popover.d.ts +4 -4
  25. package/dist/components/Progress.d.ts +2 -1
  26. package/dist/components/RadioGroup.d.ts +3 -2
  27. package/dist/components/SegmentedControl.d.ts +2 -1
  28. package/dist/components/Select.d.ts +7 -7
  29. package/dist/components/Separator.d.ts +2 -1
  30. package/dist/components/Skeleton.d.ts +5 -4
  31. package/dist/components/Slider.d.ts +2 -1
  32. package/dist/components/StatusBar.d.ts +1 -1
  33. package/dist/components/StyledText.d.ts +12 -12
  34. package/dist/components/Switch.d.ts +2 -1
  35. package/dist/components/Tabs.d.ts +5 -5
  36. package/dist/components/TextInput.d.ts +1 -1
  37. package/dist/components/TextInput.js +9 -1
  38. package/dist/components/Toggle.d.ts +3 -2
  39. package/dist/components/ToggleGroup.d.ts +4 -3
  40. package/dist/components/ToggleGroup.js +2 -7
  41. package/dist/components/Tooltip.d.ts +3 -3
  42. package/dist/components/UIProvider.d.ts +1 -1
  43. package/dist/hooks/useTheme.d.ts +1 -1
  44. package/dist/hooks/useTheme.js +28 -93
  45. package/dist/state/globalUIStore.d.ts +9 -1
  46. package/dist/state/globalUIStore.js +9 -1
  47. package/dist/state/index.d.ts +1 -0
  48. package/dist/state/index.js +1 -0
  49. package/dist/state/notify.d.ts +50 -0
  50. package/dist/state/notify.js +31 -0
  51. package/dist/state/themeColorScope.d.ts +1 -1
  52. package/llms-full.md +34 -3
  53. package/package.json +2 -2
@@ -42,7 +42,7 @@ function getCachedOrCompute(key, compute) {
42
42
  * - getTextColorForBackground("#000") → "light"
43
43
  * - getContrastingColor("#f4f4f4", "#222", "#fff") → "#222"
44
44
  * - withAlpha("#336699", 0.6) → "rgba(51,102,153,0.6)"
45
- * - getShadowStyle('base') → { shadowColor, shadowOffset, ... }
45
+ * - getShadowStyle('base') → { boxShadow: "0px 1px 3px rgba(0, 0, 0, 0.1)" }
46
46
  */
47
47
  export function useTheme() {
48
48
  const userTheme = useThemeStore((s) => s.userTheme);
@@ -93,102 +93,37 @@ export function useTheme() {
93
93
  }, [setTheme, userTheme]);
94
94
  /**
95
95
  * getShadowStyle
96
- * Returns platform-appropriate shadow styles
97
- * - Web: uses CSS boxShadow through React Native Web
98
- * - Native: uses shadowColor, shadowOffset, shadowOpacity, shadowRadius, elevation
96
+ * Returns a cross-platform shadow style using the `boxShadow` style prop.
97
+ *
98
+ * RN 0.85 + react-native-web 0.21 deprecate the legacy `shadow*` props in
99
+ * favor of `boxShadow`, which is supported on both native and web. Because
100
+ * `boxShadow` has no separate opacity field, each preset's opacity is folded
101
+ * into the color's alpha via `withAlpha`. `elevation` is dropped — `boxShadow`
102
+ * renders shadows on Android in 0.85+.
99
103
  */
100
104
  const getShadowStyle = useCallback((type) => {
105
+ // Each preset: [offsetX, offsetY, blurRadius, color, opacity].
106
+ // Darker themes get a stronger alpha so shadows stay visible.
107
+ const boost = theme.dark ? 3 : 1;
108
+ const overlay = theme.colors.overlay;
101
109
  const shadowConfigs = {
102
- base: {
103
- shadowColor: theme.colors.overlay,
104
- shadowOffset: { width: 0, height: 1 },
105
- shadowOpacity: 0.1,
106
- shadowRadius: 3,
107
- elevation: 3,
108
- },
109
- soft: {
110
- shadowColor: theme.colors.overlay,
111
- shadowOffset: { width: 0, height: 4 },
112
- shadowOpacity: 0.1,
113
- shadowRadius: 6,
114
- elevation: 4,
115
- },
116
- sharp: {
117
- shadowColor: theme.colors.overlay,
118
- shadowOffset: { width: 0, height: 1 },
119
- shadowOpacity: 0.15,
120
- shadowRadius: 1,
121
- elevation: 2,
122
- },
123
- subtle: {
124
- shadowColor: theme.colors.overlay,
125
- shadowOffset: { width: 0, height: 1 },
126
- shadowOpacity: 0.05,
127
- shadowRadius: 2,
128
- elevation: 1,
129
- },
130
- elevated: {
131
- shadowColor: theme.colors.overlay,
132
- shadowOffset: { width: 0, height: 20 },
133
- shadowOpacity: 0.15,
134
- shadowRadius: 40,
135
- elevation: 16,
136
- },
137
- glow: {
138
- shadowColor: theme.colors.primary,
139
- shadowOffset: { width: 0, height: 4 },
140
- shadowOpacity: 0.4,
141
- shadowRadius: 20,
142
- elevation: 10,
143
- },
144
- glass: {
145
- shadowColor: theme.colors.overlay,
146
- shadowOffset: { width: 0, height: 4 },
147
- shadowOpacity: 0.05,
148
- shadowRadius: 30,
149
- elevation: 4,
150
- },
151
- card: {
152
- shadowColor: theme.colors.overlay,
153
- shadowOffset: { width: 0, height: 2 },
154
- shadowOpacity: 0.08,
155
- shadowRadius: 8,
156
- elevation: 4,
157
- },
158
- cardHover: {
159
- shadowColor: theme.colors.overlay,
160
- shadowOffset: { width: 0, height: 8 },
161
- shadowOpacity: 0.12,
162
- shadowRadius: 24,
163
- elevation: 8,
164
- },
165
- cardSubtle: {
166
- shadowColor: theme.colors.overlay,
167
- shadowOffset: { width: 0, height: 1 },
168
- shadowOpacity: 0.08,
169
- shadowRadius: 3,
170
- elevation: 2,
171
- },
110
+ base: { x: 0, y: 1, blur: 3, color: overlay, opacity: 0.1 },
111
+ soft: { x: 0, y: 4, blur: 6, color: overlay, opacity: 0.1 },
112
+ sharp: { x: 0, y: 1, blur: 1, color: overlay, opacity: 0.15 },
113
+ subtle: { x: 0, y: 1, blur: 2, color: overlay, opacity: 0.05 },
114
+ elevated: { x: 0, y: 20, blur: 40, color: overlay, opacity: 0.15 },
115
+ glow: { x: 0, y: 4, blur: 20, color: theme.colors.primary, opacity: 0.4 },
116
+ glass: { x: 0, y: 4, blur: 30, color: overlay, opacity: 0.05 },
117
+ card: { x: 0, y: 2, blur: 8, color: overlay, opacity: 0.08 },
118
+ cardHover: { x: 0, y: 8, blur: 24, color: overlay, opacity: 0.12 },
119
+ cardSubtle: { x: 0, y: 1, blur: 3, color: overlay, opacity: 0.08 },
120
+ };
121
+ const { x, y, blur, color, opacity } = shadowConfigs[type];
122
+ // Don't boost the glow accent — it's already a deliberate, vivid alpha.
123
+ const alpha = color === theme.colors.primary ? opacity : Math.min(opacity * boost, 1);
124
+ return {
125
+ boxShadow: `${x}px ${y}px ${blur}px ${withAlpha(color, alpha)}`,
172
126
  };
173
- const config = shadowConfigs[type];
174
- if (Platform.OS === "web") {
175
- const webShadows = {
176
- base: { boxShadow: theme.dark ? "0 1px 2px rgba(0, 0, 0, 0.45)" : "0 1px 2px rgba(0, 0, 0, 0.08)" },
177
- soft: { boxShadow: theme.dark ? "0 8px 24px rgba(0, 0, 0, 0.36)" : "0 8px 24px rgba(0, 0, 0, 0.10)" },
178
- sharp: { boxShadow: theme.dark ? "0 1px 1px rgba(0, 0, 0, 0.55)" : "0 1px 1px rgba(0, 0, 0, 0.12)" },
179
- subtle: { boxShadow: theme.dark ? "0 1px 2px rgba(0, 0, 0, 0.32)" : "0 1px 2px rgba(0, 0, 0, 0.05)" },
180
- elevated: { boxShadow: theme.dark ? "0 20px 40px rgba(0, 0, 0, 0.38)" : "0 20px 40px rgba(0, 0, 0, 0.15)" },
181
- glow: { boxShadow: `0 0 20px ${theme.colors.primary}` },
182
- glass: { boxShadow: theme.dark ? "0 4px 30px rgba(0, 0, 0, 0.32)" : "0 4px 30px rgba(0, 0, 0, 0.05)" },
183
- card: { boxShadow: theme.dark ? "0 1px 2px rgba(0, 0, 0, 0.32)" : "0 1px 3px rgba(0, 0, 0, 0.08)" },
184
- cardHover: { boxShadow: theme.dark ? "0 8px 24px rgba(0, 0, 0, 0.36)" : "0 8px 24px rgba(0, 0, 0, 0.12)" },
185
- cardSubtle: { boxShadow: theme.dark ? "0 1px 2px rgba(0, 0, 0, 0.32)" : "0 1px 3px rgba(0, 0, 0, 0.05)" },
186
- };
187
- return webShadows[type];
188
- }
189
- return Platform.select({
190
- default: config,
191
- });
192
127
  }, [theme]);
193
128
  const getFocusRingStyle = useCallback((offset = 2) => {
194
129
  if (Platform.OS !== "web") {
@@ -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
- * Recommended: wrap in hooks or utility functions for cleaner usage across components.
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: { ...alert, show: true }
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
  }));
@@ -1,4 +1,5 @@
1
1
  export * from "./globalUIStore";
2
+ export * from "./notify";
2
3
  export * from "./themeStore";
3
4
  export * from "./themeColorScope";
4
5
  export * from "./SsrViewportContext";
@@ -1,4 +1,5 @@
1
1
  export * from "./globalUIStore.js";
2
+ export * from "./notify.js";
2
3
  export * from "./themeStore.js";
3
4
  export * from "./themeColorScope.js";
4
5
  export * from "./SsrViewportContext.js";
@@ -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
+ });
@@ -6,4 +6,4 @@ export declare function ThemeColorScope({ colors, children, }: {
6
6
  /** Per-scheme partial overrides — same shape as `setColors`. */
7
7
  colors: ColorOverrides;
8
8
  children: ReactNode;
9
- }): import("react/jsx-runtime").JSX.Element;
9
+ }): import("react").JSX.Element;
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.7.3",
3
+ "version": "0.9.0",
4
4
  "private": false,
5
5
  "description": "Reusable Expo and React Native UI primitives for MrMeg projects.",
6
6
  "keywords": [
@@ -122,7 +122,7 @@
122
122
  "zustand": ">=5.0.0 <6.0.0"
123
123
  },
124
124
  "devDependencies": {
125
- "@types/react": "~19.2.14",
125
+ "@types/react": "~19.2.17",
126
126
  "typescript": "~6.0.3"
127
127
  }
128
128
  }