@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.
Files changed (61) hide show
  1. package/CHANGELOG.md +81 -0
  2. package/README.md +50 -6
  3. package/dist/components/Accordion.d.ts +1 -1
  4. package/dist/components/Accordion.js +25 -19
  5. package/dist/components/Badge.js +10 -7
  6. package/dist/components/BottomSheet.js +21 -19
  7. package/dist/components/Button.d.ts +2 -1
  8. package/dist/components/Button.js +22 -6
  9. package/dist/components/Card.js +5 -3
  10. package/dist/components/Checkbox.js +49 -19
  11. package/dist/components/Collapsible.d.ts +1 -1
  12. package/dist/components/Collapsible.js +22 -16
  13. package/dist/components/Drawer.js +8 -7
  14. package/dist/components/DropdownMenu.d.ts +1 -1
  15. package/dist/components/DropdownMenu.js +80 -59
  16. package/dist/components/EmptyState.d.ts +7 -1
  17. package/dist/components/EmptyState.js +26 -9
  18. package/dist/components/InputOTP.js +10 -4
  19. package/dist/components/Item.d.ts +93 -0
  20. package/dist/components/Item.js +125 -0
  21. package/dist/components/Label.js +10 -4
  22. package/dist/components/Notification.js +14 -24
  23. package/dist/components/Popover.d.ts +3 -18
  24. package/dist/components/Popover.js +10 -6
  25. package/dist/components/RadioGroup.js +49 -22
  26. package/dist/components/SectionHeader.d.ts +42 -0
  27. package/dist/components/SectionHeader.js +41 -0
  28. package/dist/components/Select.js +73 -41
  29. package/dist/components/Skeleton.js +5 -3
  30. package/dist/components/StatCard.d.ts +45 -0
  31. package/dist/components/StatCard.js +41 -0
  32. package/dist/components/StyledText.d.ts +20 -10
  33. package/dist/components/StyledText.js +85 -29
  34. package/dist/components/Switch.js +84 -57
  35. package/dist/components/Tabs.js +43 -12
  36. package/dist/components/TextInput.js +17 -11
  37. package/dist/components/Toggle.js +69 -42
  38. package/dist/components/ToggleGroup.js +82 -55
  39. package/dist/components/index.d.ts +3 -0
  40. package/dist/components/index.js +3 -0
  41. package/dist/constants/fonts.d.ts +44 -5
  42. package/dist/constants/fonts.js +42 -12
  43. package/dist/constants/index.d.ts +1 -0
  44. package/dist/constants/index.js +1 -0
  45. package/dist/constants/motion.d.ts +24 -0
  46. package/dist/constants/motion.js +24 -0
  47. package/dist/constants/spacing.d.ts +7 -7
  48. package/dist/constants/spacing.js +7 -7
  49. package/dist/hooks/useResources.d.ts +4 -2
  50. package/dist/hooks/useResources.js +30 -8
  51. package/dist/hooks/useStaggeredEntrance.d.ts +1 -1
  52. package/dist/hooks/useStaggeredEntrance.js +2 -1
  53. package/dist/hooks/useTheme.d.ts +1 -1
  54. package/dist/hooks/useTheme.js +53 -16
  55. package/dist/lib/index.d.ts +1 -0
  56. package/dist/lib/index.js +1 -0
  57. package/dist/lib/themedStyles.d.ts +21 -0
  58. package/dist/lib/themedStyles.js +33 -0
  59. package/dist/state/themeStore.d.ts +17 -0
  60. package/dist/state/themeStore.js +6 -0
  61. package/package.json +20 -19
@@ -1,11 +1,12 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
- import React from "react";
2
+ import React, { useState } from "react";
3
3
  import { Icon } from "./Icon.js";
4
4
  import { TextClassContext, TextColorContext, TextSelectabilityContext } from "./StyledText.context";
5
5
  import { useTheme } from "../hooks/useTheme.js";
6
6
  import { spacing } from "../constants/spacing.js";
7
+ import { useScalePress } from "../hooks/useScalePress.js";
7
8
  import * as TogglePrimitive from "@rn-primitives/toggle";
8
- import { Platform, StyleSheet, ActivityIndicator } from "react-native";
9
+ import { Platform, StyleSheet, ActivityIndicator, Animated } from "react-native";
9
10
  import { palette } from "../constants/colors.js";
10
11
  const DEFAULT_HIT_SLOP = 8;
11
12
  // Size configurations
