@mrmeg/expo-ui 0.15.0 → 0.17.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 +118 -0
- package/LLM_USAGE.md +9 -0
- package/README.md +88 -1
- package/dist/components/Badge.js +4 -2
- package/dist/components/Button.js +31 -14
- package/dist/components/Card.js +5 -3
- package/dist/components/EmptyState.js +3 -2
- package/dist/components/InputOTP.js +7 -12
- package/dist/components/Label.js +14 -12
- package/dist/components/Notification.js +4 -2
- package/dist/components/Skeleton.js +3 -1
- package/dist/components/StyledText.d.ts +11 -2
- package/dist/components/StyledText.js +21 -30
- package/dist/components/Switch.js +2 -5
- package/dist/components/TextInput.js +22 -7
- package/dist/constants/fonts.d.ts +73 -12
- package/dist/constants/fonts.js +88 -2
- package/dist/hooks/index.d.ts +1 -0
- package/dist/hooks/index.js +1 -0
- package/dist/hooks/useFontStyle.d.ts +11 -0
- package/dist/hooks/useFontStyle.js +17 -0
- package/dist/hooks/useResources.d.ts +7 -0
- package/dist/hooks/useResources.js +13 -2
- 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 +49 -0
- package/dist/state/themeStore.js +11 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -3,6 +3,124 @@
|
|
|
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.17.0]
|
|
7
|
+
|
|
8
|
+
### Added
|
|
9
|
+
|
|
10
|
+
- **`mono` font variant.** `StyledText` accepts `variant="mono"` (plus a
|
|
11
|
+
`MonoText` convenience alias) for code, IDs, and tabular figures. Defaults
|
|
12
|
+
to the platform's system monospace (Menlo on iOS, `monospace` on Android, a
|
|
13
|
+
`ui-monospace` stack on web); host apps swap in their own faces via the new
|
|
14
|
+
`mono` group in `setFonts`.
|
|
15
|
+
|
|
16
|
+
- **Per-weight-partial font override groups.** `setFonts` families are now
|
|
17
|
+
partial at both levels: per group (`sansSerif` alone leaves `serif`/`mono`
|
|
18
|
+
at their defaults, as before) and per weight — weights missing from an
|
|
19
|
+
overridden group fall back to that group's `regular`, never to a package
|
|
20
|
+
face. Registering only Regular + Medium is enough. `serif` also gained the
|
|
21
|
+
full weight range (previously `regular`/`bold` only, and `SerifText`
|
|
22
|
+
ignored the requested weight; it now resolves per weight).
|
|
23
|
+
|
|
24
|
+
- **`useResources` skips the Inter download when `sansSerif` is overridden.**
|
|
25
|
+
A host app that forwards its own sans faces owns loading them; nothing
|
|
26
|
+
would reference the packaged Inter, so the native `.ttf` fetch and the web
|
|
27
|
+
Google-Fonts stylesheet are skipped. Call `setFonts` before the hook mounts
|
|
28
|
+
for the skip to apply; the Feather icon font always loads. Without
|
|
29
|
+
overrides, loading is unchanged.
|
|
30
|
+
|
|
31
|
+
- **`setShape` — shape injection, completing the `setColors`/`setFonts`
|
|
32
|
+
trio.** Grouped per component for future growth; currently covers Button:
|
|
33
|
+
|
|
34
|
+
```ts
|
|
35
|
+
useThemeStore.getState().setShape({
|
|
36
|
+
button: { borderRadius: 9999, withShadow: false },
|
|
37
|
+
});
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
`borderRadius` applies to every Button preset (package default stays 12);
|
|
41
|
+
`withShadow` controls the `default` preset's resting shadow (default stays
|
|
42
|
+
on). Precedence is caller-wins throughout: the per-instance `withShadow`
|
|
43
|
+
prop and caller `style` beat the global override, which beats the package
|
|
44
|
+
default. `setShape({})` clears back to the defaults.
|
|
45
|
+
|
|
46
|
+
- **Every text-rendering component now resolves families through the theme
|
|
47
|
+
store.** Button labels, Label, Switch track labels, InputOTP cells, and
|
|
48
|
+
TextInput previously hardcoded Inter family constants in their static
|
|
49
|
+
styles, so `setFonts` re-skinned `StyledText` but left control chrome on
|
|
50
|
+
Inter. They now share one resolver (`resolveFontStyle`, surfaced to
|
|
51
|
+
components as the `useFontStyle` hook, both exported), so a `setFonts` call
|
|
52
|
+
re-skins the whole package at once.
|
|
53
|
+
|
|
54
|
+
- Exported `FontVariant`, `FontFamilyOverride`, `ResolvedFontStyle`,
|
|
55
|
+
`resolveFontStyle` (from `constants`), `useFontStyle` (from `hooks`),
|
|
56
|
+
`ShapeOverrides` (from `state`), and `MonoText` (from `components`).
|
|
57
|
+
|
|
58
|
+
### Why
|
|
59
|
+
|
|
60
|
+
A consuming app rethemed to a new design language (custom sans/serif/mono
|
|
61
|
+
faces, pill buttons, flat surfaces) and hit the package's remaining hardcoded
|
|
62
|
+
Inter references, the fixed button radius, and the always-on default-preset
|
|
63
|
+
shadow — none of which were themable. Fully backward compatible: with no
|
|
64
|
+
`setFonts`/`setShape` call, fonts, radii, and shadows are identical to 0.16.0.
|
|
65
|
+
|
|
66
|
+
## [0.16.0]
|
|
67
|
+
|
|
68
|
+
### Added
|
|
69
|
+
|
|
70
|
+
- **`setFonts` — font injection, mirroring `setColors`.** A host app can now
|
|
71
|
+
forward its own bundled faces to the package:
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
useThemeStore.getState().setFonts({
|
|
75
|
+
families: { sansSerif: { light: "…", regular: "…", medium: "…",
|
|
76
|
+
semibold: "…", bold: "…" } },
|
|
77
|
+
webWeightStrategy: "family",
|
|
78
|
+
});
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
Overrides are partial and per-group — pass `sansSerif` alone and `serif`
|
|
82
|
+
keeps the package default. `setFonts({})` clears back to the defaults.
|
|
83
|
+
`StyledText` subscribes to the store, so a call after mount re-renders
|
|
84
|
+
rather than leaving stale families on screen.
|
|
85
|
+
|
|
86
|
+
- **`webWeightStrategy` — `"numeric" | "family"`.** Declares how a family map
|
|
87
|
+
expresses weight. `"numeric"` (the package default on web) assumes one
|
|
88
|
+
multi-weight CSS family where a numeric `fontWeight` selects the
|
|
89
|
+
`@font-face` variant. `"family"` means each weight is its own
|
|
90
|
+
separately-registered single-face family, so the family name alone carries
|
|
91
|
+
the weight and `StyledText` suppresses the numeric `fontWeight`. Native is
|
|
92
|
+
always `"family"`, unchanged.
|
|
93
|
+
|
|
94
|
+
Apps loading per-weight faces through `expo-font` need `"family"` on web:
|
|
95
|
+
`expo-font` registers each file as its own family there too, so the previous
|
|
96
|
+
always-numeric behaviour synthesised a second layer of bold on top of an
|
|
97
|
+
already-bold face.
|
|
98
|
+
|
|
99
|
+
- Exported `FontFamilyMap`, `FontFamilyWeight`, `FontOverrides`,
|
|
100
|
+
`FontWeightStrategy`, and `defaultWebWeightStrategy` from `constants/fonts`.
|
|
101
|
+
|
|
102
|
+
- **`createThemedStyles` — SSR-safe themed style registration**, exported from
|
|
103
|
+
`lib`. Wraps a `(theme: Theme) => styles` factory so both base themes are
|
|
104
|
+
registered with react-native-web at module scope — before expo-router
|
|
105
|
+
snapshots the server-side stylesheet — instead of lazily during render via
|
|
106
|
+
`useMemo`, which left SSR HTML referencing class names with no rules
|
|
107
|
+
(unstyled first paint until hydration). Override themes are cached per theme
|
|
108
|
+
object identity. The package's own components (Badge, Button, Card,
|
|
109
|
+
EmptyState, InputOTP, Label, Notification, Skeleton, TextInput) now use it.
|
|
110
|
+
|
|
111
|
+
### Why
|
|
112
|
+
|
|
113
|
+
`fontFamilies` was a module-level `const` with no injection point, so the only
|
|
114
|
+
way for a consumer to use its own type was to patch `dist/` inside
|
|
115
|
+
`node_modules`. Bun keys `patchedDependencies` on an exact `name@version`, so
|
|
116
|
+
those patches stop applying — silently, with no warning — the moment the
|
|
117
|
+
package version is bumped. Two consumer apps hit exactly that and shipped
|
|
118
|
+
fallback fonts without noticing. Colors already had `setColors` for this
|
|
119
|
+
reason; fonts now match.
|
|
120
|
+
|
|
121
|
+
Fully backward compatible: with no `setFonts` call, font resolution is
|
|
122
|
+
identical to 0.15.0.
|
|
123
|
+
|
|
6
124
|
## [0.15.0]
|
|
7
125
|
|
|
8
126
|
### Added
|
package/LLM_USAGE.md
CHANGED
|
@@ -164,6 +164,15 @@ React context, scoped keys win over the global brand inside it, and nested
|
|
|
164
164
|
scopes merge (inner wins, outer fills in). With no override at either layer,
|
|
165
165
|
`useTheme()` returns the base theme by reference.
|
|
166
166
|
|
|
167
|
+
Fonts and shape have matching global injection points. `setFonts({ families:
|
|
168
|
+
{ sansSerif?, serif?, mono? }, webWeightStrategy? })` replaces the bundled
|
|
169
|
+
faces (Inter/Georgia/system-mono) everywhere text renders; groups and weights
|
|
170
|
+
are partial, missing weights fall back to the group's `regular`, and an
|
|
171
|
+
overridden `sansSerif` makes `useResources` skip downloading Inter (call
|
|
172
|
+
`setFonts` before mount for the skip). `setShape({ button: { borderRadius?,
|
|
173
|
+
withShadow? } })` re-shapes Buttons globally (e.g. pill radius 9999, shadow
|
|
174
|
+
off). Per-instance props and caller `style` always win over both.
|
|
175
|
+
|
|
167
176
|
## Component Use-Case Index
|
|
168
177
|
|
|
169
178
|
Use this table before creating a new app-local primitive.
|
package/README.md
CHANGED
|
@@ -163,6 +163,92 @@ import { ThemeColorScope } from "@mrmeg/expo-ui/state";
|
|
|
163
163
|
</ThemeColorScope>
|
|
164
164
|
```
|
|
165
165
|
|
|
166
|
+
### Font overrides
|
|
167
|
+
|
|
168
|
+
The package bundles Inter (sans), Georgia (serif), and the platform's system
|
|
169
|
+
monospace (mono). Most apps ship their own faces — forward them with
|
|
170
|
+
`setFonts`, the font counterpart to `setColors`. Call it once at startup,
|
|
171
|
+
after your fonts are registered:
|
|
172
|
+
|
|
173
|
+
```tsx
|
|
174
|
+
import { useThemeStore } from "@mrmeg/expo-ui/state";
|
|
175
|
+
|
|
176
|
+
useThemeStore.getState().setFonts({
|
|
177
|
+
families: {
|
|
178
|
+
sansSerif: {
|
|
179
|
+
regular: "HankenGrotesk_400Regular",
|
|
180
|
+
medium: "HankenGrotesk_500Medium",
|
|
181
|
+
semibold: "HankenGrotesk_600SemiBold",
|
|
182
|
+
bold: "HankenGrotesk_700Bold",
|
|
183
|
+
},
|
|
184
|
+
serif: {
|
|
185
|
+
regular: "Newsreader_400Regular",
|
|
186
|
+
semibold: "Newsreader_600SemiBold",
|
|
187
|
+
},
|
|
188
|
+
mono: {
|
|
189
|
+
regular: "JetBrainsMono_400Regular",
|
|
190
|
+
medium: "JetBrainsMono_500Medium",
|
|
191
|
+
},
|
|
192
|
+
},
|
|
193
|
+
webWeightStrategy: "family",
|
|
194
|
+
});
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
Overrides are partial at both levels:
|
|
198
|
+
|
|
199
|
+
- **Per group** — pass `sansSerif` alone and `serif`/`mono` keep the package
|
|
200
|
+
defaults.
|
|
201
|
+
- **Per weight** — within an overridden group, weights you don't provide fall
|
|
202
|
+
back to that group's `regular` (never to a package face), so registering
|
|
203
|
+
only Regular + Medium is enough.
|
|
204
|
+
|
|
205
|
+
`setFonts({})` clears back to the defaults.
|
|
206
|
+
|
|
207
|
+
When `sansSerif` is overridden, `useResources` skips downloading the packaged
|
|
208
|
+
Inter faces entirely (native `.ttf`s and the web Google-Fonts stylesheet) —
|
|
209
|
+
your app owns loading its own families, and nothing would reference Inter.
|
|
210
|
+
For the skip to apply, call `setFonts` before the hook mounts (module scope,
|
|
211
|
+
or ahead of rendering the root); a later call still re-skins all text, it
|
|
212
|
+
just doesn't prevent the Inter download. The Feather icon font always loads.
|
|
213
|
+
|
|
214
|
+
**Pick the right `webWeightStrategy`** — this is the one non-obvious part:
|
|
215
|
+
|
|
216
|
+
| Strategy | Use when | Effect |
|
|
217
|
+
|---|---|---|
|
|
218
|
+
| `"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 |
|
|
219
|
+
| `"family"` | You load **per-weight faces via `expo-font`** | The family name alone carries the weight; the numeric `fontWeight` is suppressed |
|
|
220
|
+
|
|
221
|
+
If you load fonts through `expo-font` / `@expo-google-fonts`, choose
|
|
222
|
+
`"family"`. `expo-font` registers each weight as its own single-face family on
|
|
223
|
+
web as well as native, so emitting a numeric weight on top makes the browser
|
|
224
|
+
synthesise a *second* layer of bold over an already-bold face.
|
|
225
|
+
|
|
226
|
+
Native always behaves as `"family"` regardless of this setting.
|
|
227
|
+
|
|
228
|
+
> Prefer `setFonts` over patching `node_modules`. Bun keys
|
|
229
|
+
> `patchedDependencies` to an exact `name@version`, so a font patch silently
|
|
230
|
+
> stops applying the next time you bump the package — with no warning.
|
|
231
|
+
|
|
232
|
+
### Shape overrides
|
|
233
|
+
|
|
234
|
+
`setShape` is the geometry counterpart to `setColors`/`setFonts`, grouped per
|
|
235
|
+
component. It currently covers Button:
|
|
236
|
+
|
|
237
|
+
```tsx
|
|
238
|
+
import { useThemeStore } from "@mrmeg/expo-ui/state";
|
|
239
|
+
|
|
240
|
+
useThemeStore.getState().setShape({
|
|
241
|
+
button: {
|
|
242
|
+
borderRadius: 9999, // pill buttons everywhere; package default is 12
|
|
243
|
+
withShadow: false, // flatten the `default` preset; package default is true
|
|
244
|
+
},
|
|
245
|
+
});
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
Precedence is unchanged and caller-wins: a per-instance `style={{ borderRadius }}`
|
|
249
|
+
or `withShadow` prop still beats the global override, which beats the package
|
|
250
|
+
default. `setShape({})` clears back to the defaults.
|
|
251
|
+
|
|
166
252
|
Use `StyledText` for theme-aware text:
|
|
167
253
|
|
|
168
254
|
```tsx
|
|
@@ -179,7 +265,8 @@ Useful `StyledText` props:
|
|
|
179
265
|
- `semantic`: `title`, `heading`, `subheading`, `body`, `caption`, `label`
|
|
180
266
|
- `size`: `xs`, `sm`, `base`, `body`, `lg`, `xl`, `xxl`, `display`
|
|
181
267
|
- `fontWeight`: `light`, `regular`, `medium`, `semibold`, `bold`
|
|
182
|
-
- `variant`: `sansSerif`, `serif`
|
|
268
|
+
- `variant`: `sansSerif`, `serif`, `mono` (or the `MonoText` alias for code,
|
|
269
|
+
IDs, and tabular figures)
|
|
183
270
|
- `align`, `tx`, `txOptions`
|
|
184
271
|
- `selectable`: defaults to `true` for readable copy; package controls disable
|
|
185
272
|
selection for labels and other interactive chrome where accidental drag
|
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,
|
|
@@ -74,4 +75,5 @@ const createStyles = (theme) => StyleSheet.create({
|
|
|
74
75
|
userSelect: "none",
|
|
75
76
|
},
|
|
76
77
|
});
|
|
78
|
+
const themedStyles = createThemedStyles(createStyles);
|
|
77
79
|
export { Badge };
|
|
@@ -5,10 +5,12 @@ import { spacing } from "../constants/spacing.js";
|
|
|
5
5
|
import { StyledText } from "./StyledText.js";
|
|
6
6
|
import { Icon } from "./Icon.js";
|
|
7
7
|
import { TextColorContext, TextSelectabilityContext, TextStyleContext } from "./StyledText.context";
|
|
8
|
-
import { fontFamilies } from "../constants/fonts.js";
|
|
9
8
|
import { palette } from "../constants/colors.js";
|
|
10
9
|
import { useTheme } from "../hooks/useTheme.js";
|
|
10
|
+
import { useFontStyle } from "../hooks/useFontStyle.js";
|
|
11
11
|
import { useScalePress } from "../hooks/useScalePress.js";
|
|
12
|
+
import { useThemeStore } from "../state/themeStore.js";
|
|
13
|
+
import { createThemedStyles } from "../lib/themedStyles.js";
|
|
12
14
|
const SIZE_CONFIGS = {
|
|
13
15
|
sm: {
|
|
14
16
|
paddingVertical: spacing.xxs,
|
|
@@ -72,14 +74,24 @@ const getNativeHitSlop = (sizeConfig) => Math.ceil(Math.max(0, spacing.touchTarg
|
|
|
72
74
|
function ButtonRoot(props) {
|
|
73
75
|
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
76
|
const { theme, getContrastingColor, getFocusRingStyle, getShadowStyle } = useTheme();
|
|
75
|
-
const styles =
|
|
77
|
+
const styles = themedStyles(theme)[size];
|
|
76
78
|
const shadowStyle = getShadowStyle("subtle");
|
|
77
79
|
const sizeConfig = SIZE_CONFIGS[size];
|
|
80
|
+
// Host-app shape overrides (radius, default-preset shadow) — see `setShape`.
|
|
81
|
+
const buttonShape = useThemeStore((s) => s.shapeOverrides.button);
|
|
78
82
|
// Filled (`default`) buttons float by default; ghost/outline/link stay flat
|
|
79
83
|
// (transparent background reads oddly with a shadow), and destructive/secondary
|
|
80
|
-
// keep the old opt-in behavior.
|
|
81
|
-
|
|
84
|
+
// keep the old opt-in behavior. Precedence: the per-instance `withShadow`
|
|
85
|
+
// prop, then the host app's `setShape` flag, then the preset default.
|
|
86
|
+
const withShadow = withShadowProp
|
|
87
|
+
?? (preset === "default" ? buttonShape?.withShadow ?? true : false);
|
|
82
88
|
const focusRingStyle = getFocusRingStyle();
|
|
89
|
+
// Label typography resolves through the theme store so `setFonts` re-skins
|
|
90
|
+
// button labels along with everything else. Provided via TextStyleContext
|
|
91
|
+
// (after the static size styles) so nested Button.Text / StyledText children
|
|
92
|
+
// inherit it too, exactly like the previous hardcoded family did.
|
|
93
|
+
const labelFontStyle = useFontStyle("medium");
|
|
94
|
+
const labelTextStyle = useMemo(() => [styles.text, { fontFamily: labelFontStyle.fontFamily, fontWeight: labelFontStyle.fontWeight }], [styles.text, labelFontStyle.fontFamily, labelFontStyle.fontWeight]);
|
|
83
95
|
// Pre-compute background color for contrast calculation
|
|
84
96
|
// Always flatten to handle both array styles (from Slot) and RegisteredStyle
|
|
85
97
|
const flattenedStyle = styleOverride ? StyleSheet.flatten(styleOverride) : undefined;
|
|
@@ -169,8 +181,12 @@ function ButtonRoot(props) {
|
|
|
169
181
|
return nextWidth;
|
|
170
182
|
});
|
|
171
183
|
}, [fullWidth, loading]);
|
|
172
|
-
return (_jsx(TextColorContext.Provider, { value: textColor, children: _jsx(TextSelectabilityContext.Provider, { value: false, children: _jsx(TextStyleContext.Provider, { value:
|
|
184
|
+
return (_jsx(TextColorContext.Provider, { value: textColor, children: _jsx(TextSelectabilityContext.Provider, { value: false, children: _jsx(TextStyleContext.Provider, { value: labelTextStyle, children: _jsx(Pressable, { accessibilityRole: "button", accessibilityState: { disabled: !!isDisabled, busy: loading }, ...rest, onPressIn: handlePressIn, onPressOut: handlePressOut, onFocus: showFocusRing, onBlur: hideFocusRing, 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: [
|
|
173
185
|
styles.button,
|
|
186
|
+
// Host-app radius override (see `setShape`). Sits right
|
|
187
|
+
// after the static radius so presets, state styles, and —
|
|
188
|
+
// critically — the caller `style` spread below still win.
|
|
189
|
+
buttonShape?.borderRadius !== undefined && { borderRadius: buttonShape.borderRadius },
|
|
174
190
|
preset === "default" && styles.buttonDefault,
|
|
175
191
|
preset === "outline" && styles.buttonOutline,
|
|
176
192
|
preset === "ghost" && styles.buttonGhost,
|
|
@@ -190,7 +206,7 @@ function ButtonRoot(props) {
|
|
|
190
206
|
// Spread array styles from Slot to prevent nested arrays on web
|
|
191
207
|
...(Array.isArray(styleOverride) ? styleOverride : [styleOverride]),
|
|
192
208
|
], 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: [
|
|
193
|
-
|
|
209
|
+
labelTextStyle,
|
|
194
210
|
state.pressed && styles.pressedText,
|
|
195
211
|
state.pressed && pressedTextStyleOverride,
|
|
196
212
|
isDisabled && disabledTextStyleOverride,
|
|
@@ -282,15 +298,11 @@ const createStyles = (theme, size) => {
|
|
|
282
298
|
width: "100%",
|
|
283
299
|
},
|
|
284
300
|
text: {
|
|
285
|
-
// Button labels render at medium weight.
|
|
286
|
-
//
|
|
287
|
-
//
|
|
288
|
-
//
|
|
289
|
-
// fontWeight to pick the right @font-face variant. (Same rule as
|
|
290
|
-
// StyledText's getFontFamilyWeight / WEB_FONT_WEIGHTS.)
|
|
291
|
-
fontFamily: fontFamilies.sansSerif.medium,
|
|
301
|
+
// Button labels render at medium weight. The fontFamily (+ numeric
|
|
302
|
+
// fontWeight on web) is NOT set here: it's resolved per-render through
|
|
303
|
+
// the theme store (`useFontStyle("medium")` in ButtonRoot) and layered
|
|
304
|
+
// right after this style, so `setFonts` overrides reach button labels.
|
|
292
305
|
fontSize: sizeConfig.fontSize,
|
|
293
|
-
...(Platform.OS === "web" && { fontWeight: "500" }),
|
|
294
306
|
textAlign: "center",
|
|
295
307
|
lineHeight: sizeConfig.fontSize * 1.4,
|
|
296
308
|
flexShrink: 0,
|
|
@@ -321,6 +333,11 @@ const createStyles = (theme, size) => {
|
|
|
321
333
|
},
|
|
322
334
|
});
|
|
323
335
|
};
|
|
336
|
+
const themedStyles = createThemedStyles((theme) => ({
|
|
337
|
+
sm: createStyles(theme, "sm"),
|
|
338
|
+
md: createStyles(theme, "md"),
|
|
339
|
+
lg: createStyles(theme, "lg"),
|
|
340
|
+
}));
|
|
324
341
|
/**
|
|
325
342
|
* Button with explicit subcomponents.
|
|
326
343
|
* - `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,11 +1,11 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useMemo } from "react";
|
|
3
2
|
import { View, StyleSheet } from "react-native";
|
|
4
3
|
import { SansSerifBoldText, SansSerifText } from "./StyledText.js";
|
|
5
4
|
import { Button } from "./Button.js";
|
|
6
5
|
import { Icon } from "./Icon.js";
|
|
7
6
|
import { useTheme } from "../hooks/useTheme.js";
|
|
8
7
|
import { spacing } from "../constants/spacing.js";
|
|
8
|
+
import { createThemedStyles } from "../lib/themedStyles.js";
|
|
9
9
|
/**
|
|
10
10
|
* EmptyState Component
|
|
11
11
|
*
|
|
@@ -25,7 +25,7 @@ import { spacing } from "../constants/spacing.js";
|
|
|
25
25
|
*/
|
|
26
26
|
export function EmptyState({ icon, iconSize = spacing.iconMd, title, description, actionLabel, onAction, actionPreset = "default", style, children, bordered = false, }) {
|
|
27
27
|
const { theme } = useTheme();
|
|
28
|
-
const styles =
|
|
28
|
+
const styles = themedStyles(theme);
|
|
29
29
|
return (_jsxs(View, { style: [styles.container, bordered && styles.bordered, style], children: [!!icon && (_jsx(View, { style: styles.iconWrapper, children: _jsx(Icon, { name: icon, size: iconSize, color: theme.colors.mutedForeground }) })), _jsx(SansSerifBoldText, { selectable: false, size: "lg", style: styles.title, children: title }), !!description && (_jsx(SansSerifText, { selectable: false, size: "base", style: styles.description, children: description })), children, !!actionLabel && onAction && (_jsx(Button, { preset: actionPreset, onPress: onAction, text: actionLabel, style: styles.action }))] }));
|
|
30
30
|
}
|
|
31
31
|
const MEDIA_SIZE = 56;
|
|
@@ -74,3 +74,4 @@ const createStyles = (theme) => StyleSheet.create({
|
|
|
74
74
|
alignSelf: "center",
|
|
75
75
|
},
|
|
76
76
|
});
|
|
77
|
+
const themedStyles = createThemedStyles(createStyles);
|
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import {
|
|
2
|
+
import { useRef, useState, useCallback } from "react";
|
|
3
3
|
import { View, TextInput as RNTextInput, Pressable, StyleSheet, Platform, } from "react-native";
|
|
4
4
|
import { useTheme } from "../hooks/useTheme.js";
|
|
5
5
|
import { spacing } from "../constants/spacing.js";
|
|
6
|
-
import { fontFamilies } from "../constants/fonts.js";
|
|
7
6
|
import { StyledText } from "./StyledText.js";
|
|
7
|
+
import { createThemedStyles } from "../lib/themedStyles.js";
|
|
8
8
|
const CELL_WIDTH = 36;
|
|
9
9
|
const CELL_HEIGHT = 40;
|
|
10
10
|
const CELL_FONT_SIZE = 20;
|
|
11
|
-
const CELL_FONT_WEIGHT = "600";
|
|
12
11
|
const BULLET = "\u2022";
|
|
13
12
|
/**
|
|
14
13
|
* OTP/verification code input with individual character cells.
|
|
@@ -30,7 +29,7 @@ function InputOTP({ length = 6, value = "", onChangeText, onComplete, error = fa
|
|
|
30
29
|
const { theme } = useTheme();
|
|
31
30
|
const inputRef = useRef(null);
|
|
32
31
|
const [focused, setFocused] = useState(false);
|
|
33
|
-
const styles =
|
|
32
|
+
const styles = themedStyles(theme);
|
|
34
33
|
const hasError = error || !!errorText;
|
|
35
34
|
// Active cell index: next empty cell, or last cell when full
|
|
36
35
|
const activeIndex = Math.min(value.length, length - 1);
|
|
@@ -97,14 +96,8 @@ function OTPCell({ index, total, char, isActive, hasError, disabled, theme, onPr
|
|
|
97
96
|
opacity: disabled ? 0.5 : 1,
|
|
98
97
|
borderColor,
|
|
99
98
|
},
|
|
100
|
-
], children: _jsx(StyledText, { selectable: false, style: {
|
|
99
|
+
], children: _jsx(StyledText, { selectable: false, fontWeight: "semibold", style: {
|
|
101
100
|
fontSize: CELL_FONT_SIZE,
|
|
102
|
-
// Semibold weight: the family carries it on native (a real static
|
|
103
|
-
// Inter_600SemiBold file); web shares one "Inter" family across
|
|
104
|
-
// weights, so it needs the numeric fontWeight to pick the right
|
|
105
|
-
// @font-face variant (same rule as StyledText's WEB_FONT_WEIGHTS).
|
|
106
|
-
...(Platform.OS === "web" && { fontWeight: CELL_FONT_WEIGHT }),
|
|
107
|
-
fontFamily: fontFamilies.sansSerif.semibold,
|
|
108
101
|
color: theme.colors.text,
|
|
109
102
|
textAlign: "center",
|
|
110
103
|
lineHeight: CELL_FONT_SIZE * 1.2,
|
|
@@ -126,10 +119,12 @@ const createStyles = (theme) => StyleSheet.create({
|
|
|
126
119
|
gap: spacing.sm,
|
|
127
120
|
},
|
|
128
121
|
errorText: {
|
|
122
|
+
// Regular weight — StyledText's default resolves the family through the
|
|
123
|
+
// theme store, so no hardcoded fontFamily here.
|
|
129
124
|
fontSize: 12,
|
|
130
|
-
fontFamily: fontFamilies.sansSerif.regular,
|
|
131
125
|
color: theme.colors.destructive,
|
|
132
126
|
marginTop: spacing.xs,
|
|
133
127
|
},
|
|
134
128
|
});
|
|
129
|
+
const themedStyles = createThemedStyles(createStyles);
|
|
135
130
|
export { InputOTP };
|
package/dist/components/Label.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useMemo } from "react";
|
|
3
2
|
import { StyleSheet, Platform } from "react-native";
|
|
4
3
|
import * as LabelPrimitive from "@rn-primitives/label";
|
|
5
4
|
import { useTheme } from "../hooks/useTheme.js";
|
|
5
|
+
import { useFontStyle } from "../hooks/useFontStyle.js";
|
|
6
6
|
import { spacing } from "../constants/spacing.js";
|
|
7
|
-
import { fontFamilies } from "../constants/fonts.js";
|
|
8
7
|
import { StyledText } from "./StyledText.js";
|
|
8
|
+
import { createThemedStyles } from "../lib/themedStyles.js";
|
|
9
9
|
const SIZE_CONFIGS = {
|
|
10
10
|
sm: { fontSize: 12 },
|
|
11
11
|
md: { fontSize: 14 },
|
|
@@ -37,10 +37,14 @@ const SIZE_CONFIGS = {
|
|
|
37
37
|
*/
|
|
38
38
|
export function Label({ children, nativeID, required, size = "md", error, disabled, style, onPress, }) {
|
|
39
39
|
const { theme } = useTheme();
|
|
40
|
-
const styles =
|
|
40
|
+
const styles = themedStyles(theme);
|
|
41
41
|
const sizeConfig = SIZE_CONFIGS[size];
|
|
42
|
+
// Resolved through the theme store so `setFonts` overrides apply; identical
|
|
43
|
+
// to the old hardcoded medium family when no override is set.
|
|
44
|
+
const labelFont = useFontStyle("medium");
|
|
42
45
|
const textStyle = StyleSheet.flatten([
|
|
43
46
|
styles.label,
|
|
47
|
+
labelFont,
|
|
44
48
|
{ fontSize: sizeConfig.fontSize },
|
|
45
49
|
error && styles.errorLabel,
|
|
46
50
|
disabled && styles.disabledLabel,
|
|
@@ -49,26 +53,23 @@ export function Label({ children, nativeID, required, size = "md", error, disabl
|
|
|
49
53
|
return (_jsx(LabelPrimitive.Root, { nativeID: nativeID, onPress: onPress, style: {
|
|
50
54
|
...styles.root,
|
|
51
55
|
...(Platform.OS === "web" && onPress && { cursor: "pointer" }),
|
|
52
|
-
}, children: _jsxs(LabelPrimitive.Text, { style: textStyle, children: [children, required && _jsx(StyledText, { selectable: false, style: styles.required, children: " *" })] }) }));
|
|
56
|
+
}, children: _jsxs(LabelPrimitive.Text, { style: textStyle, children: [children, required && _jsx(StyledText, { selectable: false, fontWeight: "bold", style: styles.required, children: " *" })] }) }));
|
|
53
57
|
}
|
|
54
58
|
const createStyles = (theme) => StyleSheet.create({
|
|
55
59
|
root: {
|
|
56
60
|
marginBottom: spacing.xs,
|
|
57
61
|
},
|
|
58
62
|
label: {
|
|
59
|
-
// Medium weight
|
|
60
|
-
//
|
|
61
|
-
//
|
|
62
|
-
// variant. Setting fontWeight on native would faux-bold on top of the
|
|
63
|
-
// already-medium file (same rule as StyledText's WEB_FONT_WEIGHTS).
|
|
64
|
-
fontFamily: fontFamilies.sansSerif.medium,
|
|
65
|
-
...(Platform.OS === "web" && { fontWeight: "500" }),
|
|
63
|
+
// Medium weight — the fontFamily (+ numeric fontWeight on web) is
|
|
64
|
+
// resolved per-render through the theme store (`useFontStyle("medium")`
|
|
65
|
+
// in Label) so `setFonts` overrides apply here too.
|
|
66
66
|
color: theme.colors.text,
|
|
67
67
|
userSelect: "none",
|
|
68
68
|
},
|
|
69
69
|
required: {
|
|
70
|
+
// Bold weight comes from the StyledText `fontWeight="bold"` prop, which
|
|
71
|
+
// resolves the family through the theme store.
|
|
70
72
|
color: theme.colors.destructive,
|
|
71
|
-
fontFamily: fontFamilies.sansSerif.bold,
|
|
72
73
|
userSelect: "none",
|
|
73
74
|
},
|
|
74
75
|
errorLabel: {
|
|
@@ -78,3 +79,4 @@ const createStyles = (theme) => StyleSheet.create({
|
|
|
78
79
|
opacity: 0.6,
|
|
79
80
|
},
|
|
80
81
|
});
|
|
82
|
+
const themedStyles = createThemedStyles(createStyles);
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import {
|
|
2
|
+
import { useCallback, use, useEffect, useEffectEvent, useRef } from "react";
|
|
3
3
|
import { Animated, Easing, StyleSheet, View, ActivityIndicator, Pressable, Platform } from "react-native";
|
|
4
4
|
import { SafeAreaInsetsContext } from "react-native-safe-area-context";
|
|
5
5
|
import { Icon } from "./Icon.js";
|
|
@@ -10,6 +10,7 @@ import { durations } from "../constants/motion.js";
|
|
|
10
10
|
import { StyledText } from "./StyledText.js";
|
|
11
11
|
import { palette } from "../constants/colors.js";
|
|
12
12
|
import { translateText } from "../lib/i18n.js";
|
|
13
|
+
import { createThemedStyles } from "../lib/themedStyles.js";
|
|
13
14
|
import { globalUIStore } from "../state/globalUIStore.js";
|
|
14
15
|
const timingIn = { duration: durations.fast, easing: Easing.out(Easing.quad), useNativeDriver: true };
|
|
15
16
|
const timingOut = { duration: durations.instant, easing: Easing.in(Easing.quad), useNativeDriver: true };
|
|
@@ -36,7 +37,7 @@ export const Notification = () => {
|
|
|
36
37
|
const reduceMotion = useReducedMotion();
|
|
37
38
|
const insets = use(SafeAreaInsetsContext);
|
|
38
39
|
const { alert, hide } = globalUIStore();
|
|
39
|
-
const styles =
|
|
40
|
+
const styles = themedStyles(theme);
|
|
40
41
|
const position = alert?.position ?? "top";
|
|
41
42
|
const isBottom = position === "bottom";
|
|
42
43
|
// Just opacity + translateY — no scale (scale = bouncy feel)
|
|
@@ -301,3 +302,4 @@ const createStyles = (theme) => StyleSheet.create({
|
|
|
301
302
|
...(Platform.OS === "web" && { cursor: "pointer" }),
|
|
302
303
|
},
|
|
303
304
|
});
|
|
305
|
+
const themedStyles = createThemedStyles(createStyles);
|
|
@@ -4,6 +4,7 @@ import { View, StyleSheet, Animated } from "react-native";
|
|
|
4
4
|
import { useTheme } from "../hooks/useTheme.js";
|
|
5
5
|
import { useReducedMotion } from "../hooks/useReduceMotion.js";
|
|
6
6
|
import { spacing } from "../constants/spacing.js";
|
|
7
|
+
import { createThemedStyles } from "../lib/themedStyles.js";
|
|
7
8
|
/**
|
|
8
9
|
* Skeleton Component
|
|
9
10
|
*
|
|
@@ -72,7 +73,7 @@ export function SkeletonAvatar({ size = 40, style }) {
|
|
|
72
73
|
*/
|
|
73
74
|
export function SkeletonCard({ showImage = true, imageHeight = 140, showAvatar = true, textLines = 3, style, }) {
|
|
74
75
|
const { theme, getShadowStyle } = useTheme();
|
|
75
|
-
const styles =
|
|
76
|
+
const styles = themedStyles(theme);
|
|
76
77
|
return (_jsxs(View, { style: [styles.card, getShadowStyle("subtle"), style], children: [showImage && (_jsx(Skeleton, { width: "100%", height: imageHeight, borderRadius: 0 })), _jsxs(View, { style: styles.body, children: [showAvatar && (_jsxs(View, { style: styles.avatarRow, children: [_jsx(SkeletonAvatar, { size: 36 }), _jsxs(View, { style: styles.avatarText, children: [_jsx(Skeleton, { width: "50%", height: 14 }), _jsx(Skeleton, { width: "30%", height: 12 })] })] })), _jsx(SkeletonText, { lines: textLines })] })] }));
|
|
77
78
|
}
|
|
78
79
|
const createCardStyles = (theme) => StyleSheet.create({
|
|
@@ -97,3 +98,4 @@ const createCardStyles = (theme) => StyleSheet.create({
|
|
|
97
98
|
gap: spacing.xs,
|
|
98
99
|
},
|
|
99
100
|
});
|
|
101
|
+
const themedStyles = createThemedStyles(createCardStyles);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { type Ref } from "react";
|
|
2
2
|
import { Text as RNText, TextProps as RNTextProps } from "react-native";
|
|
3
|
+
import { type FontVariant } from "../constants/fonts";
|
|
3
4
|
/**
|
|
4
5
|
* Font size variants for the type scale (Inter sans-serif / Georgia serif)
|
|
5
6
|
*/
|
|
@@ -18,9 +19,10 @@ export type FontWeight = "light" | "regular" | "medium" | "semibold" | "bold";
|
|
|
18
19
|
export type TextAlign = "left" | "center" | "right" | "justify" | "auto";
|
|
19
20
|
export type TextProps = RNTextProps & {
|
|
20
21
|
/**
|
|
21
|
-
* Font variant - serif (Georgia)
|
|
22
|
+
* Font variant - serif (Georgia), sans-serif (Inter), or mono (the
|
|
23
|
+
* platform's system monospace). Host apps swap the families via `setFonts`.
|
|
22
24
|
*/
|
|
23
|
-
variant?:
|
|
25
|
+
variant?: FontVariant;
|
|
24
26
|
/**
|
|
25
27
|
* Font weight - light, regular, medium, semibold, bold
|
|
26
28
|
*/
|
|
@@ -83,6 +85,13 @@ export declare function SerifText(props: TextProps): import("react").JSX.Element
|
|
|
83
85
|
* Uses the sans-serif font family (Inter)
|
|
84
86
|
*/
|
|
85
87
|
export declare function SansSerifText(props: TextProps): import("react").JSX.Element;
|
|
88
|
+
/**
|
|
89
|
+
* Mono Text Component
|
|
90
|
+
* Uses the monospace font family (the platform's system monospace by
|
|
91
|
+
* default; host apps swap in their own via `setFonts`) — for code, IDs,
|
|
92
|
+
* and tabular figures
|
|
93
|
+
*/
|
|
94
|
+
export declare function MonoText(props: TextProps): import("react").JSX.Element;
|
|
86
95
|
/**
|
|
87
96
|
* Serif Bold Text Component
|
|
88
97
|
* Uses the serif font family — Georgia's only shipped weight is regular
|