@mrmeg/expo-ui 0.1.5 → 0.1.7
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 +28 -0
- package/README.md +34 -17
- package/dist/components/Accordion.js +19 -16
- package/dist/components/Badge.js +5 -4
- package/dist/components/Button.js +84 -51
- package/dist/components/Card.js +4 -3
- package/dist/components/Checkbox.js +6 -4
- package/dist/components/Collapsible.js +15 -14
- package/dist/components/Dialog.js +6 -6
- package/dist/components/Drawer.js +5 -5
- package/dist/components/DropdownMenu.js +119 -112
- package/dist/components/EmptyState.js +5 -3
- package/dist/components/InputOTP.js +3 -3
- package/dist/components/Label.js +5 -2
- package/dist/components/Notification.js +3 -3
- package/dist/components/Popover.js +2 -2
- package/dist/components/RadioGroup.js +6 -4
- package/dist/components/Select.js +35 -25
- package/dist/components/Slider.js +34 -24
- package/dist/components/StyledText.d.ts +13 -2
- package/dist/components/StyledText.js +28 -7
- package/dist/components/Switch.js +28 -28
- package/dist/components/Tabs.js +6 -3
- package/dist/components/TextInput.js +8 -10
- package/dist/components/Toggle.js +4 -2
- package/dist/components/ToggleGroup.js +3 -2
- package/dist/components/Tooltip.js +4 -4
- package/dist/constants/colors.d.ts +4 -0
- package/dist/constants/colors.js +9 -1
- package/dist/constants/spacing.d.ts +2 -1
- package/dist/constants/spacing.js +2 -1
- package/dist/hooks/useTheme.d.ts +9 -6
- package/dist/hooks/useTheme.js +99 -22
- package/package.json +6 -7
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,29 @@ 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. It supports
|
|
123
|
+
`base`, `soft`, `sharp`, `subtle`, `elevated`, `glow`, `glass`, `card`,
|
|
124
|
+
`cardHover`, and `cardSubtle`, returning native shadow/elevation off web and
|
|
125
|
+
CSS `boxShadow` on web. Use `getFocusRingStyle()` for web focus styling. Keep
|
|
126
|
+
web controls compact, but preserve mobile tap comfort with package controls
|
|
127
|
+
that already provide native hit slop or 44px touch rows.
|
|
128
|
+
|
|
129
|
+
Use `useStyles()` for memoized theme-aware local styles. Its factory receives
|
|
130
|
+
`{ theme, spacing, withAlpha }`, so components can derive alpha-adjusted
|
|
131
|
+
semantic colors without destructuring `withAlpha` outside the factory:
|
|
132
|
+
|
|
133
|
+
```tsx
|
|
134
|
+
const { styles } = useStyles(({ theme, spacing, withAlpha }) => ({
|
|
135
|
+
card: {
|
|
136
|
+
backgroundColor: withAlpha(theme.colors.primary, 0.08),
|
|
137
|
+
padding: spacing.md,
|
|
138
|
+
},
|
|
139
|
+
}));
|
|
140
|
+
```
|
|
113
141
|
|
|
114
142
|
## Component Use-Case Index
|
|
115
143
|
|
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
|
|
28
|
-
The tested baseline is Expo SDK 55 with React 19.2, React
|
|
29
|
-
React Native Web 0.21, Reanimated 4.2, Worklets 0.7
|
|
30
|
-
`@rn-primitives/*`
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
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,21 @@ 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,
|
|
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`. `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
|
+
|
|
100
|
+
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
|
+
|
|
102
|
+
```tsx
|
|
103
|
+
import { useStyles } from "@mrmeg/expo-ui/hooks";
|
|
104
|
+
|
|
105
|
+
const { styles } = useStyles(({ theme, spacing, withAlpha }) => ({
|
|
106
|
+
card: {
|
|
107
|
+
backgroundColor: withAlpha(theme.colors.primary, 0.08),
|
|
108
|
+
borderRadius: spacing.radiusMd,
|
|
109
|
+
padding: spacing.md,
|
|
110
|
+
},
|
|
111
|
+
}));
|
|
112
|
+
```
|
|
99
113
|
|
|
100
114
|
Use `StyledText` for theme-aware text:
|
|
101
115
|
|
|
@@ -115,6 +129,9 @@ Useful `StyledText` props:
|
|
|
115
129
|
- `fontWeight`: `light`, `regular`, `medium`, `semibold`, `bold`
|
|
116
130
|
- `variant`: `sansSerif`, `serif`
|
|
117
131
|
- `align`, `tx`, `txOptions`
|
|
132
|
+
- `selectable`: defaults to `true` for readable copy; package controls disable
|
|
133
|
+
selection for labels and other interactive chrome where accidental drag
|
|
134
|
+
selection would feel broken.
|
|
118
135
|
|
|
119
136
|
`tx` support is opt-in. Without a configured translator, `tx` renders its
|
|
120
137
|
fallback text when provided and otherwise renders the key; package-owned
|
|
@@ -141,7 +158,7 @@ All components are exported from `@mrmeg/expo-ui/components`; direct imports suc
|
|
|
141
158
|
| `AnimatedView` | Entrance and visibility animation | Hand-rolled Reanimated wrappers | Staggered list rows, revealed panels, animated empty states |
|
|
142
159
|
| `Badge` | Short status labels | Custom pill `View` + `Text` | Draft/active states, counts, plan labels, role tags |
|
|
143
160
|
| `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 |
|
|
161
|
+
| `Button` | Commands and CTAs | Pressable plus custom text styling | Submit, save, cancel, delete, navigation CTAs, icon-accessory buttons; loading state preserves resting width |
|
|
145
162
|
| `Card`, `CardHeader`, `CardTitle`, `CardDescription`, `CardContent`, `CardFooter` | Framed content groups | Ad hoc bordered panels | List items, pricing plans, settings sections, summaries, dashboards |
|
|
146
163
|
| `Checkbox` | Boolean selection | Custom checkmark controls | Terms consent, checklist items, multi-select filters, notification opt-ins |
|
|
147
164
|
| `Collapsible`, `CollapsibleTrigger`, `CollapsibleContent` | One-off disclosure | Local animated height wrappers | Advanced settings, hidden helper text, optional details |
|
|
@@ -159,13 +176,13 @@ All components are exported from `@mrmeg/expo-ui/components`; direct imports suc
|
|
|
159
176
|
| `Popover` | Anchored contextual content | Custom anchored views | Inline help, quick previews, contextual controls, small forms |
|
|
160
177
|
| `Progress` | Determinate or indeterminate progress | Layout-shifting spinners for progress regions | Upload progress, onboarding completion, long-running task state |
|
|
161
178
|
| `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 |
|
|
179
|
+
| `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
180
|
| `Separator` | Horizontal or vertical dividers | Border-only spacer views | Menu dividers, section dividers, card dividers |
|
|
164
181
|
| `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 |
|
|
182
|
+
| `Slider` | Numeric value selection | Custom pan gesture track | Volume, percentage, rating, threshold, range-like settings; accent active track and thumb affordance |
|
|
166
183
|
| `StatusBar` | Theme-aware native status bar | Per-screen status-bar duplication | Root layout status styling, dark/light mode updates |
|
|
167
184
|
| `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 |
|
|
185
|
+
| `Switch` | Binary settings | Custom toggle switches | Enable notifications, privacy setting, feature toggles; accent checked track with softened thumb border |
|
|
169
186
|
| `Tabs`, `TabsList`, `TabsTrigger`, `TabsContent` | In-page tabbed views | Custom segmented/tab controls | Profile sections, report views, settings categories |
|
|
170
187
|
| `TextInput` | Text entry | Raw `TextInput` with repeated label/error code | Email/password, search, numeric input, multiline notes |
|
|
171
188
|
| `Toggle`, `ToggleIcon` | Pressed/unpressed control | Button with local selected styling | Favorite, mute, bold/italic, view mode button |
|
|
@@ -195,11 +212,11 @@ Most compound components support both direct named imports and dot notation on t
|
|
|
195
212
|
| `ToggleGroup` | `ToggleGroupItem`, `ToggleGroupIcon` |
|
|
196
213
|
| `Tooltip` | `TooltipTrigger`, `TooltipContent`, `TooltipBody` |
|
|
197
214
|
|
|
198
|
-
Text aliases are exported for common semantic typography: `SerifText`, `SansSerifText`, `SerifBoldText`, `SansSerifBoldText`, `DisplayText`, `TitleText`, `HeadingText`, `SubheadingText`, `BodyText`, `CaptionText`, and `LabelText`. `TextClassContext` and `
|
|
215
|
+
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
216
|
|
|
200
217
|
### Common Patterns
|
|
201
218
|
|
|
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.
|
|
219
|
+
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
220
|
|
|
204
221
|
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
222
|
|
|
@@ -322,7 +339,7 @@ On web, `useResources()` injects the Google Fonts Lato stylesheet after hydratio
|
|
|
322
339
|
/>
|
|
323
340
|
```
|
|
324
341
|
|
|
325
|
-
On native, the package uses platform sans-serif fallbacks. `useResources()` still loads `Feather.font` from the
|
|
342
|
+
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
343
|
|
|
327
344
|
## Package Checks
|
|
328
345
|
|
|
@@ -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
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
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
|
package/dist/components/Badge.js
CHANGED
|
@@ -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.
|
|
14
|
-
paddingHorizontal:
|
|
13
|
+
paddingVertical: spacing.xxs,
|
|
14
|
+
paddingHorizontal: 10,
|
|
15
15
|
fontSize: 12,
|
|
16
|
-
height:
|
|
16
|
+
height: 28,
|
|
17
17
|
},
|
|
18
18
|
md: {
|
|
19
|
-
paddingVertical: spacing.
|
|
20
|
-
paddingHorizontal:
|
|
19
|
+
paddingVertical: spacing.xs,
|
|
20
|
+
paddingHorizontal: 12,
|
|
21
21
|
fontSize: 14,
|
|
22
|
-
height:
|
|
22
|
+
height: 32,
|
|
23
23
|
},
|
|
24
24
|
lg: {
|
|
25
|
-
paddingVertical:
|
|
26
|
-
paddingHorizontal: spacing.
|
|
27
|
-
fontSize:
|
|
28
|
-
height:
|
|
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
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
styles.
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
], children: children
|
|
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.
|
|
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
|
-
|
|
229
|
-
|
|
259
|
+
loaderOverlay: {
|
|
260
|
+
...StyleSheet.absoluteFillObject,
|
|
261
|
+
alignItems: "center",
|
|
262
|
+
justifyContent: "center",
|
|
230
263
|
},
|
|
231
264
|
});
|
|
232
265
|
};
|
package/dist/components/Card.js
CHANGED
|
@@ -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:
|
|
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 :
|
|
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
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
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();
|