@mrmeg/expo-ui 0.1.5 → 0.1.6

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
@@ -86,6 +86,8 @@ configureExpoUiI18n((key, options) => i18n.t(key, options));
86
86
  - Use `useTheme()` and semantic tokens instead of hardcoded colors.
87
87
  - Use `StyledText` or its semantic aliases instead of raw `Text` for app UI.
88
88
  - Use `Button.preset`, not `variant`, for buttons.
89
+ - Button visible heights are compact: `sm` 28px, `md` 32px, and `lg` 40px.
90
+ - Use `Button size="sm"` for compact popover, tooltip, and toolbar triggers; nested `StyledText` inherits the selected Button size.
89
91
  - Use `globalUIStore` plus root-mounted `UIProvider` for transient global feedback.
90
92
  - Keep app monitoring, auth, API, and domain behavior outside this package.
91
93
 
@@ -95,7 +97,10 @@ Useful theme tokens include:
95
97
  theme.colors.background;
96
98
  theme.colors.foreground;
97
99
  theme.colors.card;
100
+ theme.colors.popover;
98
101
  theme.colors.border;
102
+ theme.colors.input;
103
+ theme.colors.ring;
99
104
  theme.colors.primary;
100
105
  theme.colors.secondary;
101
106
  theme.colors.accent;
@@ -110,6 +115,14 @@ Token intent:
110
115
  - `primary`: neutral action color
111
116
  - `secondary`: neutral secondary surface
112
117
  - `accent`: teal highlight color
118
+ - `input`: form-control border color
119
+ - `ring`: focus outline color
120
+ - `popover`: elevated overlay surface
121
+
122
+ Use `getShadowStyle()` for package surfaces that need elevation and
123
+ `getFocusRingStyle()` for web focus styling. Keep web controls compact, but
124
+ preserve mobile tap comfort with package controls that already provide native
125
+ hit slop or 44px touch rows.
113
126
 
114
127
  ## Component Use-Case Index
115
128
 
package/README.md CHANGED
@@ -24,15 +24,15 @@ Install from npm after publishing:
24
24
  bun add @mrmeg/expo-ui
