@mrmeg/expo-ui 0.1.4 → 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 +16 -2
- package/README.md +24 -20
- package/dist/components/Accordion.js +19 -16
- package/dist/components/Badge.js +5 -4
- package/dist/components/Button.d.ts +1 -1
- 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 +6 -6
- 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 +15 -4
- 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 +3 -1
- package/dist/hooks/useTheme.js +46 -18
- package/dist/lib/i18n.d.ts +2 -2
- package/dist/lib/i18n.js +5 -5
- package/package.json +6 -7
package/LLM_USAGE.md
CHANGED
|
@@ -70,8 +70,9 @@ export function RootLayout() {
|
|
|
70
70
|
|
|
71
71
|
i18n is optional. Do not add app-level i18n setup just to use this package.
|
|
72
72
|
Plain children and `text` props work without `i18next` or `react-i18next`.
|
|
73
|
-
`tx` props render
|
|
74
|
-
translator
|
|
73
|
+
`tx` props render fallback text when provided and otherwise render the key
|
|
74
|
+
until the consumer opts in with a package-local translator. Package-owned
|
|
75
|
+
defaults such as notification titles stay human-readable without app i18n:
|
|
75
76
|
|
|
76
77
|
```tsx
|
|
77
78
|
import { configureExpoUiI18n } from "@mrmeg/expo-ui/lib";
|
|
@@ -85,6 +86,8 @@ configureExpoUiI18n((key, options) => i18n.t(key, options));
|
|
|
85
86
|
- Use `useTheme()` and semantic tokens instead of hardcoded colors.
|
|
86
87
|
- Use `StyledText` or its semantic aliases instead of raw `Text` for app UI.
|
|
87
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.
|
|
88
91
|
- Use `globalUIStore` plus root-mounted `UIProvider` for transient global feedback.
|
|
89
92
|
- Keep app monitoring, auth, API, and domain behavior outside this package.
|
|
90
93
|
|
|
@@ -94,7 +97,10 @@ Useful theme tokens include:
|
|
|
94
97
|
theme.colors.background;
|
|
95
98
|
theme.colors.foreground;
|
|
96
99
|
theme.colors.card;
|
|
100
|
+
theme.colors.popover;
|
|
97
101
|
theme.colors.border;
|
|
102
|
+
theme.colors.input;
|
|
103
|
+
theme.colors.ring;
|
|
98
104
|
theme.colors.primary;
|
|
99
105
|
theme.colors.secondary;
|
|
100
106
|
theme.colors.accent;
|
|
@@ -109,6 +115,14 @@ Token intent:
|
|
|
109
115
|
- `primary`: neutral action color
|
|
110
116
|
- `secondary`: neutral secondary surface
|
|
111
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.
|
|
112
126
|
|
|
113
127
|
## Component Use-Case Index
|
|
114
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
|
|
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,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,
|
|
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,10 +115,14 @@ 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
|
-
`tx` support is opt-in. Without a configured translator, `tx` renders
|
|
120
|
-
|
|
121
|
-
|
|
122
|
+
`tx` support is opt-in. Without a configured translator, `tx` renders its
|
|
123
|
+
fallback text when provided and otherwise renders the key; package-owned
|
|
124
|
+
defaults such as notification titles use readable fallback text. Consumers
|
|
125
|
+
that already use i18n can connect it once near app startup:
|
|
122
126
|
|
|
123
127
|
```tsx
|
|
124
128
|
import { configureExpoUiI18n } from "@mrmeg/expo-ui/lib";
|
|
@@ -140,7 +144,7 @@ All components are exported from `@mrmeg/expo-ui/components`; direct imports suc
|
|
|
140
144
|
| `AnimatedView` | Entrance and visibility animation | Hand-rolled Reanimated wrappers | Staggered list rows, revealed panels, animated empty states |
|
|
141
145
|
| `Badge` | Short status labels | Custom pill `View` + `Text` | Draft/active states, counts, plan labels, role tags |
|
|
142
146
|
| `BottomSheet` | Mobile-first modal sheets | Custom absolute-position sheets | Action pickers, mobile filters, quick edit forms, contextual details |
|
|
143
|
-
| `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 |
|
|
144
148
|
| `Card`, `CardHeader`, `CardTitle`, `CardDescription`, `CardContent`, `CardFooter` | Framed content groups | Ad hoc bordered panels | List items, pricing plans, settings sections, summaries, dashboards |
|
|
145
149
|
| `Checkbox` | Boolean selection | Custom checkmark controls | Terms consent, checklist items, multi-select filters, notification opt-ins |
|
|
146
150
|
| `Collapsible`, `CollapsibleTrigger`, `CollapsibleContent` | One-off disclosure | Local animated height wrappers | Advanced settings, hidden helper text, optional details |
|
|
@@ -158,13 +162,13 @@ All components are exported from `@mrmeg/expo-ui/components`; direct imports suc
|
|
|
158
162
|
| `Popover` | Anchored contextual content | Custom anchored views | Inline help, quick previews, contextual controls, small forms |
|
|
159
163
|
| `Progress` | Determinate or indeterminate progress | Layout-shifting spinners for progress regions | Upload progress, onboarding completion, long-running task state |
|
|
160
164
|
| `RadioGroup`, `RadioGroupItem` | Mutually exclusive choices | Custom radio rows | Plan interval, visibility choice, survey answer, preference setting |
|
|
161
|
-
| `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 |
|
|
162
166
|
| `Separator` | Horizontal or vertical dividers | Border-only spacer views | Menu dividers, section dividers, card dividers |
|
|
163
167
|
| `Skeleton`, `SkeletonText`, `SkeletonAvatar`, `SkeletonCard` | Loading placeholders | Blank space or generic spinners | List loading, profile card loading, dashboard placeholders |
|
|
164
|
-
| `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 |
|
|
165
169
|
| `StatusBar` | Theme-aware native status bar | Per-screen status-bar duplication | Root layout status styling, dark/light mode updates |
|
|
166
170
|
| `StyledText` and text aliases | Theme-aware typography | Raw `Text` with hardcoded styles | Titles, headings, labels, body copy, captions, translated text |
|
|
167
|
-
| `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 |
|
|
168
172
|
| `Tabs`, `TabsList`, `TabsTrigger`, `TabsContent` | In-page tabbed views | Custom segmented/tab controls | Profile sections, report views, settings categories |
|
|
169
173
|
| `TextInput` | Text entry | Raw `TextInput` with repeated label/error code | Email/password, search, numeric input, multiline notes |
|
|
170
174
|
| `Toggle`, `ToggleIcon` | Pressed/unpressed control | Button with local selected styling | Favorite, mute, bold/italic, view mode button |
|
|
@@ -194,11 +198,11 @@ Most compound components support both direct named imports and dot notation on t
|
|
|
194
198
|
| `ToggleGroup` | `ToggleGroupItem`, `ToggleGroupIcon` |
|
|
195
199
|
| `Tooltip` | `TooltipTrigger`, `TooltipContent`, `TooltipBody` |
|
|
196
200
|
|
|
197
|
-
Text aliases are exported for common semantic typography: `SerifText`, `SansSerifText`, `SerifBoldText`, `SansSerifBoldText`, `DisplayText`, `TitleText`, `HeadingText`, `SubheadingText`, `BodyText`, `CaptionText`, and `LabelText`. `TextClassContext` and `
|
|
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.
|
|
198
202
|
|
|
199
203
|
### Common Patterns
|
|
200
204
|
|
|
201
|
-
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.
|
|
202
206
|
|
|
203
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.
|
|
204
208
|
|
|
@@ -321,7 +325,7 @@ On web, `useResources()` injects the Google Fonts Lato stylesheet after hydratio
|
|
|
321
325
|
/>
|
|
322
326
|
```
|
|
323
327
|
|
|
324
|
-
On native, the package uses platform sans-serif fallbacks. `useResources()` still loads `Feather.font` from the
|
|
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.
|
|
325
329
|
|
|
326
330
|
## Package Checks
|
|
327
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
|
-
|
|
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 };
|
|
@@ -20,7 +20,7 @@ export interface ButtonProps extends PressableProps {
|
|
|
20
20
|
*/
|
|
21
21
|
tx?: TextProps["tx"];
|
|
22
22
|
/**
|
|
23
|
-
* The text to display
|
|
23
|
+
* The text to display directly, or as fallback text when `tx` is provided.
|
|
24
24
|
*/
|
|
25
25
|
text?: TextProps["text"];
|
|
26
26
|
/**
|
|
@@ -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();
|