@mrmeg/expo-ui 0.1.7 → 0.1.9

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
@@ -139,6 +139,11 @@ const { styles } = useStyles(({ theme, spacing, withAlpha }) => ({
139
139
  }));
140
140
  ```
141
141
 
142
+ When the saved theme preference is `system`, the package theme store owns the
143
+ OS color-scheme subscription, including web `prefers-color-scheme`. Do not add
144
+ app-local Appearance or `matchMedia` listeners for package components; import
145
+ `useTheme()`, `useStyles()`, and `useThemeStore` from `@mrmeg/expo-ui`.
146
+
142
147
  ## Component Use-Case Index
143
148
 
144
149
  Use this table before creating a new app-local primitive.
package/README.md CHANGED
@@ -11,10 +11,13 @@ permission.
11
11
  ## For LLMs And Coding Agents
12
12
 
13
13
  When this package is installed from npm, read
14
- `node_modules/@mrmeg/expo-ui/LLM_USAGE.md` before creating app-local UI
15
- primitives. That file is shipped in the npm package and gives the short
16
- component-selection rules, import paths, setup requirements, and examples that
17
- help agents choose existing package components instead of rebuilding them.
14
+ `node_modules/@mrmeg/expo-ui/llms.txt` first, then
15
+ `node_modules/@mrmeg/expo-ui/LLM_USAGE.md` or
16
+ `node_modules/@mrmeg/expo-ui/llms-full.md` before creating app-local UI
17
+ primitives. Those files are shipped in the npm package and give the short
18
+ component-selection rules, import paths, setup requirements, component catalog,
19
+ and examples that help agents choose existing package components instead of
20
+ rebuilding them.
18
21
 
19
22
  ## Install
20
23
 
@@ -97,6 +100,11 @@ const styles = StyleSheet.create({
97
100
 
98
101
  `useTheme()` returns the active `theme`, resolved `scheme`, persisted `currentTheme`, `setTheme`, `toggleTheme`, cross-platform shadow helpers, a web focus-ring helper, contrast helpers, and `withAlpha`. `getShadowStyle(type)` supports `base`, `soft`, `sharp`, `subtle`, `elevated`, `glow`, `glass`, `card`, `cardHover`, and `cardSubtle`. Use semantic tokens such as `theme.colors.background`, `foreground`, `card`, `popover`, `border`, `input`, `ring`, `primary`, `secondary`, `accent`, `mutedForeground`, `destructive`, `success`, and `warning`. `primary` is the neutral action color, `secondary` is a neutral secondary surface, `accent` is the teal highlight color, `input` is the default form-control border, and `ring` is the focus outline color.
99
102
 
103
+ When `currentTheme` is `"system"`, the package tracks the OS color scheme and
104
+ updates all `useTheme()` and `useStyles()` consumers through the package theme
105
+ store. Apps should not add their own Appearance or `matchMedia` listeners just
106
+ to make package components follow system light/dark changes.
107
+
100
108
  Use `useStyles()` when a component needs memoized theme-aware styles. The style factory receives `{ theme, spacing, withAlpha }`, and the returned hook value also includes the normal `useTheme()` helpers.
101
109
 
102
110
  ```tsx
@@ -1,4 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { useEffect } from "react";
2
3
  import { View, StyleSheet, Pressable, Platform } from "react-native";
3
4
  import Animated, { useSharedValue, useAnimatedStyle, withTiming, useReducedMotion, } from "react-native-reanimated";
4
5
  import { Icon } from "./Icon.js";