25
25
  ```
26
26
 
27
- Consumers must also install the peer dependencies listed in `package.json`.
28
- The tested baseline is Expo SDK 55 with React 19.2, React Native 0.83,
29
- React Native Web 0.21, Reanimated 4.2, Worklets 0.7, and
30
- `@rn-primitives/*` 1.4. `@rn-primitives/portal` is package-managed because
31
- `UIProvider` mounts the portal host used by package overlays. i18n setup is
32
- optional; plain text and children render without `i18next` or
33
- `react-i18next`. Start consumer apps from the same Expo SDK family or update
34
- the package and peer ranges deliberately. Keep npm auth tokens in developer or
35
- CI configuration, not in this repository.
27
+ Consumers must also install the native and Expo peer dependencies listed in
28
+ `package.json`. The tested baseline is Expo SDK 55 with React 19.2, React
29
+ Native 0.83, React Native Web 0.21, Reanimated 4.2, and Worklets 0.7.
30
+ `@rn-primitives/*` packages are managed by `@mrmeg/expo-ui` because they are
31
+ implementation details of the exported UI components. i18n setup is optional;
32
+ plain text and children render without `i18next` or `react-i18next`. Start
33
+ consumer apps from the same Expo SDK family or update the package and peer
34
+ ranges deliberately. Keep npm auth tokens in developer or CI configuration,
35
+ not in this repository.
36
36
 
37
37
  ## Imports
38
38
 
@@ -95,7 +95,7 @@ const styles = StyleSheet.create({
95
95
  });
96
96
  ```
97
97
 
98
- `useTheme()` returns the active `theme`, resolved `scheme`, persisted `currentTheme`, `setTheme`, `toggleTheme`, shadow helpers, contrast helpers, and `withAlpha`. Use semantic tokens such as `theme.colors.background`, `foreground`, `card`, `border`, `primary`, `secondary`, `accent`, `mutedForeground`, `destructive`, `success`, and `warning`. `primary` is the neutral action color, `secondary` is a neutral secondary surface, and `accent` is the teal highlight color.
98
+ `useTheme()` returns the active `theme`, resolved `scheme`, persisted `currentTheme`, `setTheme`, `toggleTheme`, cross-platform shadow helpers, a web focus-ring helper, contrast helpers, and `withAlpha`. 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
99
 
100
100
  Use `StyledText` for theme-aware text:
101
101
 
@@ -115,6 +115,9 @@ Useful `StyledText` props:
115
115
  - `fontWeight`: `light`, `regular`, `medium`, `semibold`, `bold`
116
116
  - `variant`: `sansSerif`, `serif`
117
117
  - `align`, `tx`, `txOptions`
118
+ - `selectable`: defaults to `true` for readable copy; package controls disable
119
+ selection for labels and other interactive chrome where accidental drag
120
+ selection would feel broken.
118
121
 
119
122
  `tx` support is opt-in. Without a configured translator, `tx` renders its
120
123
  fallback text when provided and otherwise renders the key; package-owned
@@ -141,7 +144,7 @@ All components are exported from `@mrmeg/expo-ui/components`; direct imports suc
141
144
  | `AnimatedView` | Entrance and visibility animation | Hand-rolled Reanimated wrappers | Staggered list rows, revealed panels, animated empty states |
142
145
  | `Badge` | Short status labels | Custom pill `View` + `Text` | Draft/active states, counts, plan labels, role tags |
143
146
  | `BottomSheet` | Mobile-first modal sheets | Custom absolute-position sheets | Action pickers, mobile filters, quick edit forms, contextual details |
144
- | `Button` | Commands and CTAs | Pressable plus custom text styling | Submit, save, cancel, delete, navigation CTAs, icon-accessory buttons |
147
+ | `Button` | Commands and CTAs | Pressable plus custom text styling | Submit, save, cancel, delete, navigation CTAs, icon-accessory buttons; loading state preserves resting width |
145
148
  | `Card`, `CardHeader`, `CardTitle`, `CardDescription`, `CardContent`, `CardFooter` | Framed content groups | Ad hoc bordered panels | List items, pricing plans, settings sections, summaries, dashboards |
146
149
  | `Checkbox` | Boolean selection | Custom checkmark controls | Terms consent, checklist items, multi-select filters, notification opt-ins |
147
150
  | `Collapsible`, `CollapsibleTrigger`, `CollapsibleContent` | One-off disclosure | Local animated height wrappers | Advanced settings, hidden helper text, optional details |
@@ -159,13 +162,13 @@ All components are exported from `@mrmeg/expo-ui/components`; direct imports suc
159
162
  | `Popover` | Anchored contextual content | Custom anchored views | Inline help, quick previews, contextual controls, small forms |
160
163
  | `Progress` | Determinate or indeterminate progress | Layout-shifting spinners for progress regions | Upload progress, onboarding completion, long-running task state |
161
164
  | `RadioGroup`, `RadioGroupItem` | Mutually exclusive choices | Custom radio rows | Plan interval, visibility choice, survey answer, preference setting |
162
- | `Select` | Option menus | Custom dropdowns | Country picker, category selector, status selector, compact form choice |
165
+ | `Select` | Option menus | Custom dropdowns | Country picker, category selector, status selector, compact form choice; `label` drives default item text and overlay text uses popover foreground tokens |
163
166
  | `Separator` | Horizontal or vertical dividers | Border-only spacer views | Menu dividers, section dividers, card dividers |
164
167
  | `Skeleton`, `SkeletonText`, `SkeletonAvatar`, `SkeletonCard` | Loading placeholders | Blank space or generic spinners | List loading, profile card loading, dashboard placeholders |
165
- | `Slider` | Numeric value selection | Custom pan gesture track | Volume, percentage, rating, threshold, range-like settings |
168
+ | `Slider` | Numeric value selection | Custom pan gesture track | Volume, percentage, rating, threshold, range-like settings; accent active track and thumb affordance |
166
169
  | `StatusBar` | Theme-aware native status bar | Per-screen status-bar duplication | Root layout status styling, dark/light mode updates |
167
170
  | `StyledText` and text aliases | Theme-aware typography | Raw `Text` with hardcoded styles | Titles, headings, labels, body copy, captions, translated text |
168
- | `Switch` | Binary settings | Custom toggle switches | Enable notifications, privacy setting, feature toggles |
171
+ | `Switch` | Binary settings | Custom toggle switches | Enable notifications, privacy setting, feature toggles; accent checked track with softened thumb border |
169
172
  | `Tabs`, `TabsList`, `TabsTrigger`, `TabsContent` | In-page tabbed views | Custom segmented/tab controls | Profile sections, report views, settings categories |
170
173
  | `TextInput` | Text entry | Raw `TextInput` with repeated label/error code | Email/password, search, numeric input, multiline notes |
171
174
  | `Toggle`, `ToggleIcon` | Pressed/unpressed control | Button with local selected styling | Favorite, mute, bold/italic, view mode button |
@@ -195,11 +198,11 @@ Most compound components support both direct named imports and dot notation on t
195
198
  | `ToggleGroup` | `ToggleGroupItem`, `ToggleGroupIcon` |
196
199
  | `Tooltip` | `TooltipTrigger`, `TooltipContent`, `TooltipBody` |
197
200
 
198
- Text aliases are exported for common semantic typography: `SerifText`, `SansSerifText`, `SerifBoldText`, `SansSerifBoldText`, `DisplayText`, `TitleText`, `HeadingText`, `SubheadingText`, `BodyText`, `CaptionText`, and `LabelText`. `TextClassContext` and `TextColorContext` are advanced context exports used by package controls to pass nested text styling.
201
+ Text aliases are exported for common semantic typography: `SerifText`, `SansSerifText`, `SerifBoldText`, `SansSerifBoldText`, `DisplayText`, `TitleText`, `HeadingText`, `SubheadingText`, `BodyText`, `CaptionText`, and `LabelText`. `TextClassContext`, `TextColorContext`, `TextStyleContext`, and `TextSelectabilityContext` are advanced context exports used by package controls to pass nested text styling and control-label selectability.
199
202
 
200
203
  ### Common Patterns
201
204
 
202
- Use `Button.preset`, not `variant`. `default` is the neutral primary action, `secondary` is a neutral secondary surface, `outline` is for lower-emphasis actions, `ghost` is for compact toolbars, `link` is for text-like commands, and `destructive` is for dangerous actions.
205
+ Use `Button.preset`, not `variant`. `default` is the neutral primary action, `secondary` is a neutral secondary surface, `outline` is for lower-emphasis actions, `ghost` is for compact toolbars, `link` is for text-like commands, and `destructive` is for dangerous actions. Button visible heights are compact: `sm` 28px, `md` 32px, and `lg` 40px. Native Button targets preserve tap comfort with computed hit slop up to 44px. Nested `StyledText` children inherit the selected Button size, so use `size="sm"` for compact popover, tooltip, and toolbar triggers.
203
206
 
204
207
  Use `StyledText` or its aliases instead of raw `Text` whenever the text is part of app UI. Use `TextInput` for labeled fields because it already owns label, helper text, error text, clear buttons, password visibility, numeric filtering, and left/right elements.
205
208
 
@@ -322,7 +325,7 @@ On web, `useResources()` injects the Google Fonts Lato stylesheet after hydratio
322
325
  />
323
326
  ```
324
327
 
325
- On native, the package uses platform sans-serif fallbacks. `useResources()` still loads `Feather.font` from the consumer app's `@expo/vector-icons` peer dependency for icon rendering.
328
+ On native, the package uses platform sans-serif fallbacks. `useResources()` still loads `Feather.font` from the package-managed `@expo/vector-icons` dependency for icon rendering.
326
329
 
327
330
  ## Package Checks
328
331
 
@@ -3,7 +3,7 @@ import { useEffect, useState } from "react";
3
3
  import { Platform, Pressable, View } from "react-native";
4
4
  import Animated, { useSharedValue, useAnimatedStyle, withTiming, useReducedMotion, } from "react-native-reanimated";
5
5
  import { Icon } from "./Icon.js";
6
- import { TextClassContext } from "./StyledText.js";
6
+ import { TextClassContext, TextSelectabilityContext } from "./StyledText.js";
7
7
  import { useTheme } from "../hooks/useTheme.js";
8
8
  import { spacing } from "../constants/spacing.js";
9
9
  import * as AccordionPrimitive from "@rn-primitives/accordion";
@@ -114,21 +114,24 @@ function AccordionTrigger({ children, style: styleOverride, ...props }) {
114
114
  const chevronStyle = useAnimatedStyle(() => ({
115
115
  transform: [{ rotate: `${rotation.value * 180}deg` }],
116
116
  }));
117
- return (_jsx(TextClassContext.Provider, { value: "", children: _jsx(AccordionPrimitive.Header, { children: _jsx(AccordionPrimitive.Trigger, { ...props, asChild: true, children: _jsxs(Trigger, { style: [
118
- {
119
- flexDirection: "row",
120
- alignItems: "center",
121
- justifyContent: "space-between",
122
- gap: spacing.md,
123
- borderRadius: spacing.radiusMd,
124
- paddingVertical: spacing.md,
125
- ...(Platform.OS === "web" && { cursor: "pointer" }),
126
- },
127
- // Spread array styles from primitives to prevent nested arrays on web
128
- ...(styleOverride && typeof styleOverride !== "function"
129
- ? (Array.isArray(styleOverride) ? styleOverride : [styleOverride])
130
- : []),
131
- ], children: [_jsx(_Fragment, { children: children }), _jsx(Animated.View, { style: chevronStyle, children: _jsx(Icon, { name: "chevron-down", size: 16, color: theme.colors.textDim, decorative: true }) })] }) }) }) }));
117
+ return (_jsx(TextClassContext.Provider, { value: "", children: _jsx(TextSelectabilityContext.Provider, { value: false, children: _jsx(AccordionPrimitive.Header, { children: _jsx(AccordionPrimitive.Trigger, { ...props, asChild: true, children: _jsxs(Trigger, { style: [
118
+ {
119
+ flexDirection: "row",
120
+ alignItems: "center",
121
+ justifyContent: "space-between",
122
+ gap: spacing.md,
123
+ borderRadius: spacing.radiusMd,
124
+ paddingVertical: spacing.md,
125
+ ...(Platform.OS === "web" && {
126
+ cursor: "pointer",
127
+ userSelect: "none",
128
+ }),
129
+ },
130
+ // Spread array styles from primitives to prevent nested arrays on web
131
+ ...(styleOverride && typeof styleOverride !== "function"
132
+ ? (Array.isArray(styleOverride) ? styleOverride : [styleOverride])
133
+ : []),
134
+ ], children: [_jsx(_Fragment, { children: children }), _jsx(Animated.View, { style: chevronStyle, children: _jsx(Icon, { name: "chevron-down", size: 16, color: theme.colors.textDim, decorative: true }) })] }) }) }) }) }));
132
135
  }
133
136
  /**
134
137
  * Accordion Content Component
@@ -1,5 +1,5 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- import React from "react";
2
+ import React, { useMemo } from "react";
3
3
  import { View, StyleSheet } from "react-native";
4
4
  import { useTheme } from "../hooks/useTheme.js";
5
5
  import { spacing } from "../constants/spacing.js";
@@ -19,7 +19,7 @@ import { StyledText } from "./StyledText.js";
19
19
  */
