@mrmeg/expo-ui 0.14.0 → 0.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +81 -0
- package/README.md +50 -6
- package/dist/components/Accordion.d.ts +1 -1
- package/dist/components/Accordion.js +25 -19
- package/dist/components/Badge.js +10 -7
- package/dist/components/BottomSheet.js +21 -19
- package/dist/components/Button.d.ts +2 -1
- package/dist/components/Button.js +22 -6
- package/dist/components/Card.js +5 -3
- package/dist/components/Checkbox.js +49 -19
- package/dist/components/Collapsible.d.ts +1 -1
- package/dist/components/Collapsible.js +22 -16
- package/dist/components/Drawer.js +8 -7
- package/dist/components/DropdownMenu.d.ts +1 -1
- package/dist/components/DropdownMenu.js +80 -59
- package/dist/components/EmptyState.d.ts +7 -1
- package/dist/components/EmptyState.js +26 -9
- package/dist/components/InputOTP.js +10 -4
- package/dist/components/Item.d.ts +93 -0
- package/dist/components/Item.js +125 -0
- package/dist/components/Label.js +10 -4
- package/dist/components/Notification.js +14 -24
- package/dist/components/Popover.d.ts +3 -18
- package/dist/components/Popover.js +10 -6
- package/dist/components/RadioGroup.js +49 -22
- package/dist/components/SectionHeader.d.ts +42 -0
- package/dist/components/SectionHeader.js +41 -0
- package/dist/components/Select.js +73 -41
- package/dist/components/Skeleton.js +5 -3
- package/dist/components/StatCard.d.ts +45 -0
- package/dist/components/StatCard.js +41 -0
- package/dist/components/StyledText.d.ts +20 -10
- package/dist/components/StyledText.js +85 -29
- package/dist/components/Switch.js +84 -57
- package/dist/components/Tabs.js +43 -12
- package/dist/components/TextInput.js +17 -11
- package/dist/components/Toggle.js +69 -42
- package/dist/components/ToggleGroup.js +82 -55
- package/dist/components/index.d.ts +3 -0
- package/dist/components/index.js +3 -0
- package/dist/constants/fonts.d.ts +44 -5
- package/dist/constants/fonts.js +42 -12
- package/dist/constants/index.d.ts +1 -0
- package/dist/constants/index.js +1 -0
- package/dist/constants/motion.d.ts +24 -0
- package/dist/constants/motion.js +24 -0
- package/dist/constants/spacing.d.ts +7 -7
- package/dist/constants/spacing.js +7 -7
- package/dist/hooks/useResources.d.ts +4 -2
- package/dist/hooks/useResources.js +30 -8
- package/dist/hooks/useStaggeredEntrance.d.ts +1 -1
- package/dist/hooks/useStaggeredEntrance.js +2 -1
- package/dist/hooks/useTheme.d.ts +1 -1
- package/dist/hooks/useTheme.js +53 -16
- package/dist/lib/index.d.ts +1 -0
- package/dist/lib/index.js +1 -0
- package/dist/lib/themedStyles.d.ts +21 -0
- package/dist/lib/themedStyles.js +33 -0
- package/dist/state/themeStore.d.ts +17 -0
- package/dist/state/themeStore.js +6 -0
- package/package.json +20 -19
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,87 @@
|
|
|
3
3
|
All notable changes to `@mrmeg/expo-ui` are documented here. This project
|
|
4
4
|
adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
5
5
|
|
|
6
|
+
## [0.16.0]
|
|
7
|
+
|
|
8
|
+
### Added
|
|
9
|
+
|
|
10
|
+
- **`setFonts` — font injection, mirroring `setColors`.** A host app can now
|
|
11
|
+
forward its own bundled faces to the package:
|
|
12
|
+
|
|
13
|
+
```ts
|
|
14
|
+
useThemeStore.getState().setFonts({
|
|
15
|
+
families: { sansSerif: { light: "…", regular: "…", medium: "…",
|
|
16
|
+
semibold: "…", bold: "…" } },
|
|
17
|
+
webWeightStrategy: "family",
|
|
18
|
+
});
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Overrides are partial and per-group — pass `sansSerif` alone and `serif`
|
|
22
|
+
keeps the package default. `setFonts({})` clears back to the defaults.
|
|
23
|
+
`StyledText` subscribes to the store, so a call after mount re-renders
|
|
24
|
+
rather than leaving stale families on screen.
|
|
25
|
+
|
|
26
|
+
- **`webWeightStrategy` — `"numeric" | "family"`.** Declares how a family map
|
|
27
|
+
expresses weight. `"numeric"` (the package default on web) assumes one
|
|
28
|
+
multi-weight CSS family where a numeric `fontWeight` selects the
|
|
29
|
+
`@font-face` variant. `"family"` means each weight is its own
|
|
30
|
+
separately-registered single-face family, so the family name alone carries
|
|
31
|
+
the weight and `StyledText` suppresses the numeric `fontWeight`. Native is
|
|
32
|
+
always `"family"`, unchanged.
|
|
33
|
+
|
|
34
|
+
Apps loading per-weight faces through `expo-font` need `"family"` on web:
|
|
35
|
+
`expo-font` registers each file as its own family there too, so the previous
|
|
36
|
+
always-numeric behaviour synthesised a second layer of bold on top of an
|
|
37
|
+
already-bold face.
|
|
38
|
+
|
|
39
|
+
- Exported `FontFamilyMap`, `FontFamilyWeight`, `FontOverrides`,
|
|
40
|
+
`FontWeightStrategy`, and `defaultWebWeightStrategy` from `constants/fonts`.
|
|
41
|
+
|
|
42
|
+
- **`createThemedStyles` — SSR-safe themed style registration**, exported from
|
|
43
|
+
`lib`. Wraps a `(theme: Theme) => styles` factory so both base themes are
|
|
44
|
+
registered with react-native-web at module scope — before expo-router
|
|
45
|
+
snapshots the server-side stylesheet — instead of lazily during render via
|
|
46
|
+
`useMemo`, which left SSR HTML referencing class names with no rules
|
|
47
|
+
(unstyled first paint until hydration). Override themes are cached per theme
|
|
48
|
+
object identity. The package's own components (Badge, Button, Card,
|
|
49
|
+
EmptyState, InputOTP, Label, Notification, Skeleton, TextInput) now use it.
|
|
50
|
+
|
|
51
|
+
### Why
|
|
52
|
+
|
|
53
|
+
`fontFamilies` was a module-level `const` with no injection point, so the only
|
|
54
|
+
way for a consumer to use its own type was to patch `dist/` inside
|
|
55
|
+
`node_modules`. Bun keys `patchedDependencies` on an exact `name@version`, so
|
|
56
|
+
those patches stop applying — silently, with no warning — the moment the
|
|
57
|
+
package version is bumped. Two consumer apps hit exactly that and shipped
|
|
58
|
+
fallback fonts without noticing. Colors already had `setColors` for this
|
|
59
|
+
reason; fonts now match.
|
|
60
|
+
|
|
61
|
+
Fully backward compatible: with no `setFonts` call, font resolution is
|
|
62
|
+
identical to 0.15.0.
|
|
63
|
+
|
|
64
|
+
## [0.15.0]
|
|
65
|
+
|
|
66
|
+
### Added
|
|
67
|
+
|
|
68
|
+
- **New composition primitives: `SectionHeader`, `StatCard`, and `Item`.**
|
|
69
|
+
`EmptyState` was also upgraded with richer layout options as part of the
|
|
70
|
+
same pass.
|
|
71
|
+
- **Typography system.** The package now loads Inter with real weights and a
|
|
72
|
+
letter-spacing scale via `useResources`, replacing synthetic bolding.
|
|
73
|
+
- Motion tokens, dual-layer shadow tokens, and softer radii in the design
|
|
74
|
+
tokens.
|
|
75
|
+
|
|
76
|
+
### Changed
|
|
77
|
+
|
|
78
|
+
- Interaction sweep across components: press feedback, focus rings, and
|
|
79
|
+
shadow adoption.
|
|
80
|
+
- Updated all `@rn-primitives/*` dependencies from `~1.4.0` to `~1.5.2`,
|
|
81
|
+
picking up upstream's native accessibility overhaul: menus, popovers, and
|
|
82
|
+
dialogs are now usable with VoiceOver/TalkBack (focus moves into opened
|
|
83
|
+
content, escape gestures dismiss), toggles and switches announce the
|
|
84
|
+
correct state, and `nativeID` plumbing that broke Reanimated exiting
|
|
85
|
+
animations was removed.
|
|
86
|
+
|
|
6
87
|
## [0.14.0]
|
|
7
88
|
|
|
8
89
|
### Changed
|
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @mrmeg/expo-ui
|
|
2
2
|
|
|
3
|
-
Reusable Expo and React Native UI primitives shared by the template and consumer apps.
|
|
3
|
+
Reusable Expo and React Native UI primitives shared by the template and consumer apps. Typography is Inter: native consumers load four static Inter weights via `@expo-google-fonts/inter`, web consumers load Inter from Google Fonts.
|
|
4
4
|
|
|
5
5
|
This package is public for installability, reuse across MrMeg projects, and
|
|
6
6
|
discoverability. It is a personal reusable Expo / React Native design-system
|
|
@@ -163,6 +163,50 @@ import { ThemeColorScope } from "@mrmeg/expo-ui/state";
|
|
|
163
163
|
</ThemeColorScope>
|
|
164
164
|
```
|
|
165
165
|
|
|
166
|
+
### Font overrides
|
|
167
|
+
|
|
168
|
+
The package bundles Inter (sans) and Georgia (serif). Most apps ship their own
|
|
169
|
+
faces — forward them with `setFonts`, the font counterpart to `setColors`.
|
|
170
|
+
Call it once at startup, after your fonts are registered:
|
|
171
|
+
|
|
172
|
+
```tsx
|
|
173
|
+
import { useThemeStore } from "@mrmeg/expo-ui/state";
|
|
174
|
+
|
|
175
|
+
useThemeStore.getState().setFonts({
|
|
176
|
+
families: {
|
|
177
|
+
sansSerif: {
|
|
178
|
+
light: "HankenGrotesk_300Light",
|
|
179
|
+
regular: "HankenGrotesk_400Regular",
|
|
180
|
+
medium: "HankenGrotesk_500Medium",
|
|
181
|
+
semibold: "HankenGrotesk_600SemiBold",
|
|
182
|
+
bold: "HankenGrotesk_700Bold",
|
|
183
|
+
},
|
|
184
|
+
},
|
|
185
|
+
webWeightStrategy: "family",
|
|
186
|
+
});
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
Overrides are partial and per-group: pass `sansSerif` alone and `serif` keeps
|
|
190
|
+
the package default. `setFonts({})` clears back to the defaults.
|
|
191
|
+
|
|
192
|
+
**Pick the right `webWeightStrategy`** — this is the one non-obvious part:
|
|
193
|
+
|
|
194
|
+
| Strategy | Use when | Effect |
|
|
195
|
+
|---|---|---|
|
|
196
|
+
| `"numeric"` (default) | You ship **one multi-weight CSS family** (a variable font, or Google Fonts' single `"Inter"` family) | A numeric `fontWeight` selects the `@font-face` variant |
|
|
197
|
+
| `"family"` | You load **per-weight faces via `expo-font`** | The family name alone carries the weight; the numeric `fontWeight` is suppressed |
|
|
198
|
+
|
|
199
|
+
If you load fonts through `expo-font` / `@expo-google-fonts`, choose
|
|
200
|
+
`"family"`. `expo-font` registers each weight as its own single-face family on
|
|
201
|
+
web as well as native, so emitting a numeric weight on top makes the browser
|
|
202
|
+
synthesise a *second* layer of bold over an already-bold face.
|
|
203
|
+
|
|
204
|
+
Native always behaves as `"family"` regardless of this setting.
|
|
205
|
+
|
|
206
|
+
> Prefer `setFonts` over patching `node_modules`. Bun keys
|
|
207
|
+
> `patchedDependencies` to an exact `name@version`, so a font patch silently
|
|
208
|
+
> stops applying the next time you bump the package — with no warning.
|
|
209
|
+
|
|
166
210
|
Use `StyledText` for theme-aware text:
|
|
167
211
|
|
|
168
212
|
```tsx
|
|
@@ -394,21 +438,21 @@ export default function RootLayout() {
|
|
|
394
438
|
|
|
395
439
|
## Fonts
|
|
396
440
|
|
|
397
|
-
|
|
441
|
+
Typography is Inter across every platform, in four static weights (`Inter_400Regular`, `Inter_500Medium`, `Inter_600SemiBold`, `Inter_700Bold`) from `@expo-google-fonts/inter` — a package dependency, so consumers get it automatically.
|
|
398
442
|
|
|
399
|
-
On web, `useResources()` injects
|
|
443
|
+
On native, `useResources()` loads the four static weights so `StyledText`'s `light`–`bold` range resolves to real font files instead of a faked OS bold. On web, `useResources()` injects a single Google Fonts Inter stylesheet (covering all four weights) after hydration if the app has not already added it; weight differentiation on web comes from a numeric `fontWeight` alongside the shared `"Inter"` CSS family. For better first paint in Expo Router web apps, add the links in app-owned `app/+html.tsx`:
|
|
400
444
|
|
|
401
445
|
```tsx
|
|
402
446
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
403
447
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossOrigin="anonymous" />
|
|
404
448
|
<link
|
|
405
|
-
id="mrmeg-expo-ui-
|
|
449
|
+
id="mrmeg-expo-ui-inter"
|
|
406
450
|
rel="stylesheet"
|
|
407
|
-
href="https://fonts.googleapis.com/css2?family=
|
|
451
|
+
href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap"
|
|
408
452
|
/>
|
|
409
453
|
```
|
|
410
454
|
|
|
411
|
-
|
|
455
|
+
`useResources()` also loads `Feather.font` from the package-managed `@expo/vector-icons` dependency for icon rendering.
|
|
412
456
|
|
|
413
457
|
## Package Checks
|
|
414
458
|
|
|
@@ -43,7 +43,7 @@ declare function AccordionItem({ children, value, style: styleOverride, ...props
|
|
|
43
43
|
* Clickable header that expands/collapses the content
|
|
44
44
|
* Includes animated chevron icon
|
|
45
45
|
*/
|
|
46
|
-
declare function AccordionTrigger({ children, style: styleOverride, ...props }: AccordionPrimitive.TriggerProps & {
|
|
46
|
+
declare function AccordionTrigger({ children, style: styleOverride, disabled, ...props }: AccordionPrimitive.TriggerProps & {
|
|
47
47
|
children?: React.ReactNode;
|
|
48
48
|
} & React.RefAttributes<AccordionPrimitive.TriggerRef>): import("react").JSX.Element;
|
|
49
49
|
/**
|
|
@@ -5,6 +5,7 @@ import { Icon } from "./Icon.js";
|
|
|
5
5
|
import { TextClassContext, TextSelectabilityContext } from "./StyledText.context";
|
|
6
6
|
import { useTheme } from "../hooks/useTheme.js";
|
|
7
7
|
import { useReducedMotion } from "../hooks/useReduceMotion.js";
|
|
8
|
+
import { useScalePress } from "../hooks/useScalePress.js";
|
|
8
9
|
import { spacing } from "../constants/spacing.js";
|
|
9
10
|
import * as AccordionPrimitive from "@rn-primitives/accordion";
|
|
10
11
|
function normalizeSingleValue(value) {
|
|
@@ -96,11 +97,16 @@ const Trigger = Platform.OS === "web" ? View : Pressable;
|
|
|
96
97
|
* Clickable header that expands/collapses the content
|
|
97
98
|
* Includes animated chevron icon
|
|
98
99
|
*/
|
|
99
|
-
function AccordionTrigger({ children, style: styleOverride, ...props }) {
|
|
100
|
+
function AccordionTrigger({ children, style: styleOverride, disabled, ...props }) {
|
|
100
101
|
const { theme } = useTheme();
|
|
101
102
|
const reduceMotion = useReducedMotion();
|
|
102
103
|
const { isExpanded } = AccordionPrimitive.useItemContext();
|
|
103
104
|
const rotation = useRef(new Animated.Value(isExpanded ? 1 : 0)).current;
|
|
105
|
+
const { animatedStyle: scaleStyle, pressHandlers } = useScalePress({
|
|
106
|
+
disabled: !!disabled,
|
|
107
|
+
scaleTo: 0.97,
|
|
108
|
+
haptic: false,
|
|
109
|
+
});
|
|
104
110
|
useEffect(() => {
|
|
105
111
|
const target = isExpanded ? 1 : 0;
|
|
106
112
|
Animated.timing(rotation, {
|
|
@@ -119,24 +125,24 @@ function AccordionTrigger({ children, style: styleOverride, ...props }) {
|
|
|
119
125
|
},
|
|
120
126
|
],
|
|
121
127
|
};
|
|
122
|
-
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: [
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
128
|
+
return (_jsx(TextClassContext.Provider, { value: "", children: _jsx(TextSelectabilityContext.Provider, { value: false, children: _jsx(AccordionPrimitive.Header, { children: _jsx(Animated.View, { style: scaleStyle, children: _jsx(AccordionPrimitive.Trigger, { ...props, disabled: disabled, asChild: true, children: _jsxs(Trigger, { onPressIn: pressHandlers.onPressIn, onPressOut: pressHandlers.onPressOut, style: [
|
|
129
|
+
{
|
|
130
|
+
flexDirection: "row",
|
|
131
|
+
alignItems: "center",
|
|
132
|
+
justifyContent: "space-between",
|
|
133
|
+
gap: spacing.md,
|
|
134
|
+
borderRadius: spacing.radiusMd,
|
|
135
|
+
paddingVertical: spacing.md,
|
|
136
|
+
...(Platform.OS === "web" && {
|
|
137
|
+
cursor: "pointer",
|
|
138
|
+
userSelect: "none",
|
|
139
|
+
}),
|
|
140
|
+
},
|
|
141
|
+
// Spread array styles from primitives to prevent nested arrays on web
|
|
142
|
+
...(styleOverride && typeof styleOverride !== "function"
|
|
143
|
+
? (Array.isArray(styleOverride) ? styleOverride : [styleOverride])
|
|
144
|
+
: []),
|
|
145
|
+
], children: [_jsx(_Fragment, { children: children }), _jsx(Animated.View, { style: chevronStyle, children: _jsx(Icon, { name: "chevron-down", size: 16, color: theme.colors.textDim, decorative: true }) })] }) }) }) }) }) }));
|
|
140
146
|
}
|
|
141
147
|
/**
|
|
142
148
|
* Accordion Content Component
|
package/dist/components/Badge.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import React
|
|
2
|
+
import React 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";
|
|
6
6
|
import { StyledText } from "./StyledText.js";
|
|
7
|
+
import { createThemedStyles } from "../lib/themedStyles.js";
|
|
7
8
|
/**
|
|
8
9
|
* Badge Component
|
|
9
10
|
*
|
|
@@ -19,7 +20,7 @@ import { StyledText } from "./StyledText.js";
|
|
|
19
20
|
*/
|
|
20
21
|
function Badge({ children, text, variant = "default", style: styleOverride }) {
|
|
21
22
|
const { theme } = useTheme();
|
|
22
|
-
const styles =
|
|
23
|
+
const styles = themedStyles(theme);
|
|
23
24
|
const badgeContent = text ?? children;
|
|
24
25
|
const textStyle = [
|
|
25
26
|
styles.text,
|
|
@@ -30,9 +31,13 @@ function Badge({ children, text, variant = "default", style: styleOverride }) {
|
|
|
30
31
|
];
|
|
31
32
|
const normalizedChildren = React.Children.toArray(badgeContent);
|
|
32
33
|
const hasOnlyTextChildren = normalizedChildren.every((child) => typeof child === "string" || typeof child === "number");
|
|
33
|
-
|
|
34
|
+
// size="sm" + fontWeight="medium" match this badge's fontSize/lineHeight/weight
|
|
35
|
+
// (12/18/500) via the shared StyledText scale — including its "sm" letter
|
|
36
|
+
// spacing and correct native weight-file resolution — instead of a hand-rolled
|
|
37
|
+
// fontSize/fontWeight pair.
|
|
38
|
+
const content = hasOnlyTextChildren ? (_jsx(StyledText, { selectable: false, size: "sm", fontWeight: "medium", style: textStyle, children: normalizedChildren.join("") })) : (React.Children.map(badgeContent, (child) => {
|
|
34
39
|
if (typeof child === "string" || typeof child === "number") {
|
|
35
|
-
return _jsx(StyledText, { selectable: false, style: textStyle, children: child });
|
|
40
|
+
return _jsx(StyledText, { selectable: false, size: "sm", fontWeight: "medium", style: textStyle, children: child });
|
|
36
41
|
}
|
|
37
42
|
return child;
|
|
38
43
|
}));
|
|
@@ -67,10 +72,8 @@ const createStyles = (theme) => StyleSheet.create({
|
|
|
67
72
|
backgroundColor: theme.colors.destructive,
|
|
68
73
|
},
|
|
69
74
|
text: {
|
|
70
|
-
fontSize: 12,
|
|
71
|
-
fontWeight: "500",
|
|
72
|
-
lineHeight: 18,
|
|
73
75
|
userSelect: "none",
|
|
74
76
|
},
|
|
75
77
|
});
|
|
78
|
+
const themedStyles = createThemedStyles(createStyles);
|
|
76
79
|
export { Badge };
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import React, { createContext, use, useCallback, useEffect, useMemo, useReducer, useRef, useSyncExternalStore } from "react";
|
|
3
|
-
import { View, Pressable, StyleSheet, ScrollView, Platform, useWindowDimensions, } from "react-native";
|
|
3
|
+
import { View, Pressable, StyleSheet, ScrollView, Platform, Animated, useWindowDimensions, } from "react-native";
|
|
4
4
|
import { BottomSheet as NativeBottomSheet } from "@expo/ui/community/bottom-sheet";
|
|
5
5
|
import { KeyboardController } from "react-native-keyboard-controller";
|
|
6
6
|
import { useSafeAreaInsets, initialWindowMetrics } from "react-native-safe-area-context";
|
|
7
7
|
import { useTheme } from "../hooks/useTheme.js";
|
|
8
8
|
import { spacing } from "../constants/spacing.js";
|
|
9
|
+
import { useScalePress } from "../hooks/useScalePress.js";
|
|
9
10
|
import { TextColorContext, TextClassContext } from "./StyledText.context";
|
|
10
11
|
import { Icon } from "./Icon.js";
|
|
11
12
|
import { dismissKeyboardFocusedInput, hasKeyboardFocusedInput, subscribeKeyboardFocus, } from "./keyboardFocusRegistry.js";
|
|
@@ -140,24 +141,25 @@ function useShowClose() {
|
|
|
140
141
|
function SheetCloseButton({ style }) {
|
|
141
142
|
const { onOpenChange } = useBottomSheetContext();
|
|
142
143
|
const { theme } = useTheme();
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
144
|
+
const { animatedStyle: scaleStyle, pressHandlers } = useScalePress({
|
|
145
|
+
scaleTo: 0.96,
|
|
146
|
+
});
|
|
147
|
+
return (_jsx(Animated.View, { style: [style, scaleStyle], children: _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: "Close", onPress: () => onOpenChange(false), onPressIn: pressHandlers.onPressIn, onPressOut: pressHandlers.onPressOut, hitSlop: spacing.sm, style: [
|
|
148
|
+
{
|
|
149
|
+
width: spacing.xl,
|
|
150
|
+
height: spacing.xl,
|
|
151
|
+
borderRadius: spacing.xl / 2,
|
|
152
|
+
alignItems: "center",
|
|
153
|
+
justifyContent: "center",
|
|
154
|
+
// Higher-contrast than `muted`: a solid `secondary` fill with a
|
|
155
|
+
// bordered edge so the control reads clearly against the card, and a
|
|
156
|
+
// full-strength `foreground` glyph instead of the faint muted one.
|
|
157
|
+
backgroundColor: theme.colors.secondary,
|
|
158
|
+
borderWidth: 1,
|
|
159
|
+
borderColor: theme.colors.border,
|
|
160
|
+
},
|
|
161
|
+
Platform.OS === "web" && { cursor: "pointer" },
|
|
162
|
+
], children: _jsx(Icon, { name: "x", size: 22, color: "text" }) }) }));
|
|
161
163
|
}
|
|
162
164
|
// ============================================================================
|
|
163
165
|
// Keyboard dismiss overlay — tap-away dismissal inside the sheet
|
|
@@ -90,7 +90,8 @@ export interface ButtonProps extends PressableProps {
|
|
|
90
90
|
*/
|
|
91
91
|
disabledStyle?: StyleProp<ViewStyle>;
|
|
92
92
|
/**
|
|
93
|
-
* Whether to show shadow
|
|
93
|
+
* Whether to show shadow.
|
|
94
|
+
* @default true for the `default` preset, `false` for every other preset.
|
|
94
95
|
*/
|
|
95
96
|
withShadow?: boolean;
|
|
96
97
|
/**
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { use, useCallback,
|
|
2
|
+
import { use, useCallback, useState } from "react";
|
|
3
3
|
import { Pressable, StyleSheet, View, Platform, ActivityIndicator, Animated, } from "react-native";
|
|
4
4
|
import { spacing } from "../constants/spacing.js";
|
|
5
5
|
import { StyledText } from "./StyledText.js";
|
|
@@ -9,6 +9,7 @@ import { fontFamilies } from "../constants/fonts.js";
|
|
|
9
9
|
import { palette } from "../constants/colors.js";
|
|
10
10
|
import { useTheme } from "../hooks/useTheme.js";
|
|
11
11
|
import { useScalePress } from "../hooks/useScalePress.js";
|
|
12
|
+
import { createThemedStyles } from "../lib/themedStyles.js";
|
|
12
13
|
const SIZE_CONFIGS = {
|
|
13
14
|
sm: {
|
|
14
15
|
paddingVertical: spacing.xxs,
|
|
@@ -70,11 +71,15 @@ const getNativeHitSlop = (sizeConfig) => Math.ceil(Math.max(0, spacing.touchTarg
|
|
|
70
71
|
* ```
|
|
71
72
|
*/
|
|
72
73
|
function ButtonRoot(props) {
|
|
73
|
-
const { tx, text, txOptions, style: styleOverride, pressedStyle: pressedStyleOverride, textStyle: textStyleOverride, pressedTextStyle: pressedTextStyleOverride, disabledTextStyle: disabledTextStyleOverride, children, RightAccessory, LeftAccessory, disabled, disabledStyle: disabledStyleOverride, withShadow
|
|
74
|
+
const { tx, text, txOptions, style: styleOverride, pressedStyle: pressedStyleOverride, textStyle: textStyleOverride, pressedTextStyle: pressedTextStyleOverride, disabledTextStyle: disabledTextStyleOverride, children, RightAccessory, LeftAccessory, disabled, disabledStyle: disabledStyleOverride, withShadow: withShadowProp, preset = "default", size = "md", loading = false, fullWidth = false, onFocus, onBlur, onPressIn, onPressOut, ...rest } = props;
|
|
74
75
|
const { theme, getContrastingColor, getFocusRingStyle, getShadowStyle } = useTheme();
|
|
75
|
-
const styles =
|
|
76
|
-
const shadowStyle = getShadowStyle("
|
|
76
|
+
const styles = themedStyles(theme)[size];
|
|
77
|
+
const shadowStyle = getShadowStyle("subtle");
|
|
77
78
|
const sizeConfig = SIZE_CONFIGS[size];
|
|
79
|
+
// Filled (`default`) buttons float by default; ghost/outline/link stay flat
|
|
80
|
+
// (transparent background reads oddly with a shadow), and destructive/secondary
|
|
81
|
+
// keep the old opt-in behavior. Callers can still override via `withShadow`.
|
|
82
|
+
const withShadow = withShadowProp ?? preset === "default";
|
|
78
83
|
const focusRingStyle = getFocusRingStyle();
|
|
79
84
|
// Pre-compute background color for contrast calculation
|
|
80
85
|
// Always flatten to handle both array styles (from Slot) and RegisteredStyle
|
|
@@ -278,9 +283,15 @@ const createStyles = (theme, size) => {
|
|
|
278
283
|
width: "100%",
|
|
279
284
|
},
|
|
280
285
|
text: {
|
|
281
|
-
|
|
286
|
+
// Button labels render at medium weight. On native, the family itself
|
|
287
|
+
// carries the weight (a real static Inter_500Medium file) — pairing it
|
|
288
|
+
// with a numeric fontWeight would faux-bold on top of that file. Web
|
|
289
|
+
// shares one "Inter" family across weights, so it needs the numeric
|
|
290
|
+
// fontWeight to pick the right @font-face variant. (Same rule as
|
|
291
|
+
// StyledText's getFontFamilyWeight / WEB_FONT_WEIGHTS.)
|
|
292
|
+
fontFamily: fontFamilies.sansSerif.medium,
|
|
282
293
|
fontSize: sizeConfig.fontSize,
|
|
283
|
-
fontWeight: "500",
|
|
294
|
+
...(Platform.OS === "web" && { fontWeight: "500" }),
|
|
284
295
|
textAlign: "center",
|
|
285
296
|
lineHeight: sizeConfig.fontSize * 1.4,
|
|
286
297
|
flexShrink: 0,
|
|
@@ -311,6 +322,11 @@ const createStyles = (theme, size) => {
|
|
|
311
322
|
},
|
|
312
323
|
});
|
|
313
324
|
};
|
|
325
|
+
const themedStyles = createThemedStyles((theme) => ({
|
|
326
|
+
sm: createStyles(theme, "sm"),
|
|
327
|
+
md: createStyles(theme, "md"),
|
|
328
|
+
lg: createStyles(theme, "lg"),
|
|
329
|
+
}));
|
|
314
330
|
/**
|
|
315
331
|
* Button with explicit subcomponents.
|
|
316
332
|
* - `Button.Text` for label text (inherits control typography)
|
package/dist/components/Card.js
CHANGED
|
@@ -5,6 +5,7 @@ import { StyledText } from "./StyledText.js";
|
|
|
5
5
|
import { useTheme } from "../hooks/useTheme.js";
|
|
6
6
|
import { useScalePress } from "../hooks/useScalePress.js";
|
|
7
7
|
import { spacing } from "../constants/spacing.js";
|
|
8
|
+
import { createThemedStyles } from "../lib/themedStyles.js";
|
|
8
9
|
/**
|
|
9
10
|
* Card Component
|
|
10
11
|
*
|
|
@@ -30,15 +31,15 @@ import { spacing } from "../constants/spacing.js";
|
|
|
30
31
|
const CardContext = createContext(null);
|
|
31
32
|
function useCardContext() {
|
|
32
33
|
const ctx = use(CardContext);
|
|
33
|
-
// useTheme must run unconditionally (Rules of Hooks);
|
|
34
|
+
// useTheme must run unconditionally (Rules of Hooks); themedStyles is a
|
|
34
35
|
// plain function, so the `??` keeps the fallback styles lazy.
|
|
35
36
|
const { theme } = useTheme();
|
|
36
37
|
// Fallback for standalone usage without a Card parent.
|
|
37
|
-
return ctx ?? { theme, styles:
|
|
38
|
+
return ctx ?? { theme, styles: themedStyles(theme) };
|
|
38
39
|
}
|
|
39
40
|
function Card({ children, style: styleOverride, variant = "default", onPress, disabled }) {
|
|
40
41
|
const { theme, getShadowStyle } = useTheme();
|
|
41
|
-
const styles =
|
|
42
|
+
const styles = themedStyles(theme);
|
|
42
43
|
const shadowStyle = getShadowStyle("subtle");
|
|
43
44
|
const ctx = { theme, styles };
|
|
44
45
|
const { animatedStyle: scaleStyle, pressHandlers } = useScalePress({
|
|
@@ -122,4 +123,5 @@ const createCardStyles = (theme) => StyleSheet.create({
|
|
|
122
123
|
lineHeight: 20,
|
|
123
124
|
},
|
|
124
125
|
});
|
|
126
|
+
const themedStyles = createThemedStyles(createCardStyles);
|
|
125
127
|
export { Card, CardHeader, CardContent, CardFooter, CardTitle, CardDescription };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useCallback, useEffect, useRef } from "react";
|
|
2
|
+
import { useCallback, useEffect, useRef, useState } from "react";
|
|
3
3
|
import { View, StyleSheet, Pressable, Platform, Animated } from "react-native";
|
|
4
4
|
import { Icon } from "./Icon.js";
|
|
5
5
|
import { StyledText } from "./StyledText.js";
|
|
@@ -7,6 +7,7 @@ import { useTheme } from "../hooks/useTheme.js";
|
|
|
7
7
|
import { spacing } from "../constants/spacing.js";
|
|
8
8
|
import { hapticLight } from "../lib/haptics.js";
|
|
9
9
|
import { useReducedMotion } from "../hooks/useReduceMotion.js";
|
|
10
|
+
import { useScalePress } from "../hooks/useScalePress.js";
|
|
10
11
|
import * as CheckboxPrimitive from "@rn-primitives/checkbox";
|
|
11
12
|
const DEFAULT_HIT_SLOP = 8;
|
|
12
13
|
const SIZE_CONFIGS = {
|
|
@@ -15,9 +16,34 @@ const SIZE_CONFIGS = {
|
|
|
15
16
|
lg: { size: 24, iconSize: 20, strokeWidth: 3.5 },
|
|
16
17
|
};
|
|
17
18
|
function Checkbox({ size = "md", label, indeterminate = false, error = false, style: styleOverride, labelStyle, required = false, checked, onCheckedChange, disabled, ...props }) {
|
|
18
|
-
const { theme, getContrastingColor } = useTheme();
|
|
19
|
+
const { theme, getContrastingColor, getFocusRingStyle } = useTheme();
|
|
19
20
|
const reduceMotion = useReducedMotion();
|
|
20
21
|
const sizeConfig = SIZE_CONFIGS[size];
|
|
22
|
+
const focusRingStyle = getFocusRingStyle();
|
|
23
|
+
const [focused, setFocused] = useState(false);
|
|
24
|
+
const { animatedStyle: scaleStyle, pressHandlers } = useScalePress({
|
|
25
|
+
disabled: !!disabled,
|
|
26
|
+
scaleTo: 0.92,
|
|
27
|
+
haptic: false,
|
|
28
|
+
});
|
|
29
|
+
const showFocusRing = (event) => {
|
|
30
|
+
let ringVisible = true;
|
|
31
|
+
if (Platform.OS === "web") {
|
|
32
|
+
const target = event?.nativeEvent?.target;
|
|
33
|
+
if (target && typeof target.matches === "function") {
|
|
34
|
+
try {
|
|
35
|
+
ringVisible = target.matches(":focus-visible");
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
ringVisible = true;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
setFocused(ringVisible);
|
|
43
|
+
};
|
|
44
|
+
const hideFocusRing = () => {
|
|
45
|
+
setFocused(false);
|
|
46
|
+
};
|
|
21
47
|
// Simple fast opacity for the checkmark icon
|
|
22
48
|
const checkOpacity = useRef(new Animated.Value(checked || indeterminate ? 1 : 0)).current;
|
|
23
49
|
const isVisuallyChecked = !!checked || indeterminate;
|
|
@@ -45,22 +71,23 @@ function Checkbox({ size = "md", label, indeterminate = false, error = false, st
|
|
|
45
71
|
: getContrastingColor(theme.colors.background, theme.colors.text, theme.colors.textDim);
|
|
46
72
|
// Flatten style override for web compatibility
|
|
47
73
|
const flattenedStyle = styleOverride ? StyleSheet.flatten(styleOverride) : undefined;
|
|
48
|
-
const checkboxElement = (_jsx(CheckboxPrimitive.Root, { ...props, checked: checked, onCheckedChange: wrappedOnCheckedChange, disabled: disabled, style: {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
74
|
+
const checkboxElement = (_jsx(Animated.View, { style: scaleStyle, children: _jsx(CheckboxPrimitive.Root, { ...props, checked: checked, onCheckedChange: wrappedOnCheckedChange, disabled: disabled, onPressIn: pressHandlers.onPressIn, onPressOut: pressHandlers.onPressOut, onFocus: showFocusRing, onBlur: hideFocusRing, style: {
|
|
75
|
+
...styles.box,
|
|
76
|
+
borderColor,
|
|
77
|
+
backgroundColor: isVisuallyChecked ? theme.colors.primary : theme.colors.background,
|
|
78
|
+
width: sizeConfig.size,
|
|
79
|
+
height: sizeConfig.size,
|
|
80
|
+
opacity: disabled ? 0.5 : 1,
|
|
81
|
+
...(Platform.OS === "web" && { cursor: disabled ? "not-allowed" : "pointer" }),
|
|
82
|
+
...(focused && !disabled ? focusRingStyle : null),
|
|
83
|
+
...(flattenedStyle || {}),
|
|
84
|
+
}, hitSlop: DEFAULT_HIT_SLOP, accessibilityRole: "checkbox", accessibilityState: {
|
|
85
|
+
checked: indeterminate ? "mixed" : checked,
|
|
86
|
+
disabled: !!disabled,
|
|
87
|
+
}, accessibilityLabel: label, children: _jsx(CheckboxPrimitive.Indicator, { style: {
|
|
88
|
+
justifyContent: "center",
|
|
89
|
+
alignItems: "center",
|
|
90
|
+
}, children: _jsx(Animated.View, { style: { opacity: checkOpacity }, children: indeterminate ? (_jsx(Icon, { name: "minus", size: sizeConfig.iconSize, color: theme.colors.primaryForeground })) : (_jsx(Icon, { name: "check", size: sizeConfig.iconSize, color: theme.colors.primaryForeground })) }) }) }) }));
|
|
64
91
|
// If no label, return just the checkbox
|
|
65
92
|
if (!label) {
|
|
66
93
|
return checkboxElement;
|
|
@@ -78,7 +105,10 @@ function Checkbox({ size = "md", label, indeterminate = false, error = false, st
|
|
|
78
105
|
}
|
|
79
106
|
const styles = StyleSheet.create({
|
|
80
107
|
box: {
|
|
81
|
-
|
|
108
|
+
// radiusXs (not radiusSm) — at the checkbox's 16-24px sizes, radiusSm
|
|
109
|
+
// post-rebase (8px) reads as over-rounded; radiusXs keeps the same
|
|
110
|
+
// proportion the box had before the radius rebase.
|
|
111
|
+
borderRadius: spacing.radiusXs,
|
|
82
112
|
borderWidth: 1,
|
|
83
113
|
justifyContent: "center",
|
|
84
114
|
alignItems: "center",
|
|
@@ -41,7 +41,7 @@ declare function Collapsible({ children, ...props }: CollapsibleProps): React.JS
|
|
|
41
41
|
* ```
|
|
42
42
|
*/
|
|
43
43
|
type CollapsibleTriggerProps = CollapsiblePrimitive.TriggerProps;
|
|
44
|
-
declare function CollapsibleTrigger({ style: styleOverride, ...props }: CollapsibleTriggerProps): React.JSX.Element;
|
|
44
|
+
declare function CollapsibleTrigger({ style: styleOverride, disabled, ...props }: CollapsibleTriggerProps): React.JSX.Element;
|
|
45
45
|
/**
|
|
46
46
|
* CollapsibleContent Component
|
|
47
47
|
* The content that expands and collapses
|
|
@@ -1,28 +1,34 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
|
-
import { Platform, StyleSheet, View } from "react-native";
|
|
2
|
+
import { Animated, Platform, StyleSheet, View } from "react-native";
|
|
3
3
|
import { TextClassContext, TextSelectabilityContext } from "./StyledText.context";
|
|
4
4
|
import { spacing } from "../constants/spacing.js";
|
|
5
5
|
import { useTheme } from "../hooks/useTheme.js";
|
|
6
|
+
import { useScalePress } from "../hooks/useScalePress.js";
|
|
6
7
|
import * as CollapsiblePrimitive from "@rn-primitives/collapsible";
|
|
7
8
|
function Collapsible({ children, ...props }) {
|
|
8
9
|
return (_jsx(CollapsiblePrimitive.Root, { ...props, asChild: Platform.OS !== "web", children: _jsx(View, { children: children }) }));
|
|
9
10
|
}
|
|
10
|
-
function CollapsibleTrigger({ style: styleOverride, ...props }) {
|
|
11
|
+
function CollapsibleTrigger({ style: styleOverride, disabled, ...props }) {
|
|
11
12
|
const { theme } = useTheme();
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
13
|
+
const { animatedStyle: scaleStyle, pressHandlers } = useScalePress({
|
|
14
|
+
disabled: !!disabled,
|
|
15
|
+
scaleTo: 0.97,
|
|
16
|
+
haptic: false,
|
|
17
|
+
});
|
|
18
|
+
return (_jsx(TextClassContext.Provider, { value: "", children: _jsx(TextSelectabilityContext.Provider, { value: false, children: _jsx(Animated.View, { style: scaleStyle, children: _jsx(CollapsiblePrimitive.Trigger, { disabled: disabled, ...props, onPressIn: pressHandlers.onPressIn, onPressOut: pressHandlers.onPressOut, style: {
|
|
19
|
+
flexDirection: "row",
|
|
20
|
+
alignItems: "center",
|
|
21
|
+
justifyContent: "space-between",
|
|
22
|
+
paddingVertical: spacing.sm,
|
|
23
|
+
...(Platform.OS === "web" && {
|
|
24
|
+
cursor: "pointer",
|
|
25
|
+
outlineStyle: "none",
|
|
26
|
+
userSelect: "none",
|
|
27
|
+
}),
|
|
28
|
+
...(styleOverride && typeof styleOverride !== "function"
|
|
29
|
+
? StyleSheet.flatten(styleOverride)
|
|
30
|
+
: {}),
|
|
31
|
+
} }) }) }) }));
|
|
26
32
|
}
|
|
27
33
|
function CollapsibleContent({ forceMount, style: styleOverride, children, ...props }) {
|
|
28
34
|
return (_jsx(TextClassContext.Provider, { value: "", children: _jsx(CollapsiblePrimitive.Content, { ...props, forceMount: forceMount, children: _jsx(View, { style: {
|