@@ -19,6 +20,15 @@ function Checkbox({ size = "md", label, indeterminate = false, error = false, st
19
20
  const sizeConfig = SIZE_CONFIGS[size];
20
21
  // Simple fast opacity for the checkmark icon
21
22
  const checkOpacity = useSharedValue(checked || indeterminate ? 1 : 0);
23
+ const isVisuallyChecked = !!checked || indeterminate;
24
+ useEffect(() => {
25
+ if (reduceMotion) {
26
+ checkOpacity.value = withTiming(isVisuallyChecked ? 1 : 0, { duration: 0 });
27
+ }
28
+ else {
29
+ checkOpacity.value = withTiming(isVisuallyChecked ? 1 : 0, { duration: 60 });
30
+ }
31
+ }, [checkOpacity, isVisuallyChecked, reduceMotion]);
22
32
  const wrappedOnCheckedChange = (next) => {
23
33
  if (next)
24
34
  hapticLight();
@@ -36,14 +46,14 @@ function Checkbox({ size = "md", label, indeterminate = false, error = false, st
36
46
  // Dynamic border color with sufficient contrast against background
37
47
  const borderColor = error
38
48
  ? theme.colors.destructive
39
- : checked || indeterminate
49
+ : isVisuallyChecked
40
50
  ? theme.colors.primary
41
51
  : getContrastingColor(theme.colors.background, theme.colors.text, theme.colors.textDim);
42
52
  // Flatten style override for web compatibility
43
53
  const flattenedStyle = styleOverride ? StyleSheet.flatten(styleOverride) : undefined;
44
54
  const checkboxElement = (_jsx(CheckboxPrimitive.Root, { ...props, checked: checked, onCheckedChange: wrappedOnCheckedChange, disabled: disabled, style: {
45
55
  borderColor,
46
- backgroundColor: checked || indeterminate ? theme.colors.primary : theme.colors.background,
56
+ backgroundColor: isVisuallyChecked ? theme.colors.primary : theme.colors.background,
47
57
  borderRadius: spacing.radiusSm,
48
58
  borderWidth: 1,
49
59
  width: sizeConfig.size,
@@ -23,4 +23,4 @@
23
23
  * });
24
24
  * ```
25
25
  */
26
- export declare const Notification: () => import("react/jsx-runtime").JSX.Element;
26
+ export declare const Notification: () => import("react/jsx-runtime").JSX.Element | null;
@@ -134,9 +134,12 @@ export const Notification = () => {
134
134
  };
135
135
  }
136
136
  };
137
- const getTitle = () => {
138
- if (alert?.title)
137
+ const getTitle = (message) => {
138
+ if (alert?.title?.trim())
139
139
  return alert.title;
140
+ if (alert?.loading) {
141
+ return translateText("notification.loading", "Loading");
142
+ }
140
143
  switch (alert?.type) {
141
144
  case "error":
142
145
  return translateText("notification.error", "Error");
@@ -145,14 +148,18 @@ export const Notification = () => {
145
148
  case "warning":
146
149
  return translateText("notification.warning", "Warning");
147
150
  case "info":
148
- return "";
151
+ return message ? "" : translateText("notification.info", "Info");
149
152
  default:
150
153
  return "";
151
154
  }
152
155
  };
153
156
  const { icon, color: iconColor, bgColor: iconBgColor } = getIconProps();
154
- const title = getTitle();
155
- const hasMessage = !!alert?.messages?.[0];
157
+ const message = alert?.messages?.find((item) => item.trim().length > 0);
158
+ const title = getTitle(message);
159
+ const hasMessage = !!message;
160
+ if (!alert?.show) {
161
+ return null;
162
+ }
156
163
  return (_jsx(Animated.View, { accessibilityLiveRegion: "polite", accessibilityRole: "alert", pointerEvents: alert?.show ? "auto" : "none", style: [
157
164
  styles.container,
158
165
  isBottom
@@ -164,7 +171,7 @@ export const Notification = () => {
164
171
  styles.alert,
165
172
  isBottom && styles.alertBottom,
166
173
  getShadowStyle("base"),
167
- ], 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: alert.messages[0] }))] }), _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 }) })] }) }));
174
+ ], 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 }) })] }) }));
168
175
  };
169
176
  const createStyles = (theme) => StyleSheet.create({
170
177
  container: {
@@ -133,9 +133,6 @@ export const StyledText = forwardRef((props, ref) => {
133
133
  },
134
134
  resolvedContextTextStyle,
135
135
  style,
136
- // When a parent (Button, ToggleGroupItem) sets TextColorContext,
137
- // that color must win over any color in the style prop
138
- contextColor != null && { color: contextColor },
139
136
  ], selectable: resolvedSelectable, ...otherProps, children: content }));
140
137
  });
141
138
  StyledText.displayName = "StyledText";
@@ -1,7 +1,7 @@
1
1
  import { useCallback, useEffect, useMemo } from "react";
2
2
  import { colors } from "../constants/colors.js";
3
- import { useColorScheme as useColorSchemeDefault, Platform, StyleSheet } from "react-native";
4
- import { useThemeStore } from "../state/themeStore.js";
3
+ import { Platform, StyleSheet } from "react-native";
4
+ import { resolveThemePreference, useThemeStore } from "../state/themeStore.js";
5
5
  import { spacing as spacingConstants } from "../constants/spacing.js";
6
6
  // Module-level cache for contrast calculations to avoid memory leak
7
7
  // and share across components
@@ -45,14 +45,10 @@ function getCachedOrCompute(key, compute) {
45
45
  */
46
46
  export function useTheme() {
47
47
  const userTheme = useThemeStore((s) => s.userTheme);
48
+ const systemTheme = useThemeStore((s) => s.systemTheme);
48
49
  const setTheme = useThemeStore((s) => s.setTheme);
49
- let defaultScheme = useColorSchemeDefault();
50
- // Ensure a scheme is selected, even if we fail to get one
51
- if (!defaultScheme) {
52
- defaultScheme = "light";
53
- }
54
50
  // Determine which theme to use (user preference or system)
55
- const effectiveScheme = userTheme === "system" ? defaultScheme : userTheme;
51
+ const effectiveScheme = resolveThemePreference(userTheme, systemTheme);
56
52
  const theme = colors[effectiveScheme];
57
53
  // Sync theme to DOM so CSS in +html.tsx follows the app's runtime theme
58
54
  useEffect(() => {
@@ -1,6 +1,13 @@
1
+ export type ThemePreference = "system" | "light" | "dark";
2
+ export type ResolvedTheme = "light" | "dark";
1
3
  export type ThemeStore = {
2
- userTheme: "system" | "light" | "dark";
3
- setTheme: (theme: "system" | "light" | "dark") => void;
4
+ userTheme: ThemePreference;
5
+ systemTheme: ResolvedTheme;
6
+ setTheme: (theme: ThemePreference) => void;
7
+ setSystemTheme: (theme: ResolvedTheme) => void;
4
8
  loadTheme: () => void;
5
9
  };
10
+ export declare function resolveThemePreference(userTheme: ThemePreference, systemTheme: ResolvedTheme): ResolvedTheme;
6
11
  export declare const useThemeStore: import("zustand").UseBoundStore<import("zustand").StoreApi<ThemeStore>>;
12
+ export declare function syncSystemTheme(): void;
13
+ export declare function startSystemThemeListener(): () => void;
@@ -1,11 +1,24 @@
1
- import { Platform } from "react-native";
1
+ import { Appearance, Platform } from "react-native";
2
2
  import { create } from "zustand";
3
3
  import AsyncStorage from "@react-native-async-storage/async-storage";
4
4
  const THEME_KEY = "user-theme-preference";
5
+ export function resolveThemePreference(userTheme, systemTheme) {
6
+ return userTheme === "system" ? systemTheme : userTheme;
7
+ }
8
+ function getSystemTheme() {
9
+ if (Platform.OS === "web" && typeof window !== "undefined" && typeof window.matchMedia === "function") {
10
+ return window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light";
11
+ }
12
+ return Appearance.getColorScheme() === "dark" ? "dark" : "light";
13
+ }
5
14
  export const useThemeStore = create((set) => ({
6
15
  userTheme: "system",
16
+ systemTheme: getSystemTheme(),
7
17
  setTheme: (theme) => {
8
- set({ userTheme: theme });
18
+ set({
19
+ userTheme: theme,
20
+ ...(theme === "system" ? { systemTheme: getSystemTheme() } : {}),
21
+ });
9
22
  // Save directly when setting theme
10
23
  if (Platform.OS !== "web") {
11
24
  AsyncStorage.setItem(THEME_KEY, theme).catch(() => {
@@ -16,6 +29,9 @@ export const useThemeStore = create((set) => ({
16
29
  localStorage.setItem(THEME_KEY, theme);
17
30
  }
18
31
  },
32
+ setSystemTheme: (theme) => {
33
+ set({ systemTheme: theme });
34
+ },
19
35
  loadTheme: () => {
20
36
  if (Platform.OS !== "web") {
21
37
  AsyncStorage.getItem(THEME_KEY).then((saved) => {
@@ -34,5 +50,45 @@ export const useThemeStore = create((set) => ({
34
50
  }
35
51
  }
36
52
  }));
53
+ let stopSystemThemeListener = null;
54
+ export function syncSystemTheme() {
55
+ useThemeStore.getState().setSystemTheme(getSystemTheme());
56
+ }
57
+ export function startSystemThemeListener() {
58
+ if (stopSystemThemeListener) {
59
+ return stopSystemThemeListener;
60
+ }
61
+ syncSystemTheme();
62
+ if (Platform.OS === "web" && typeof window !== "undefined" && typeof window.matchMedia === "function") {
63
+ const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
64
+ const onChange = () => {
65
+ useThemeStore.getState().setSystemTheme(mediaQuery.matches ? "dark" : "light");
66
+ };
67
+ if (typeof mediaQuery.addEventListener === "function") {
68
+ mediaQuery.addEventListener("change", onChange);
69
+ stopSystemThemeListener = () => {
70
+ mediaQuery.removeEventListener("change", onChange);
71
+ stopSystemThemeListener = null;
72
+ };
73
+ }
74
+ else {
75
+ mediaQuery.addListener(onChange);
76
+ stopSystemThemeListener = () => {
77
+ mediaQuery.removeListener(onChange);
78
+ stopSystemThemeListener = null;
79
+ };
80
+ }
81
+ return stopSystemThemeListener;
82
+ }
83
+ const subscription = Appearance.addChangeListener(({ colorScheme }) => {
84
+ useThemeStore.getState().setSystemTheme(colorScheme === "dark" ? "dark" : "light");
85
+ });
86
+ stopSystemThemeListener = () => {
87
+ subscription.remove();
88
+ stopSystemThemeListener = null;
89
+ };
90
+ return stopSystemThemeListener;
91
+ }
37
92
  // Load saved theme on store creation
38
93
  useThemeStore.getState().loadTheme();
94
+ startSystemThemeListener();
package/llms-full.md ADDED
@@ -0,0 +1,146 @@
1
+ # @mrmeg/expo-ui Full Contract
2
+
3
+ `@mrmeg/expo-ui` packages reusable Expo and React Native UI primitives for
4
+ MrMeg apps. It owns the shared design system, tokens, package theme store,
5
+ resource hook, global notification store, overlay shell, and small interaction
6
+ helpers. The consuming app owns routes, screens, feature state, auth, billing,
7
+ media, monitoring, API calls, product copy, and app-specific layouts.
8
+
9
+ ## Agent Rules
10
+
11
+ Do not recreate primitives that this package already provides. Import from
12
+ `@mrmeg/expo-ui` and compose package components in the app.
13
+
14
+ Use only exported package paths:
15
+
16
+ - `@mrmeg/expo-ui`
17
+ - `@mrmeg/expo-ui/components`
18
+ - `@mrmeg/expo-ui/components/*`
19
+ - `@mrmeg/expo-ui/constants`
20
+ - `@mrmeg/expo-ui/constants/*`
21
+ - `@mrmeg/expo-ui/hooks`
22
+ - `@mrmeg/expo-ui/hooks/*`
23
+ - `@mrmeg/expo-ui/state`
24
+ - `@mrmeg/expo-ui/lib`
25
+
26
+ Do not import from `@mrmeg/expo-ui/dist/*`, `packages/ui/src/*`, or copied
27
+ app-local component files.
28
+
29
+ ## App Setup
30
+
31
+ Call `useResources()` once near the Expo app root. Mount `UIProvider` once near
32
+ the root when the app uses package feedback or overlay components.
33
+
34
+ `UIProvider` owns the package `Notification`, `StatusBar`, and default
35
+ `@rn-primitives` portal host. Mount it before using `Dialog`, `AlertDialog`,
36
+ `BottomSheet`, `Drawer`, `DropdownMenu`, `Popover`, `SelectContent`,
37
+ `Tooltip`, or `globalUIStore` notifications.
38
+
39
+ i18n is optional. Plain children and `text` props work without `i18next` or
40
+ `react-i18next`. Use `configureExpoUiI18n()` only when a consuming app wants
41
+ package `tx` props translated by its app-owned i18n instance.
42
+
43
+ ## Theme Rules
44
+
45
+ Use `useTheme()` and `useStyles()` from `@mrmeg/expo-ui/hooks`. Use semantic
46
+ tokens such as `background`, `foreground`, `card`, `popover`, `border`,
47
+ `input`, `ring`, `primary`, `secondary`, `accent`, `mutedForeground`,
48
+ `destructive`, `success`, and `warning`.
49
+
50
+ Use `StyledText` and semantic text aliases instead of raw `Text` for app UI.
51
+ Use package controls instead of hardcoded Pressable/View/Text combinations.
52
+
53
+ When the saved theme preference is `system`, the package theme store owns OS
54
+ color-scheme sync, including web `prefers-color-scheme`. Apps should not add
55
+ their own `Appearance` or `matchMedia` listeners just to make package
56
+ components follow system light/dark changes.
57
+
58
+ ## Import Examples
59
+
60
+ ```tsx
61
+ import { Button, StyledText, UIProvider } from "@mrmeg/expo-ui/components";
62
+ import { Button as ButtonDirect } from "@mrmeg/expo-ui/components/Button";
63
+ import { colors, spacing, typography } from "@mrmeg/expo-ui/constants";
64
+ import { useResources, useTheme } from "@mrmeg/expo-ui/hooks";
65
+ import { globalUIStore, useThemeStore } from "@mrmeg/expo-ui/state";
66
+ import { configureExpoUiI18n, hapticLight } from "@mrmeg/expo-ui/lib";
67
+ ```
68
+
69
+ The root barrel also exports the public surface:
70
+
71
+ ```tsx
72
+ import { Button, UIProvider, colors, useTheme } from "@mrmeg/expo-ui";
73
+ ```
74
+
75
+ ## Component Catalog
76
+
77
+ Use this catalog before creating a new app-local primitive.
78
+
79
+ | Component | Import | Use When | Gotchas |
80
+ |-----------|--------|----------|---------|
81
+ | `Accordion` | `@mrmeg/expo-ui/components` | Multi-section disclosure such as FAQ or grouped settings | Use compound parts instead of custom expanders. |
82
+ | `Alert` | `@mrmeg/expo-ui/components` | Cross-platform imperative alerts | Avoid direct `window.alert` and duplicated native/web branching. |
83
+ | `AnimatedView` | `@mrmeg/expo-ui/components` | Entrance and visibility animation | Keep simple reveal effects in the package wrapper. |
84
+ | `Badge` | `@mrmeg/expo-ui/components` | Short status labels | Prefer over custom pill views. |
85
+ | `BottomSheet` | `@mrmeg/expo-ui/components` | Mobile-first modal sheets | Requires root `UIProvider` portal setup. |
86
+ | `Button` | `@mrmeg/expo-ui/components` | Commands and CTAs | Use `preset`, not `variant`; visible heights are compact. |
87
+ | `Card` | `@mrmeg/expo-ui/components` | Individual framed content groups | Do not wrap whole page sections in cards. |
88
+ | `Checkbox` | `@mrmeg/expo-ui/components` | Boolean selection in forms or lists | Prefer over custom checkmark controls. |
89
+ | `Collapsible` | `@mrmeg/expo-ui/components` | One-off disclosure | Use for advanced settings or helper sections. |
90
+ | `Dialog`, `AlertDialog` | `@mrmeg/expo-ui/components` | Blocking modal content or decisions | Requires root `UIProvider` portal setup. |
91
+ | `DismissKeyboard` | `@mrmeg/expo-ui/components` | Tap-away keyboard dismissal | Prefer over screen-level keyboard wrappers. |
92
+ | `Drawer` | `@mrmeg/expo-ui/components` | Side panels and drawer navigation | `Drawer.Content` owns safe-area top/bottom padding; do not duplicate it in children. |
93
+ | `DropdownMenu` | `@mrmeg/expo-ui/components` | Menus and command lists | Requires root `UIProvider` portal setup. |
94
+ | `EmptyState` | `@mrmeg/expo-ui/components` | No-data or recoverable error regions | Prefer over one-off empty placeholders. |
95
+ | `ErrorBoundary` | `@mrmeg/expo-ui/components` | React render error fallback | Use for route or feature boundaries. |
96
+ | `Icon` | `@mrmeg/expo-ui/components` | Feather or custom icons with theme tokens | Avoid raw vector icons with hardcoded colors. |
97
+ | `InputOTP` | `@mrmeg/expo-ui/components` | Verification code entry | Prefer over manually managed text input groups. |
98
+ | `Label` | `@mrmeg/expo-ui/components` | Accessible form labels | Use with package form controls. |
99
+ | `MaxWidthContainer` | `@mrmeg/expo-ui/components` | Centered responsive width | Use for web and tablet constrained layouts. |
100
+ | `Notification` | `@mrmeg/expo-ui/components` | Global toast surface | Trigger through `globalUIStore` with root `UIProvider`. |
101
+ | `Popover` | `@mrmeg/expo-ui/components` | Anchored contextual content | Requires root `UIProvider` portal setup. |
102
+ | `Progress` | `@mrmeg/expo-ui/components` | Determinate or indeterminate progress | Prefer over layout-shifting spinners for progress regions. |
103
+ | `RadioGroup` | `@mrmeg/expo-ui/components` | Small mutually exclusive choices | Use `Select` for longer option sets. |
104
+ | `Select` | `@mrmeg/expo-ui/components` | Option menus | `SelectContent` requires root `UIProvider` portal setup. |
105
+ | `Separator` | `@mrmeg/expo-ui/components` | Horizontal or vertical dividers | Prefer over border-only spacer views. |
106
+ | `Skeleton` | `@mrmeg/expo-ui/components` | Loading placeholders | Use stable dimensions to avoid layout shift. |
107
+ | `Slider` | `@mrmeg/expo-ui/components` | Numeric value selection | Prefer over custom pan gesture tracks. |
108
+ | `StatusBar` | `@mrmeg/expo-ui/components` | Theme-aware native status bar | Usually mounted through `UIProvider`. |
109
+ | `StyledText` | `@mrmeg/expo-ui/components` | Theme-aware typography | Prefer semantic aliases over raw `Text`. |
110
+ | `Switch` | `@mrmeg/expo-ui/components` | Binary settings | Prefer over custom toggles for on/off state. |
111
+ | `Tabs` | `@mrmeg/expo-ui/components` | In-page tabbed views | Use for profile sections, report views, and settings categories. |
112
+ | `TextInput` | `@mrmeg/expo-ui/components` | Text entry | Use built-in label and error text support. |
113
+ | `Toggle` | `@mrmeg/expo-ui/components` | One pressed/unpressed control | Use `Button` for commands and `Switch` for settings. |
114
+ | `ToggleGroup` | `@mrmeg/expo-ui/components` | Related pressed states | Use for segmented controls, formatting, and filter chips. |
115
+ | `Tooltip` | `@mrmeg/expo-ui/components` | Short hover/focus help | Requires root `UIProvider` portal setup. |
116
+
117
+ ## Selection Rules
118
+
119
+ Use `Button` for commands, `Toggle` for one pressed state, `ToggleGroup` for
120
+ related pressed states, and `Switch` for binary settings.
121
+
122
+ Use `RadioGroup` for small mutually exclusive choices and `Select` for longer
123
+ option sets.
124
+
125
+ Use `Dialog` for blocking decisions, `Popover` for contextual controls,
126
+ `Tooltip` for short explanations, and `DropdownMenu` for action lists.
127
+
128
+ Use `Card` for individual repeated or framed items, not as a wrapper around
129
+ full page sections. Use `EmptyState` for no-data or recoverable error regions,
130
+ `Skeleton` for loading content with stable layout, and `Progress` for real
131
+ progress or indeterminate long-running work.
132
+
133
+ ## Validation
134
+
135
+ Run the UI package gates when changing package code or shipped docs:
136
+
137
+ ```sh
138
+ bun run ui:typecheck
139
+ bun run ui:test
140
+ bun run ui:build
141
+ bun run ui:pack
142
+ bun run ui:consumer-smoke
143
+ ```
144
+
145
+ For documentation-only package surface changes, `bun run ui:pack` is the
146
+ minimum check that proves the new docs are included in the npm tarball.
package/llms.txt ADDED
@@ -0,0 +1,34 @@
1
+ # @mrmeg/expo-ui
2
+
3
+ Docs for agents:
4
+
5
+ - README.md: install, setup, imports, theme usage, components, publishing.
6
+ - LLM_USAGE.md: concise component-selection and implementation rules.
7
+ - llms-full.md: expanded package contract and component catalog.
8
+
9
+ Public entrypoints:
10
+
11
+ - @mrmeg/expo-ui
12
+ - @mrmeg/expo-ui/components
13
+ - @mrmeg/expo-ui/components/*
14
+ - @mrmeg/expo-ui/constants
15
+ - @mrmeg/expo-ui/constants/*
16
+ - @mrmeg/expo-ui/hooks
17
+ - @mrmeg/expo-ui/hooks/*
18
+ - @mrmeg/expo-ui/state
19
+ - @mrmeg/expo-ui/lib
20
+
21
+ Do not recreate primitives this package already provides. Import reusable UI
22
+ from the package and compose it in the app.
23
+
24
+ Call useResources() once near the Expo app root. Mount UIProvider once near the
25
+ root before using package notifications or overlay primitives: Dialog,
26
+ AlertDialog, BottomSheet, Drawer, DropdownMenu, Popover, SelectContent, or
27
+ Tooltip.
28
+
29
+ Use useTheme(), useStyles(), semantic tokens, StyledText, and package controls.
30
+ Do not add app-local Appearance or matchMedia listeners for package theme sync.
31
+ Do not import from dist, source checkout paths, or app-local copies.
32
+
33
+ i18n is optional. Plain children and text props work without app i18n. Use
34
+ configureExpoUiI18n() only when the app wants package tx props translated.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mrmeg/expo-ui",
3
- "version": "0.1.7",
3
+ "version": "0.1.9",
4
4
  "private": false,
5
5
  "description": "Reusable Expo and React Native UI primitives for MrMeg projects.",
6
6
  "keywords": [
@@ -32,7 +32,9 @@
32
32
  "dist",
33
33
  "package.json",
34
34
  "README.md",
35
- "LLM_USAGE.md"
35
+ "LLM_USAGE.md",
36
+ "llms.txt",
37
+ "llms-full.md"
36
38
  ],
37
39
  "exports": {
38
40
  ".": {