@@ -73,8 +74,10 @@ const TOGGLE_SIZES = {
73
74
  * ```
74
75
  */
75
76
  function Toggle({ variant = "default", size = "default", shape = "default", loading = false, iconOnly = false, style: styleOverride, ...props }) {
76
- const { theme, getContrastingColor, withAlpha } = useTheme();
77
+ const { theme, getContrastingColor, getFocusRingStyle, withAlpha } = useTheme();
77
78
  const sizeConfig = TOGGLE_SIZES[size];
79
+ const focusRingStyle = getFocusRingStyle();
80
+ const [focused, setFocused] = useState(false);
78
81
  // Calculate text color based on state and variant
79
82
  const getTextColor = () => {
80
83
  if (props.pressed) {
@@ -101,45 +104,69 @@ function Toggle({ variant = "default", size = "default", shape = "default", load
101
104
  const flattenedStyle = styleOverride ? StyleSheet.flatten(styleOverride) : undefined;
102
105
  const isDisabled = props.disabled || loading;
103
106
  const children = props.children;
104
- return (_jsx(TextColorContext.Provider, { value: textColor, children: _jsx(TextClassContext.Provider, { value: "", children: _jsx(TogglePrimitive.Root, { ...props, disabled: isDisabled, style: {
105
- ...styles.root,
106
- height: sizeConfig.height,
107
- minWidth: sizeConfig.minWidth,
108
- paddingHorizontal: iconOnly ? sizeConfig.height / 2 - sizeConfig.iconSize / 2 : sizeConfig.paddingHorizontal,
109
- borderRadius: getBorderRadius(),
110
- // Base variant styles
111
- ...(variant === "default" && !props.pressed && {
112
- backgroundColor: "transparent",
113
- borderColor: theme.colors.border,
114
- }),
115
- ...(variant === "default" && props.pressed && {
116
- backgroundColor: withAlpha(theme.colors.primary, 0.1),
117
- borderColor: theme.colors.primary,
118
- }),
119
- // Outline variant styles
120
- ...(variant === "outline" && !props.pressed && {
121
- backgroundColor: "transparent",
122
- borderColor: theme.colors.primary,
123
- }),
124
- ...(variant === "outline" && props.pressed && {
125
- backgroundColor: theme.colors.primary,
126
- borderColor: theme.colors.primary,
127
- }),
128
- // Disabled state
129
- opacity: isDisabled ? 0.5 : 1,
130
- // Web-specific styles
131
- ...(Platform.OS === "web" && {
132
- cursor: isDisabled ? "not-allowed" : "pointer",
133
- transition: "all 150ms",
134
- userSelect: "none",
135
- }),
136
- // Apply custom style override
137
- ...(flattenedStyle || {}),
138
- }, hitSlop: DEFAULT_HIT_SLOP, accessibilityRole: "button", accessibilityState: {
139
- selected: props.pressed,
140
- disabled: !!isDisabled,
141
- busy: loading,
142
- }, children: loading ? (_jsx(TextSelectabilityContext.Provider, { value: false, children: _jsx(ActivityIndicator, { size: "small", color: textColor }) })) : typeof children === "function" ? ((state) => (_jsx(TextSelectabilityContext.Provider, { value: false, children: children(state) }))) : (_jsx(TextSelectabilityContext.Provider, { value: false, children: children })) }) }) }));
107
+ const { animatedStyle: scaleStyle, pressHandlers } = useScalePress({
108
+ disabled: !!isDisabled,
109
+ scaleTo: 0.92,
110
+ haptic: false,
111
+ });
112
+ const showFocusRing = (event) => {
113
+ let ringVisible = true;
114
+ if (Platform.OS === "web") {
115
+ const target = event?.nativeEvent?.target;
116
+ if (target && typeof target.matches === "function") {
117
+ try {
118
+ ringVisible = target.matches(":focus-visible");
119
+ }
120
+ catch {
121
+ ringVisible = true;
122
+ }
123
+ }
124
+ }
125
+ setFocused(ringVisible);
126
+ };
127
+ const hideFocusRing = () => {
128
+ setFocused(false);
129
+ };
130
+ return (_jsx(TextColorContext.Provider, { value: textColor, children: _jsx(TextClassContext.Provider, { value: "", children: _jsx(Animated.View, { style: scaleStyle, children: _jsx(TogglePrimitive.Root, { ...props, disabled: isDisabled, onPressIn: pressHandlers.onPressIn, onPressOut: pressHandlers.onPressOut, onFocus: showFocusRing, onBlur: hideFocusRing, style: {
131
+ ...styles.root,
132
+ height: sizeConfig.height,
133
+ minWidth: sizeConfig.minWidth,
134
+ paddingHorizontal: iconOnly ? sizeConfig.height / 2 - sizeConfig.iconSize / 2 : sizeConfig.paddingHorizontal,
135
+ borderRadius: getBorderRadius(),
136
+ // Base variant styles
137
+ ...(variant === "default" && !props.pressed && {
138
+ backgroundColor: "transparent",
139
+ borderColor: theme.colors.border,
140
+ }),
141
+ ...(variant === "default" && props.pressed && {
142
+ backgroundColor: withAlpha(theme.colors.primary, 0.1),
143
+ borderColor: theme.colors.primary,
144
+ }),
145
+ // Outline variant styles
146
+ ...(variant === "outline" && !props.pressed && {
147
+ backgroundColor: "transparent",
148
+ borderColor: theme.colors.primary,
149
+ }),
150
+ ...(variant === "outline" && props.pressed && {
151
+ backgroundColor: theme.colors.primary,
152
+ borderColor: theme.colors.primary,
153
+ }),
154
+ // Disabled state
155
+ opacity: isDisabled ? 0.5 : 1,
156
+ // Web-specific styles
157
+ ...(Platform.OS === "web" && {
158
+ cursor: isDisabled ? "not-allowed" : "pointer",
159
+ transition: "all 150ms",
160
+ userSelect: "none",
161
+ }),
162
+ ...(focused && !isDisabled ? focusRingStyle : null),
163
+ // Apply custom style override
164
+ ...(flattenedStyle || {}),
165
+ }, hitSlop: DEFAULT_HIT_SLOP, accessibilityRole: "button", accessibilityState: {
166
+ selected: props.pressed,
167
+ disabled: !!isDisabled,
168
+ busy: loading,
169
+ }, children: loading ? (_jsx(TextSelectabilityContext.Provider, { value: false, children: _jsx(ActivityIndicator, { size: "small", color: textColor }) })) : typeof children === "function" ? ((state) => (_jsx(TextSelectabilityContext.Provider, { value: false, children: children(state) }))) : (_jsx(TextSelectabilityContext.Provider, { value: false, children: children })) }) }) }) }));
143
170
  }
144
171
  function ToggleIcon({ name, size, color }) {
145
172
  const contextColor = React.use(TextColorContext);
@@ -3,9 +3,10 @@ import { Icon } from "./Icon.js";
3
3
  import { TextClassContext, TextColorContext, 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 ToggleGroupPrimitive from "@rn-primitives/toggle-group";
7
8
  import * as React from "react";
8
- import { Platform, StyleSheet } from "react-native";
9
+ import { Animated, Platform, StyleSheet } from "react-native";
9
10
  const DEFAULT_HIT_SLOP = 8;
10
11
  // Size configurations (same as Toggle)
11
12
  const TOGGLE_GROUP_SIZES = {
@@ -101,10 +102,35 @@ function ToggleGroup({ variant = "default", size = "default", children, ...props
101
102
  * Position (first/last) is automatically detected for rounded corners
102
103
  */
103
104
  function ToggleGroupItem({ isFirst = false, isLast = false, children, ...props }) {
104
- const { theme, withAlpha, getContrastingColor } = useTheme();
105
+ const { theme, withAlpha, getContrastingColor, getFocusRingStyle } = useTheme();
105
106
  const context = useToggleGroupContext();
106
107
  const { value: groupValue } = ToggleGroupPrimitive.useRootContext();
107
108
  const sizeConfig = TOGGLE_GROUP_SIZES[context.size];
109
+ const focusRingStyle = getFocusRingStyle();
110
+ const [focused, setFocused] = React.useState(false);
111
+ const { animatedStyle: scaleStyle, pressHandlers } = useScalePress({
112
+ disabled: !!props.disabled,
113
+ scaleTo: 0.97,
114
+ haptic: false,
115
+ });
116
+ const showFocusRing = (event) => {
117
+ let ringVisible = true;
118
+ if (Platform.OS === "web") {
119
+ const target = event?.nativeEvent?.target;
120
+ if (target && typeof target.matches === "function") {
121
+ try {
122
+ ringVisible = target.matches(":focus-visible");
123
+ }
124
+ catch {
125
+ ringVisible = true;
126
+ }
127
+ }
128
+ }
129
+ setFocused(ringVisible);
130
+ };
131
+ const hideFocusRing = () => {
132
+ setFocused(false);
133
+ };
108
134
  // Check if this item is selected
109
135
  const isSelected = ToggleGroupPrimitive.utils.getIsSelected(groupValue, props.value);
110
136
  // Determine the actual background color for this item
@@ -119,60 +145,61 @@ function ToggleGroupItem({ isFirst = false, isLast = false, children, ...props }
119
145
  const textColor = isSelected
120
146
  ? getContrastingColor(itemBgColor, theme.colors.foreground, theme.colors.background)
121
147
  : theme.colors.foreground;
122
- return (_jsx(TextColorContext.Provider, { value: textColor, children: _jsx(TextClassContext.Provider, { value: "", children: _jsx(ToggleGroupPrimitive.Item, { ...props, style: {
123
- ...styles.item,
124
- height: sizeConfig.height,
125
- minWidth: sizeConfig.minWidth,
126
- paddingHorizontal: sizeConfig.paddingHorizontal,
127
- // Base variant styles
128
- ...(context.variant === "default" && !isSelected && {
129
- backgroundColor: "transparent",
130
- borderColor: theme.colors.border,
131
- }),
132
- ...(context.variant === "default" && isSelected && {
133
- backgroundColor: withAlpha(theme.colors.primary, 0.1),
134
- borderColor: theme.colors.primary,
135
- }),
136
- // Outline variant styles
137
- ...(context.variant === "outline" && !isSelected && {
138
- backgroundColor: "transparent",
139
- borderColor: theme.colors.border,
140
- }),
141
- ...(context.variant === "outline" && isSelected && {
142
- backgroundColor: theme.colors.primary,
143
- borderColor: theme.colors.primary,
144
- }),
145
- // Remove borders between items (except first)
146
- ...(context.variant === "outline" && !isFirst && {
147
- borderLeftWidth: 0,
148
- }),
149
- // Border radius handling
150
- borderRadius: 0, // Remove all radius by default
151
- ...(isFirst && {
152
- borderTopLeftRadius: spacing.radiusMd,
153
- borderBottomLeftRadius: spacing.radiusMd,
154
- }),
155
- ...(isLast && {
156
- borderTopRightRadius: spacing.radiusMd,
157
- borderBottomRightRadius: spacing.radiusMd,
158
- }),
159
- // Single item (both first and last)
160
- ...(isFirst && isLast && {
161
- borderRadius: spacing.radiusMd,
162
- }),
163
- // Disabled state
164
- opacity: props.disabled ? 0.5 : 1,
165
- // Web-specific styles
166
- ...(Platform.OS === "web" && {
167
- cursor: props.disabled ? "not-allowed" : "pointer",
168
- transition: "all 150ms",
169
- userSelect: "none",
170
- // Ensure proper z-index on focus
171
- ...(isSelected && {
172
- zIndex: 10,
148
+ return (_jsx(TextColorContext.Provider, { value: textColor, children: _jsx(TextClassContext.Provider, { value: "", children: _jsx(Animated.View, { style: scaleStyle, children: _jsx(ToggleGroupPrimitive.Item, { ...props, onPressIn: pressHandlers.onPressIn, onPressOut: pressHandlers.onPressOut, onFocus: showFocusRing, onBlur: hideFocusRing, style: {
149
+ ...styles.item,
150
+ height: sizeConfig.height,
151
+ minWidth: sizeConfig.minWidth,
152
+ paddingHorizontal: sizeConfig.paddingHorizontal,
153
+ // Base variant styles
154
+ ...(context.variant === "default" && !isSelected && {
155
+ backgroundColor: "transparent",
156
+ borderColor: theme.colors.border,
157
+ }),
158
+ ...(context.variant === "default" && isSelected && {
159
+ backgroundColor: withAlpha(theme.colors.primary, 0.1),
160
+ borderColor: theme.colors.primary,
161
+ }),
162
+ // Outline variant styles
163
+ ...(context.variant === "outline" && !isSelected && {
164
+ backgroundColor: "transparent",
165
+ borderColor: theme.colors.border,
166
+ }),
167
+ ...(context.variant === "outline" && isSelected && {
168
+ backgroundColor: theme.colors.primary,
169
+ borderColor: theme.colors.primary,
170
+ }),
171
+ // Remove borders between items (except first)
172
+ ...(context.variant === "outline" && !isFirst && {
173
+ borderLeftWidth: 0,
174
+ }),
175
+ // Border radius handling
176
+ borderRadius: 0, // Remove all radius by default
177
+ ...(isFirst && {
178
+ borderTopLeftRadius: spacing.radiusMd,
179
+ borderBottomLeftRadius: spacing.radiusMd,
180
+ }),
181
+ ...(isLast && {
182
+ borderTopRightRadius: spacing.radiusMd,
183
+ borderBottomRightRadius: spacing.radiusMd,
184
+ }),
185
+ // Single item (both first and last)
186
+ ...(isFirst && isLast && {
187
+ borderRadius: spacing.radiusMd,
188
+ }),
189
+ // Disabled state
190
+ opacity: props.disabled ? 0.5 : 1,
191
+ // Web-specific styles
192
+ ...(Platform.OS === "web" && {
193
+ cursor: props.disabled ? "not-allowed" : "pointer",
194
+ transition: "all 150ms",
195
+ userSelect: "none",
196
+ // Ensure proper z-index on focus
197
+ ...(isSelected && {
198
+ zIndex: 10,
199
+ }),
173
200
  }),
174
- }),
175
- }, hitSlop: DEFAULT_HIT_SLOP, children: typeof children === "function" ? ((state) => (_jsx(TextSelectabilityContext.Provider, { value: false, children: children(state) }))) : (_jsx(TextSelectabilityContext.Provider, { value: false, children: children })) }) }) }));
201
+ ...(focused && !props.disabled ? focusRingStyle : null),
202
+ }, hitSlop: DEFAULT_HIT_SLOP, children: typeof children === "function" ? ((state) => (_jsx(TextSelectabilityContext.Provider, { value: false, children: children(state) }))) : (_jsx(TextSelectabilityContext.Provider, { value: false, children: children })) }) }) }) }));
176
203
  }
177
204
  function ToggleGroupIcon({ name, size, color }) {
178
205
  const contextColor = React.use(TextColorContext);
@@ -15,6 +15,7 @@ export * from "./EmptyState";
15
15
  export * from "./ErrorBoundary";
16
16
  export * from "./Icon";
17
17
  export * from "./InputOTP";
18
+ export * from "./Item";
18
19
  export * from "./KeyboardAvoidingView";
19
20
  export * from "./Label";
20
21
  export * from "./MaxWidthContainer";
@@ -22,11 +23,13 @@ export * from "./Notification";
22
23
  export * from "./Popover";
23
24
  export * from "./Progress";
24
25
  export * from "./RadioGroup";
26
+ export * from "./SectionHeader";
25
27
  export * from "./SegmentedControl";
26
28
  export * from "./Select";
27
29
  export * from "./Separator";
28
30
  export * from "./Skeleton";
29
31
  export * from "./Slider";
32
+ export * from "./StatCard";
30
33
  export * from "./StatusBar";
31
34
  export * from "./StyledText";
32
35
  export * from "./Switch";
@@ -15,6 +15,7 @@ export * from "./EmptyState.js";
15
15
  export * from "./ErrorBoundary.js";
16
16
  export * from "./Icon.js";
17
17
  export * from "./InputOTP.js";
18
+ export * from "./Item.js";
18
19
  export * from "./KeyboardAvoidingView.js";
19
20
  export * from "./Label.js";
20
21
  export * from "./MaxWidthContainer.js";
@@ -22,11 +23,13 @@ export * from "./Notification.js";
22
23
  export * from "./Popover.js";
23
24
  export * from "./Progress.js";
24
25
  export * from "./RadioGroup.js";
26
+ export * from "./SectionHeader.js";
25
27
  export * from "./SegmentedControl.js";
26
28
  export * from "./Select.js";
27
29
  export * from "./Separator.js";
28
30
  export * from "./Skeleton.js";
29
31
  export * from "./Slider.js";
32
+ export * from "./StatCard.js";
30
33
  export * from "./StatusBar.js";
31
34
  export * from "./StyledText.js";
32
35
  export * from "./Switch.js";
@@ -9,16 +9,55 @@ interface NavigationFonts {
9
9
  bold: FontStyle;
10
10
  heavy: FontStyle;
11
11
  }
12
- export declare const fontFamilies: {
12
+ /** Weight slots every sans-serif family map must provide. */
13
+ export type FontFamilyWeight = "light" | "regular" | "medium" | "semibold" | "bold";
14
+ /**
15
+ * How a platform expresses weight for a given family map.
16
+ *
17
+ * - `"numeric"` — one multi-weight CSS family (the package's own web default:
18
+ * a single `"Inter"` family where a numeric `fontWeight` selects the
19
+ * `@font-face` variant).
20
+ * - `"family"` — each weight is its own separately-registered single-face
21
+ * family, so the family name alone carries the weight. Emitting a numeric
22
+ * `fontWeight` on top of one makes the renderer synthesise a second layer of
23
+ * bold, so `StyledText` suppresses it.
24
+ *
25
+ * Native is always `"family"` (discrete static font files). Web is `"numeric"`
26
+ * by default, but apps that load per-weight faces through `expo-font` — which
27
+ * registers each file as its own family on web too — must declare `"family"`.
28
+ */
29
+ export type FontWeightStrategy = "numeric" | "family";
30
+ export type FontFamilyMap = {
13
31
  serif: {
14
32
  regular: string;
15
33
  bold: string;
16
34
  };
17
- sansSerif: {
18
- regular: string;
19
- bold: string;
20
- };
35
+ sansSerif: Record<FontFamilyWeight, string>;
21
36
  };
37
+ /**
38
+ * Font overrides a host app can inject to brand the package.
39
+ *
40
+ * Mirrors `setColors`: the package ships neutral defaults, but a consuming app
41
+ * almost always bundles its own faces. Without an injection point, package
42
+ * components render the package's fonts while app-authored siblings render the
43
+ * app's — and the only remaining lever is patching `node_modules`, which is
44
+ * keyed to an exact version and silently stops applying on the next bump.
45
+ *
46
+ * Partial by design: override `sansSerif` alone and `serif` keeps its default.
47
+ */
48
+ export type FontOverrides = {
49
+ families?: Partial<FontFamilyMap>;
50
+ /** Defaults to the package's own per-platform behaviour when omitted. */
51
+ webWeightStrategy?: FontWeightStrategy;
52
+ };
53
+ /** The package's built-in families. Override per-app via `setFonts`. */
54
+ export declare const fontFamilies: FontFamilyMap;
55
+ /**
56
+ * The package's own web weight strategy: one multi-weight "Inter" family, so
57
+ * weight comes from a numeric `fontWeight`. Native always behaves as
58
+ * `"family"` regardless of this value.
59
+ */
60
+ export declare const defaultWebWeightStrategy: FontWeightStrategy;
22
61
  /**
23
62
  * Typography scale — Tailwind-matching sizes for consistent text hierarchy.
24
63
  * Components can progressively adopt these tokens.
@@ -11,24 +11,48 @@ const isReactNativeRuntime = Platform.OS !== "web";
11
11
  const serifFamilies = isWebRuntime
12
12
  ? { regular: "Georgia, 'Times New Roman', serif", bold: "Georgia, 'Times New Roman', serif" }
13
13
  : { regular: "Georgia", bold: "Georgia" };
14
+ // Web ships one Inter CSS family for every weight — weight differentiation
15
+ // comes from a numeric `fontWeight` set alongside this family (see
16
+ // StyledText's getFontFamilyWeight). Native ships four discrete Inter static
17
+ // font files (loaded by useResources via @expo-google-fonts/inter), each a
18
+ // real weight, so the family name alone carries the weight — do NOT also set
19
+ // `fontWeight` on native or it faux-bolds on top of an already-bold file.
20
+ // `light` maps to the regular file on native: a 5th (300) static weight isn't
21
+ // worth shipping for how rarely `light` is used.
14
22
  const sansSerifFamilies = isWebRuntime
15
23
  ? {
16
- regular: `"Lato", ${WEB_FONT_STACK}`,
17
- bold: `"Lato", ${WEB_FONT_STACK}`,
24
+ light: `"Inter", ${WEB_FONT_STACK}`,
25
+ regular: `"Inter", ${WEB_FONT_STACK}`,
26
+ medium: `"Inter", ${WEB_FONT_STACK}`,
27
+ semibold: `"Inter", ${WEB_FONT_STACK}`,
28
+ bold: `"Inter", ${WEB_FONT_STACK}`,
18
29
  }
19
30
  : isReactNativeRuntime
20
31
  ? {
21
- regular: "System",
22
- bold: "System",
32
+ light: "Inter_400Regular",
33
+ regular: "Inter_400Regular",
34
+ medium: "Inter_500Medium",
35
+ semibold: "Inter_600SemiBold",
36
+ bold: "Inter_700Bold",
23
37
  }
24
38
  : {
39
+ light: "sans-serif",
25
40
  regular: "sans-serif",
41
+ medium: "sans-serif",
42
+ semibold: "sans-serif",
26
43
  bold: "sans-serif",
27
44
  };
45
+ /** The package's built-in families. Override per-app via `setFonts`. */
28
46
  export const fontFamilies = {
29
47
  serif: serifFamilies,
30
48
  sansSerif: sansSerifFamilies,
31
49
  };
50
+ /**
51
+ * The package's own web weight strategy: one multi-weight "Inter" family, so
52
+ * weight comes from a numeric `fontWeight`. Native always behaves as
53
+ * `"family"` regardless of this value.
54
+ */
55
+ export const defaultWebWeightStrategy = "numeric";
32
56
  export const typography = {
33
57
  xs: { fontSize: 12, lineHeight: 16 },
34
58
  sm: { fontSize: 14, lineHeight: 20 },
@@ -39,17 +63,19 @@ export const typography = {
39
63
  "3xl": { fontSize: 30, lineHeight: 36 },
40
64
  "4xl": { fontSize: 36, lineHeight: 40 },
41
65
  };
66
+ // Web: single "Inter" family per weight slot, numeric fontWeight picks the
67
+ // right @font-face variant from the multi-weight stylesheet (see useResources).
42
68
  const webNavigationFonts = {
43
69
  regular: {
44
70
  fontFamily: fontFamilies.sansSerif.regular || WEB_FONT_STACK,
45
71
  fontWeight: "400",
46
72
  },
47
73
  medium: {
48
- fontFamily: fontFamilies.sansSerif.regular || WEB_FONT_STACK,
74
+ fontFamily: fontFamilies.sansSerif.medium || WEB_FONT_STACK,
49
75
  fontWeight: "500",
50
76
  },
51
77
  bold: {
52
- fontFamily: fontFamilies.sansSerif.bold || WEB_FONT_STACK,
78
+ fontFamily: fontFamilies.sansSerif.semibold || WEB_FONT_STACK,
53
79
  fontWeight: "600",
54
80
  },
55
81
  heavy: {
@@ -57,22 +83,26 @@ const webNavigationFonts = {
57
83
  fontWeight: "700",
58
84
  },
59
85
  };
86
+ // Native: each slot points at its own real Inter static file, so the family
87
+ // name alone carries the weight. No numeric fontWeight here — pairing one
88
+ // with an already-weighted static file triggers faux-bold on top of a real
89
+ // weight file (same rule as StyledText's getFontFamilyWeight).
60
90
  const nativeNavigationFonts = {
61
91
  regular: {
62
92
  fontFamily: fontFamilies.sansSerif.regular || "System",
63
- fontWeight: "400",
93
+ fontWeight: "normal",
64
94
  },
65
95
  medium: {
66
- fontFamily: fontFamilies.sansSerif.regular || "System",
67
- fontWeight: "500",
96
+ fontFamily: fontFamilies.sansSerif.medium || "System",
97
+ fontWeight: "normal",
68
98
  },
69
99
  bold: {
70
- fontFamily: fontFamilies.sansSerif.bold || "System",
71
- fontWeight: "600",
100
+ fontFamily: fontFamilies.sansSerif.semibold || "System",
101
+ fontWeight: "normal",
72
102
  },
73
103
  heavy: {
74
104
  fontFamily: fontFamilies.sansSerif.bold || "System",
75
- fontWeight: "700",
105
+ fontWeight: "normal",
76
106
  },
77
107
  };
78
108
  const defaultNavigationFonts = {
@@ -1,3 +1,4 @@
1
1
  export * from "./colors";
2
2
  export * from "./fonts";
3
+ export * from "./motion";
3
4
  export * from "./spacing";
@@ -1,3 +1,4 @@
1
1
  export * from "./colors.js";
2
2
  export * from "./fonts.js";
3
+ export * from "./motion.js";
3
4
  export * from "./spacing.js";
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Motion Duration Tokens
3
+ *
4
+ * Standard durations (ms) for `Animated.timing` transitions. Convention:
5
+ * ease-out on enter (element appearing/growing), ease-in on exit
6
+ * (element disappearing/shrinking). Pick the token closest to the intended
7
+ * feel rather than hand-picking a duration inline.
8
+ *
9
+ * - instant: near-immediate feedback (e.g. checkmark/dot toggles)
10
+ * - fast: small, frequent UI transitions (e.g. switches, accordions)
11
+ * - normal: default entrance/exit transitions (e.g. drawers, dialogs, toasts)
12
+ * - slow: larger or more deliberate transitions
13
+ * - slower: pronounced, attention-getting transitions
14
+ *
15
+ * Deliberate outliers (e.g. skeleton/indeterminate pulse loops) are not
16
+ * expected to use these tokens.
17
+ */
18
+ export declare const durations: {
19
+ readonly instant: 75;
20
+ readonly fast: 150;
21
+ readonly normal: 200;
22
+ readonly slow: 300;
23
+ readonly slower: 500;
24
+ };
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Motion Duration Tokens
3
+ *
4
+ * Standard durations (ms) for `Animated.timing` transitions. Convention:
5
+ * ease-out on enter (element appearing/growing), ease-in on exit
6
+ * (element disappearing/shrinking). Pick the token closest to the intended
7
+ * feel rather than hand-picking a duration inline.
8
+ *
9
+ * - instant: near-immediate feedback (e.g. checkmark/dot toggles)
10
+ * - fast: small, frequent UI transitions (e.g. switches, accordions)
11
+ * - normal: default entrance/exit transitions (e.g. drawers, dialogs, toasts)
12
+ * - slow: larger or more deliberate transitions
13
+ * - slower: pronounced, attention-getting transitions
14
+ *
15
+ * Deliberate outliers (e.g. skeleton/indeterminate pulse loops) are not
16
+ * expected to use these tokens.
17
+ */
18
+ export const durations = {
19
+ instant: 75,
20
+ fast: 150,
21
+ normal: 200,
22
+ slow: 300,
23
+ slower: 500,
24
+ };
@@ -24,12 +24,12 @@ export declare const spacing: {
24
24
  readonly listItemSpacing: 8;
25
25
  readonly touchTarget: 44;
26
26
  readonly radiusNone: 0;
27
- readonly radiusXs: 2;
28
- readonly radiusSm: 4;
29
- readonly radiusMd: 6;
30
- readonly radiusLg: 8;
31
- readonly radiusXl: 12;
32
- readonly radius2xl: 16;
27
+ readonly radiusXs: 4;
28
+ readonly radiusSm: 8;
29
+ readonly radiusMd: 12;
30
+ readonly radiusLg: 14;
31
+ readonly radiusXl: 18;
32
+ readonly radius2xl: 24;
33
33
  readonly radiusFull: 9999;
34
34
  readonly iconXs: 12;
35
35
  readonly iconSm: 16;
@@ -37,5 +37,5 @@ export declare const spacing: {
37
37
  readonly iconLg: 32;
38
38
  readonly iconXl: 48;
39
39
  };
40
- export declare const base: 8, xxs: 2, xs: 4, sm: 8, md: 16, lg: 24, xl: 32, xxl: 48, xxxl: 64, gutter: 16, gutterVertical: 24, screenPadding: 16, buttonPadding: 10, inputPadding: 10, cardPadding: 16, sectionSpacing: 32, listItemSpacing: 8, touchTarget: 44, radiusNone: 0, radiusXs: 2, radiusSm: 4, radiusMd: 6, radiusLg: 8, radiusXl: 12, radius2xl: 16, radiusFull: 9999, iconXs: 12, iconSm: 16, iconMd: 24, iconLg: 32, iconXl: 48;
40
+ export declare const base: 8, xxs: 2, xs: 4, sm: 8, md: 16, lg: 24, xl: 32, xxl: 48, xxxl: 64, gutter: 16, gutterVertical: 24, screenPadding: 16, buttonPadding: 10, inputPadding: 10, cardPadding: 16, sectionSpacing: 32, listItemSpacing: 8, touchTarget: 44, radiusNone: 0, radiusXs: 4, radiusSm: 8, radiusMd: 12, radiusLg: 14, radiusXl: 18, radius2xl: 24, radiusFull: 9999, iconXs: 12, iconSm: 16, iconMd: 24, iconLg: 32, iconXl: 48;
41
41
  export declare const space: (multiplier: number) => number;
@@ -27,14 +27,14 @@ export const spacing = {
27
27
  sectionSpacing: 32, // Space between major sections
28
28
  listItemSpacing: 8, // Space between list items
29
29
  touchTarget: 44, // Minimum comfortable native touch target
30
- // Border radius — shadcn-inspired scale (radiusMd = 6px default)
30
+ // Border radius — shadcn-inspired scale (radiusMd = 12px default)
31
31
  radiusNone: 0,
32
- radiusXs: 2,
33
- radiusSm: 4,
34
- radiusMd: 6,
35
- radiusLg: 8,
36
- radiusXl: 12,
37
- radius2xl: 16,
32
+ radiusXs: 4,
33
+ radiusSm: 8,
34
+ radiusMd: 12,
35
+ radiusLg: 14,
36
+ radiusXl: 18,
37
+ radius2xl: 24,
38
38
  radiusFull: 9999, // For circular elements
39
39
  // Icon sizes
40
40
  iconXs: 12,
@@ -5,8 +5,10 @@ interface LoadResourcesResult {
5
5
  /**
6
6
  * Loads essential app resources on startup.
7
7
  *
8
- * The UI package does not bundle font files. Web loads Lato from Google Fonts;
9
- * native platforms use their system sans-serif fallback.
8
+ * Native platforms load four static Inter weights (via
9
+ * @expo-google-fonts/inter) so StyledText's weight range resolves to real
10
+ * font files. Web loads Inter from Google Fonts as a single CSS family;
11
+ * weight differentiation there comes from a numeric fontWeight instead.
10
12
  */
11
13
  export declare const useResources: () => LoadResourcesResult;
12
14
  export {};