20
20
  function Badge({ children, variant = "default", style: styleOverride }) {
21
21
  const { theme } = useTheme();
22
- const styles = createStyles(theme);
22
+ const styles = useMemo(() => createStyles(theme), [theme]);
23
23
  const textStyle = [
24
24
  styles.text,
25
25
  variant === "default" && { color: theme.colors.primaryForeground },
@@ -29,9 +29,9 @@ function Badge({ children, variant = "default", style: styleOverride }) {
29
29
  ];
30
30
  const normalizedChildren = React.Children.toArray(children);
31
31
  const hasOnlyTextChildren = normalizedChildren.every((child) => typeof child === "string" || typeof child === "number");
32
- const content = hasOnlyTextChildren ? (_jsx(StyledText, { style: textStyle, children: normalizedChildren.join("") })) : (React.Children.map(children, (child) => {
32
+ const content = hasOnlyTextChildren ? (_jsx(StyledText, { selectable: false, style: textStyle, children: normalizedChildren.join("") })) : (React.Children.map(children, (child) => {
33
33
  if (typeof child === "string" || typeof child === "number") {
34
- return _jsx(StyledText, { style: textStyle, children: child });
34
+ return _jsx(StyledText, { selectable: false, style: textStyle, children: child });
35
35
  }
36
36
  return child;
37
37
  }));
@@ -69,6 +69,7 @@ const createStyles = (theme) => StyleSheet.create({
69
69
  fontSize: 12,
70
70
  fontWeight: "500",
71
71
  lineHeight: 18,
72
+ userSelect: "none",
72
73
  },
73
74
  });
74
75
  export { Badge };
@@ -1,33 +1,34 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { useState } from "react";
2
+ import { useCallback, useMemo, useState } from "react";
3
3
  import { Pressable, StyleSheet, View, Platform, ActivityIndicator, } from "react-native";
4
4
  import Animated from "react-native-reanimated";
5
5
  import { spacing } from "../constants/spacing.js";
6
- import { StyledText, TextColorContext } from "./StyledText.js";
6
+ import { StyledText, TextColorContext, TextSelectabilityContext, TextStyleContext } from "./StyledText.js";
7
7
  import { fontFamilies } from "../constants/fonts.js";
8
8
  import { palette } from "../constants/colors.js";
9
9
  import { useTheme } from "../hooks/useTheme.js";
10
10
  import { useScalePress } from "../hooks/useScalePress.js";
11
11
  const SIZE_CONFIGS = {
12
12
  sm: {
13
- paddingVertical: spacing.xs,
14
- paddingHorizontal: 12,
13
+ paddingVertical: spacing.xxs,
14
+ paddingHorizontal: 10,
15
15
  fontSize: 12,
16
- height: 32,
16
+ height: 28,
17
17
  },
18
18
  md: {
19
- paddingVertical: spacing.sm,
20
- paddingHorizontal: spacing.md,
19
+ paddingVertical: spacing.xs,
20
+ paddingHorizontal: 12,
21
21
  fontSize: 14,
22
- height: 36,
22
+ height: 32,
23
23
  },
24
24
  lg: {
25
- paddingVertical: 10,
26
- paddingHorizontal: spacing.lg,
27
- fontSize: 16,
28
- height: 44,
25
+ paddingVertical: 6,
26
+ paddingHorizontal: spacing.md,
27
+ fontSize: 15,
28
+ height: 40,
29
29
  },
30
30
  };
31
+ const getNativeHitSlop = (sizeConfig) => Math.ceil(Math.max(0, spacing.touchTarget - sizeConfig.height) / 2);
31
32
  /**
32
33
  * Enhanced Button Component
33
34
  *
@@ -65,10 +66,11 @@ const SIZE_CONFIGS = {
65
66
  */
66
67
  export function Button(props) {
67
68
  const { tx, text, txOptions, style: styleOverride, pressedStyle: pressedStyleOverride, textStyle: textStyleOverride, pressedTextStyle: pressedTextStyleOverride, disabledTextStyle: disabledTextStyleOverride, children, RightAccessory, LeftAccessory, disabled, disabledStyle: disabledStyleOverride, withShadow = false, preset = "default", size = "md", loading = false, fullWidth = false, onFocus, onBlur, onPressIn, onPressOut, ...rest } = props;
68
- const { theme, getContrastingColor, getShadowStyle } = useTheme();
69
- const styles = createStyles(theme, size);
69
+ const { theme, getContrastingColor, getFocusRingStyle, getShadowStyle } = useTheme();
70
+ const styles = useMemo(() => createStyles(theme, size), [theme, size]);
70
71
  const shadowStyle = getShadowStyle("base");
71
72
  const sizeConfig = SIZE_CONFIGS[size];
73
+ const focusRingStyle = getFocusRingStyle();
72
74
  // Pre-compute background color for contrast calculation
73
75
  // Always flatten to handle both array styles (from Slot) and RegisteredStyle
74
76
  const flattenedStyle = styleOverride ? StyleSheet.flatten(styleOverride) : undefined;
@@ -106,6 +108,7 @@ export function Button(props) {
106
108
  ? theme.colors.secondaryForeground
107
109
  : getContrastingColor(backgroundColor, palette.white, palette.black);
108
110
  const [focused, setFocused] = useState(false);
111
+ const [restingWidth, setRestingWidth] = useState();
109
112
  const isDisabled = disabled || loading;
110
113
  const { animatedStyle: scaleStyle, pressHandlers } = useScalePress({
111
114
  disabled: !!isDisabled,
@@ -128,45 +131,60 @@ export function Button(props) {
128
131
  pressHandlers.onPressOut();
129
132
  onPressOut?.(event);
130
133
  };
131
- return (_jsx(TextColorContext.Provider, { value: textColor, children: _jsx(Pressable, { accessibilityRole: "button", accessibilityState: { disabled: !!isDisabled, busy: loading }, ...rest, onPressIn: handlePressIn, onPressOut: handlePressOut, onFocus: handleFocus, onBlur: handleBlur, style: { alignSelf: fullWidth ? "stretch" : flattenedStyle?.alignSelf ?? "flex-start" }, disabled: isDisabled, children: (state) => (_jsx(Animated.View, { style: scaleStyle, children: _jsxs(View, { style: [
132
- styles.button,
133
- preset === "default" && styles.buttonDefault,
134
- preset === "outline" && styles.buttonOutline,
135
- preset === "ghost" && styles.buttonGhost,
136
- preset === "link" && styles.buttonLink,
137
- preset === "destructive" && styles.buttonDestructive,
138
- preset === "secondary" && styles.buttonSecondary,
139
- fullWidth && styles.fullWidth,
140
- withShadow && !isDisabled && shadowStyle,
141
- state.pressed && styles.pressed,
142
- state.pressed && pressedStyleOverride,
143
- isDisabled && styles.disabled,
144
- isDisabled && disabledStyleOverride,
145
- Platform.OS === "web" && focused && !isDisabled && {
146
- boxShadow: `0 0 0 2px ${theme.colors.background}, 0 0 0 4px ${theme.colors.accent}`,
147
- },
148
- // Spread array styles from Slot to prevent nested arrays on web
149
- ...(Array.isArray(styleOverride) ? styleOverride : [styleOverride]),
150
- ], children: [!!LeftAccessory && !loading && (_jsx(LeftAccessory, { style: styles.leftAccessory, pressableState: state, disabled: isDisabled })), loading && (_jsx(ActivityIndicator, { size: "small", color: textColor, style: styles.loader })), (tx || text) ? (_jsx(StyledText, { tx: tx, text: text, txOptions: txOptions, style: [
151
- styles.text,
152
- state.pressed && styles.pressedText,
153
- state.pressed && pressedTextStyleOverride,
154
- isDisabled && disabledTextStyleOverride,
155
- textStyleOverride,
156
- ] })) : !loading && children ? (
157
- // Wrap string children in StyledText to apply TextColorContext
158
- typeof children === "string" ? (_jsx(StyledText, { style: [
159
- styles.text,
160
- state.pressed && styles.pressedText,
161
- state.pressed && pressedTextStyleOverride,
162
- isDisabled && disabledTextStyleOverride,
163
- textStyleOverride,
164
- ], children: children })) : (children)) : null, !!RightAccessory && !loading && (_jsx(RightAccessory, { style: styles.rightAccessory, pressableState: state, disabled: isDisabled }))] }) })) }) }));
134
+ const handleButtonLayout = useCallback((event) => {
135
+ if (loading || fullWidth)
136
+ return;
137
+ const nextWidth = event.nativeEvent.layout.width;
138
+ if (nextWidth <= 0)
139
+ return;
140
+ setRestingWidth((currentWidth) => {
141
+ if (currentWidth !== undefined && Math.abs(currentWidth - nextWidth) < 0.5) {
142
+ return currentWidth;
143
+ }
144
+ return nextWidth;
145
+ });
146
+ }, [fullWidth, loading]);
147
+ return (_jsx(TextColorContext.Provider, { value: textColor, children: _jsx(TextSelectabilityContext.Provider, { value: false, children: _jsx(TextStyleContext.Provider, { value: styles.text, children: _jsx(Pressable, { accessibilityRole: "button", accessibilityState: { disabled: !!isDisabled, busy: loading }, ...rest, onPressIn: handlePressIn, onPressOut: handlePressOut, onFocus: handleFocus, onBlur: handleBlur, style: { alignSelf: fullWidth ? "stretch" : flattenedStyle?.alignSelf ?? "flex-start" }, hitSlop: rest.hitSlop ?? (Platform.OS === "web" ? undefined : getNativeHitSlop(sizeConfig)), disabled: isDisabled, children: (state) => (_jsx(Animated.View, { style: scaleStyle, children: _jsxs(View, { style: [
148
+ styles.button,
149
+ preset === "default" && styles.buttonDefault,
150
+ preset === "outline" && styles.buttonOutline,
151
+ preset === "ghost" && styles.buttonGhost,
152
+ preset === "link" && styles.buttonLink,
153
+ preset === "destructive" && styles.buttonDestructive,
154
+ preset === "secondary" && styles.buttonSecondary,
155
+ fullWidth && styles.fullWidth,
156
+ withShadow && !isDisabled && shadowStyle,
157
+ state.pressed && preset === "outline" && styles.pressedMuted,
158
+ state.pressed && preset === "ghost" && styles.pressedMuted,
159
+ state.pressed && styles.pressed,
160
+ state.pressed && pressedStyleOverride,
161
+ isDisabled && styles.disabled,
162
+ isDisabled && disabledStyleOverride,
163
+ focused && !isDisabled && focusRingStyle,
164
+ loading && restingWidth !== undefined && !fullWidth && { width: restingWidth },
165
+ // Spread array styles from Slot to prevent nested arrays on web
166
+ ...(Array.isArray(styleOverride) ? styleOverride : [styleOverride]),
167
+ ], onLayout: handleButtonLayout, children: [loading && (_jsx(View, { style: styles.loaderOverlay, pointerEvents: "none", children: _jsx(ActivityIndicator, { size: "small", color: textColor }) })), _jsxs(View, { style: [styles.content, loading && styles.loadingContent], pointerEvents: loading ? "none" : "auto", children: [!!LeftAccessory && (_jsx(LeftAccessory, { style: styles.leftAccessory, pressableState: state, disabled: isDisabled })), (tx || text) ? (_jsx(StyledText, { tx: tx, text: text, txOptions: txOptions, style: [
168
+ styles.text,
169
+ state.pressed && styles.pressedText,
170
+ state.pressed && pressedTextStyleOverride,
171
+ isDisabled && disabledTextStyleOverride,
172
+ textStyleOverride,
173
+ ] })) : children ? (
174
+ // Wrap string children in StyledText to apply control typography.
175
+ typeof children === "string" ? (_jsx(StyledText, { style: [
176
+ styles.text,
177
+ state.pressed && styles.pressedText,
178
+ state.pressed && pressedTextStyleOverride,
179
+ isDisabled && disabledTextStyleOverride,
180
+ textStyleOverride,
181
+ ], children: children })) : (children)) : (null), !!RightAccessory && (_jsx(RightAccessory, { style: styles.rightAccessory, pressableState: state, disabled: isDisabled }))] })] }) })) }) }) }) }));
165
182
  }
166
183
  const createStyles = (theme, size) => {
167
184
  const sizeConfig = SIZE_CONFIGS[size];
168
185
  return StyleSheet.create({
169
186
  button: {
187
+ position: "relative",
170
188
  flexDirection: "row",
171
189
  alignItems: "center",
172
190
  justifyContent: "center",
@@ -177,6 +195,15 @@ const createStyles = (theme, size) => {
177
195
  flexShrink: 0,
178
196
  ...(Platform.OS === "web" && { cursor: "pointer" }),
179
197
  },
198
+ content: {
199
+ flexDirection: "row",
200
+ alignItems: "center",
201
+ justifyContent: "center",
202
+ flexShrink: 0,
203
+ },
204
+ loadingContent: {
205
+ opacity: 0,
206
+ },
180
207
  buttonDefault: {
181
208
  backgroundColor: theme.colors.primary,
182
209
  },
@@ -189,7 +216,7 @@ const createStyles = (theme, size) => {
189
216
  buttonOutline: {
190
217
  backgroundColor: "transparent",
191
218
  borderWidth: 1,
192
- borderColor: theme.colors.border,
219
+ borderColor: theme.colors.input,
193
220
  },
194
221
  buttonGhost: {
195
222
  backgroundColor: "transparent",
@@ -209,10 +236,14 @@ const createStyles = (theme, size) => {
209
236
  textAlign: "center",
210
237
  lineHeight: sizeConfig.fontSize * 1.4,
211
238
  flexShrink: 0,
239
+ userSelect: "none",
212
240
  },
213
241
  pressed: {
214
242
  opacity: 0.9,
215
243
  },
244
+ pressedMuted: {
245
+ backgroundColor: theme.colors.muted,
246
+ },
216
247
  pressedText: {
217
248
  opacity: 0.9,
218
249
  },
@@ -225,8 +256,10 @@ const createStyles = (theme, size) => {
225
256
  rightAccessory: {
226
257
  marginLeft: spacing.sm,
227
258
  },
228
- loader: {
229
- marginRight: spacing.sm,
259
+ loaderOverlay: {
260
+ ...StyleSheet.absoluteFillObject,
261
+ alignItems: "center",
262
+ justifyContent: "center",
230
263
  },
231
264
  });
232
265
  };
@@ -39,8 +39,9 @@ function useCardContext() {
39
39
  return ctx;
40
40
  }
41
41
  function Card({ children, style: styleOverride, variant = "default", onPress, disabled }) {
42
- const { theme } = useTheme();
42
+ const { theme, getShadowStyle } = useTheme();
43
43
  const styles = createCardStyles(theme);
44
+ const shadowStyle = getShadowStyle("subtle");
44
45
  const ctx = { theme, styles };
45
46
  const { animatedStyle: scaleStyle, pressHandlers } = useScalePress({
46
47
  disabled: !onPress || !!disabled,
@@ -50,6 +51,7 @@ function Card({ children, style: styleOverride, variant = "default", onPress, di
50
51
  const cardContent = (_jsx(View, { style: [
51
52
  styles.card,
52
53
  variant === "default" && styles.cardDefault,
54
+ variant === "default" && shadowStyle,
53
55
  variant === "outline" && styles.cardOutline,
54
56
  variant === "ghost" && styles.cardGhost,
55
57
  styleOverride,
@@ -82,7 +84,6 @@ function CardDescription({ children, style: styleOverride, ...props }) {
82
84
  const createCardStyles = (theme) => StyleSheet.create({
83
85
  card: {
84
86
  borderRadius: spacing.radiusLg,
85
- overflow: "hidden",
86
87
  },
87
88
  cardDefault: {
88
89
  backgroundColor: theme.colors.card,
@@ -116,7 +117,7 @@ const createCardStyles = (theme) => StyleSheet.create({
116
117
  title: {
117
118
  fontSize: 18,
118
119
  lineHeight: 24,
119
- letterSpacing: -0.3,
120
+ letterSpacing: 0,
120
121
  },
121
122
  description: {
122
123
  fontSize: 14,
@@ -1,5 +1,5 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { View, StyleSheet, Pressable } from "react-native";
2
+ import { View, StyleSheet, Pressable, Platform } from "react-native";
3
3
  import Animated, { useSharedValue, useAnimatedStyle, withTiming, useReducedMotion, } from "react-native-reanimated";
4
4
  import { Icon } from "./Icon.js";
5
5
  import { StyledText } from "./StyledText.js";
@@ -43,7 +43,7 @@ function Checkbox({ size = "md", label, indeterminate = false, error = false, st
43
43
  const flattenedStyle = styleOverride ? StyleSheet.flatten(styleOverride) : undefined;
44
44
  const checkboxElement = (_jsx(CheckboxPrimitive.Root, { ...props, checked: checked, onCheckedChange: wrappedOnCheckedChange, disabled: disabled, style: {
45
45
  borderColor,
46
- backgroundColor: checked || indeterminate ? theme.colors.primary : "transparent",
46
+ backgroundColor: checked || indeterminate ? theme.colors.primary : theme.colors.background,
47
47
  borderRadius: spacing.radiusSm,
48
48
  borderWidth: 1,
49
49
  width: sizeConfig.size,
@@ -51,6 +51,7 @@ function Checkbox({ size = "md", label, indeterminate = false, error = false, st
51
51
  justifyContent: "center",
52
52
  alignItems: "center",
53
53
  opacity: disabled ? 0.5 : 1,
54
+ ...(Platform.OS === "web" && { cursor: disabled ? "not-allowed" : "pointer" }),
54
55
  ...(flattenedStyle || {}),
55
56
  }, hitSlop: DEFAULT_HIT_SLOP, accessibilityRole: "checkbox", accessibilityState: {
56
57
  checked: indeterminate ? "mixed" : checked,
@@ -67,18 +68,19 @@ function Checkbox({ size = "md", label, indeterminate = false, error = false, st
67
68
  return (_jsxs(Pressable, { onPress: () => !disabled && wrappedOnCheckedChange(!checked), style: [styles.container, labelStyle], disabled: disabled, accessibilityRole: "checkbox", accessibilityState: {
68
69
  checked: indeterminate ? "mixed" : checked,
69
70
  disabled: !!disabled,
70
- }, accessibilityLabel: label, children: [checkboxElement, _jsx(View, { style: styles.labelContainer, children: _jsxs(StyledText, { style: [
71
+ }, accessibilityLabel: label, children: [checkboxElement, _jsx(View, { style: styles.labelContainer, children: _jsxs(StyledText, { selectable: false, style: [
71
72
  styles.label,
72
73
  { color: theme.colors.text },
73
74
  disabled && styles.disabledLabel,
74
75
  error && { color: theme.colors.destructive },
75
- ], children: [label, required && (_jsx(StyledText, { style: [styles.required, { color: theme.colors.destructive }], children: " *" }))] }) })] }));
76
+ ], children: [label, required && (_jsx(StyledText, { selectable: false, style: [styles.required, { color: theme.colors.destructive }], children: " *" }))] }) })] }));
76
77
  }
77
78
  const styles = StyleSheet.create({
78
79
  container: {
79
80
  flexDirection: "row",
80
81
  alignItems: "center",
81
82
  gap: spacing.sm,
83
+ minHeight: spacing.touchTarget,
82
84
  },
83
85
  labelContainer: {
84
86
  flex: 1,
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import * as React from "react";
3
3
  import { Animated, Platform, StyleSheet, View } from "react-native";
4
- import { TextClassContext } from "./StyledText.js";
4
+ import { TextClassContext, TextSelectabilityContext } from "./StyledText.js";
5
5
  import { spacing } from "../constants/spacing.js";
6
6
  import { useTheme } from "../hooks/useTheme.js";
7
7
  import * as CollapsiblePrimitive from "@rn-primitives/collapsible";
@@ -10,19 +10,20 @@ function Collapsible({ children, ...props }) {
10
10
  }
11
11
  function CollapsibleTrigger({ style: styleOverride, ...props }) {
12
12
  const { theme } = useTheme();
13
- return (_jsx(TextClassContext.Provider, { value: "", children: _jsx(CollapsiblePrimitive.Trigger, { ...props, style: {
14
- flexDirection: "row",
15
- alignItems: "center",
16
- justifyContent: "space-between",
17
- paddingVertical: spacing.sm,
18
- ...(Platform.OS === "web" && {
19
- cursor: "pointer",
20
- outlineStyle: "none",
21
- }),
22
- ...(styleOverride && typeof styleOverride !== "function"
23
- ? StyleSheet.flatten(styleOverride)
24
- : {}),
25
- } }) }));
13
+ return (_jsx(TextClassContext.Provider, { value: "", children: _jsx(TextSelectabilityContext.Provider, { value: false, children: _jsx(CollapsiblePrimitive.Trigger, { ...props, style: {
14
+ flexDirection: "row",
15
+ alignItems: "center",
16
+ justifyContent: "space-between",
17
+ paddingVertical: spacing.sm,
18
+ ...(Platform.OS === "web" && {
19
+ cursor: "pointer",
20
+ outlineStyle: "none",
21
+ userSelect: "none",
22
+ }),
23
+ ...(styleOverride && typeof styleOverride !== "function"
24
+ ? StyleSheet.flatten(styleOverride)
25
+ : {}),
26
+ } }) }) }));
26
27
  }
27
28
  function CollapsibleContent({ forceMount, style: styleOverride, children, ...props }) {
28
29
  const { theme } = useTheme();
@@ -21,14 +21,14 @@ function DialogRoot({ children, ...props }) {
21
21
  }
22
22
  function DialogContent({ portalHost, style, children, ...props }) {
23
23
  const { theme, getShadowStyle, getContrastingColor } = useTheme();
24
- const textColor = getContrastingColor(theme.colors.background, palette.white, palette.black);
24
+ const textColor = getContrastingColor(theme.colors.popover, palette.white, palette.black);
25
25
  return (_jsx(DialogPrimitive.Portal, { hostName: portalHost, children: _jsx(FullWindowOverlay, { children: _jsx(DialogPrimitive.Overlay, { style: StyleSheet.flatten([
26
26
  StyleSheet.absoluteFill,
27
27
  { backgroundColor: theme.colors.overlay },
28
28
  Platform.OS === "web" && { zIndex: 50 },
29
29
  ]), children: _jsx(AnimatedView, { type: "fade", enterDuration: 200, children: _jsx(View, { style: overlayStyles.centeredContainer, children: _jsx(AnimatedView, { type: "scale", enterDuration: 250, children: _jsx(TextColorContext.Provider, { value: textColor, children: _jsx(TextClassContext.Provider, { value: "", children: _jsx(DialogPrimitive.Content, { style: StyleSheet.flatten([
30
30
  {
31
- backgroundColor: theme.colors.background,
31
+ backgroundColor: theme.colors.popover,
32
32
  borderColor: theme.colors.border,
33
33
  borderWidth: 1,
34
34
  borderRadius: spacing.radiusLg,
@@ -61,7 +61,7 @@ function DialogTitle({ children, style, ...props }) {
61
61
  {
62
62
  fontSize: 18,
63
63
  lineHeight: 24,
64
- letterSpacing: -0.3,
64
+ letterSpacing: 0,
65
65
  color: theme.colors.text,
66
66
  },
67
67
  style,
@@ -98,14 +98,14 @@ function AlertDialogRoot({ children, ...props }) {
98
98
  }
99
99
  function AlertDialogContent({ portalHost, style, children, ...props }) {
100
100
  const { theme, getShadowStyle, getContrastingColor } = useTheme();
101
- const textColor = getContrastingColor(theme.colors.background, palette.white, palette.black);
101
+ const textColor = getContrastingColor(theme.colors.popover, palette.white, palette.black);
102
102
  return (_jsx(AlertDialogPrimitive.Portal, { hostName: portalHost, children: _jsx(FullWindowOverlay, { children: _jsx(AlertDialogPrimitive.Overlay, { style: StyleSheet.flatten([
103
103
  StyleSheet.absoluteFill,
104
104
  { backgroundColor: theme.colors.overlay },
105
105
  Platform.OS === "web" && { zIndex: 52 },
106
106
  ]), children: _jsx(AnimatedView, { type: "fade", enterDuration: 200, children: _jsx(View, { style: overlayStyles.centeredContainer, children: _jsx(AnimatedView, { type: "scale", enterDuration: 250, children: _jsx(TextColorContext.Provider, { value: textColor, children: _jsx(TextClassContext.Provider, { value: "", children: _jsx(AlertDialogPrimitive.Content, { style: StyleSheet.flatten([
107
107
  {
108
- backgroundColor: theme.colors.background,
108
+ backgroundColor: theme.colors.popover,
109
109
  borderColor: theme.colors.border,
110
110
  borderWidth: 1,
111
111
  borderRadius: spacing.radiusLg,
@@ -124,7 +124,7 @@ function AlertDialogTitle({ children, style, ...props }) {
124
124
  {
125
125
  fontSize: 18,
126
126
  lineHeight: 24,
127
- letterSpacing: -0.3,
127
+ letterSpacing: 0,
128
128
  color: theme.colors.text,
129
129
  },
130
130
  style,