@mrmeg/expo-ui 0.13.0 → 0.15.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 (55) hide show
  1. package/CHANGELOG.md +34 -0
  2. package/LLM_USAGE.md +4 -0
  3. package/README.md +13 -9
  4. package/dist/components/Accordion.d.ts +1 -1
  5. package/dist/components/Accordion.js +25 -19
  6. package/dist/components/Badge.js +6 -5
  7. package/dist/components/BottomSheet.js +21 -19
  8. package/dist/components/Button.d.ts +2 -1
  9. package/dist/components/Button.js +14 -4
  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 +23 -7
  18. package/dist/components/InputOTP.js +6 -2
  19. package/dist/components/Item.d.ts +93 -0
  20. package/dist/components/Item.js +125 -0
  21. package/dist/components/Label.js +7 -2
  22. package/dist/components/Notification.js +10 -22
  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 +2 -2
  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 +64 -25
  34. package/dist/components/Switch.js +84 -57
  35. package/dist/components/Tabs.js +43 -12
  36. package/dist/components/TextInput.js +8 -9
  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 +3 -0
  42. package/dist/constants/fonts.js +35 -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/package.json +24 -24
@@ -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";
@@ -15,7 +15,10 @@ export declare const fontFamilies: {
15
15
  bold: string;
16
16
  };
17
17
  sansSerif: {
18
+ light: string;
18
19
  regular: string;
20
+ medium: string;
21
+ semibold: string;
19
22
  bold: string;
20
23
  };
21
24
  };
@@ -11,18 +11,35 @@ 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
  };
28
45
  export const fontFamilies = {
@@ -39,17 +56,19 @@ export const typography = {
39
56
  "3xl": { fontSize: 30, lineHeight: 36 },
40
57
  "4xl": { fontSize: 36, lineHeight: 40 },
41
58
  };
59
+ // Web: single "Inter" family per weight slot, numeric fontWeight picks the
60
+ // right @font-face variant from the multi-weight stylesheet (see useResources).
42
61
  const webNavigationFonts = {
43
62
  regular: {
44
63
  fontFamily: fontFamilies.sansSerif.regular || WEB_FONT_STACK,
45
64
  fontWeight: "400",
46
65
  },
47
66
  medium: {
48
- fontFamily: fontFamilies.sansSerif.regular || WEB_FONT_STACK,
67
+ fontFamily: fontFamilies.sansSerif.medium || WEB_FONT_STACK,
49
68
  fontWeight: "500",
50
69
  },
51
70
  bold: {
52
- fontFamily: fontFamilies.sansSerif.bold || WEB_FONT_STACK,
71
+ fontFamily: fontFamilies.sansSerif.semibold || WEB_FONT_STACK,
53
72
  fontWeight: "600",
54
73
  },
55
74
  heavy: {
@@ -57,22 +76,26 @@ const webNavigationFonts = {
57
76
  fontWeight: "700",
58
77
  },
59
78
  };
79
+ // Native: each slot points at its own real Inter static file, so the family
80
+ // name alone carries the weight. No numeric fontWeight here — pairing one
81
+ // with an already-weighted static file triggers faux-bold on top of a real
82
+ // weight file (same rule as StyledText's getFontFamilyWeight).
60
83
  const nativeNavigationFonts = {
61
84
  regular: {
62
85
  fontFamily: fontFamilies.sansSerif.regular || "System",
63
- fontWeight: "400",
86
+ fontWeight: "normal",
64
87
  },
65
88
  medium: {
66
- fontFamily: fontFamilies.sansSerif.regular || "System",
67
- fontWeight: "500",
89
+ fontFamily: fontFamilies.sansSerif.medium || "System",
90
+ fontWeight: "normal",
68
91
  },
69
92
  bold: {
70
- fontFamily: fontFamilies.sansSerif.bold || "System",
71
- fontWeight: "600",
93
+ fontFamily: fontFamilies.sansSerif.semibold || "System",
94
+ fontWeight: "normal",
72
95
  },
73
96
  heavy: {
74
97
  fontFamily: fontFamilies.sansSerif.bold || "System",
75
- fontWeight: "700",
98
+ fontWeight: "normal",
76
99
  },
77
100
  };
78
101
  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 {};
@@ -1,6 +1,7 @@
1
1
  import { useEffect, useState } from "react";
2
2
  import * as Font from "expo-font";
3
3
  import Feather from "@expo/vector-icons/Feather";
4
+ import { Inter_400Regular, Inter_500Medium, Inter_600SemiBold, Inter_700Bold, } from "@expo-google-fonts/inter";
4
5
  import { Platform } from "react-native";
5
6
  // Eager, module-scope load so expo-font registers Feather in its SSR
6
7
  // serverContext. When the server renders the HTML document, expo-font emits
@@ -11,30 +12,50 @@ import { Platform } from "react-native";
11
12
  // the effect; the effect's loadAsync becomes a no-op for the already-loaded
12
13
  // font.
13
14
  void Font.loadAsync(Feather.font);
14
- const LATO_STYLESHEET_ID = "mrmeg-expo-ui-lato";
15
- const LATO_STYLESHEET_URL = "https://fonts.googleapis.com/css2?family=Lato:wght@400;700&display=swap";
15
+ // The four static Inter weights StyledText's native family keys point at
16
+ // (see constants/fonts.ts). Native-only: web never renders these family names
17
+ // ("Inter_400Regular" etc.) — fontFamilies.sansSerif resolves every weight to
18
+ // the single "Inter" CSS family on web (loaded via ensureWebFontStylesheet
19
+ // below), so fetching these .ttf assets there would just be ~1.3MB of dead
20
+ // weight with nothing pointing at them.
21
+ const interFontMap = {
22
+ Inter_400Regular,
23
+ Inter_500Medium,
24
+ Inter_600SemiBold,
25
+ Inter_700Bold,
26
+ };
27
+ function loadNativeInterFonts() {
28
+ if (Platform.OS === "web") {
29
+ return Promise.resolve();
30
+ }
31
+ return Font.loadAsync(interFontMap);
32
+ }
33
+ const INTER_STYLESHEET_ID = "mrmeg-expo-ui-inter";
34
+ const INTER_STYLESHEET_URL = "https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap";
16
35
  function ensureWebFontStylesheet() {
17
36
  if (Platform.OS !== "web" || typeof document === "undefined") {
18
37
  return Promise.resolve();
19
38
  }
20
- if (document.getElementById(LATO_STYLESHEET_ID)) {
39
+ if (document.getElementById(INTER_STYLESHEET_ID)) {
21
40
  return Promise.resolve();
22
41
  }
23
42
  return new Promise((resolve, reject) => {
24
43
  const link = document.createElement("link");
25
- link.id = LATO_STYLESHEET_ID;
44
+ link.id = INTER_STYLESHEET_ID;
26
45
  link.rel = "stylesheet";
27
- link.href = LATO_STYLESHEET_URL;
46
+ link.href = INTER_STYLESHEET_URL;
28
47
  link.onload = () => resolve();
29
- link.onerror = () => reject(new Error("Lato stylesheet failed to load"));
48
+ link.onerror = () => reject(new Error("Inter stylesheet failed to load"));
30
49
  document.head.appendChild(link);
31
50
  });
32
51
  }
33
52
  /**
34
53
  * Loads essential app resources on startup.
35
54
  *
36
- * The UI package does not bundle font files. Web loads Lato from Google Fonts;
37
- * native platforms use their system sans-serif fallback.
55
+ * Native platforms load four static Inter weights (via
56
+ * @expo-google-fonts/inter) so StyledText's weight range resolves to real
57
+ * font files. Web loads Inter from Google Fonts as a single CSS family;
58
+ * weight differentiation there comes from a numeric fontWeight instead.
38
59
  */
39
60
  export const useResources = () => {
40
61
  const [loaded, setLoaded] = useState(false);
@@ -45,6 +66,7 @@ export const useResources = () => {
45
66
  try {
46
67
  const fontPromise = Promise.all([
47
68
  Font.loadAsync(Feather.font),
69
+ loadNativeInterFonts(),
48
70
  ensureWebFontStylesheet(),
49
71
  ]);
50
72
  // Timeout after 5 seconds — proceed with system fallback fonts
@@ -14,7 +14,7 @@ interface StaggeredEntranceOptions {
14
14
  delay?: number;
15
15
  /**
16
16
  * Duration of the entrance animation (ms)
17
- * @default 200
17
+ * @default durations.normal (200)
18
18
  */
19
19
  duration?: number;
20
20
  /**
@@ -1,6 +1,7 @@
1
1
  import { useEffect, useMemo, useRef } from "react";
2
2
  import { Animated, Easing } from "react-native";
3
3
  import { useReducedMotion } from "./useReduceMotion.js";
4
+ import { durations } from "../constants/motion.js";
4
5
  /**
5
6
  * Hook for entrance animations with stagger support using React Native Animated.
6
7
  *
@@ -21,7 +22,7 @@ import { useReducedMotion } from "./useReduceMotion.js";
21
22
  * ```
22
23
  */
23
24
  export function useStaggeredEntrance(options = {}) {
24
- const { type = "fadeSlideUp", delay = 0, duration = 200, slideDistance = 8, initialScale = 0.95, } = options;
25
+ const { type = "fadeSlideUp", delay = 0, duration = durations.normal, slideDistance = 8, initialScale = 0.95, } = options;
25
26
  const reduceMotion = useReducedMotion();
26
27
  const opacity = useRef(new Animated.Value(reduceMotion ? 1 : 0)).current;
27
28
  const translateY = useRef(new Animated.Value(reduceMotion
@@ -32,7 +32,7 @@ interface ExtendedColorScheme {
32
32
  * - getTextColorForBackground("#000") → "light"
33
33
  * - getContrastingColor("#f4f4f4", "#222", "#fff") → "#222"
34
34
  * - withAlpha("#336699", 0.6) → "rgba(51,102,153,0.6)"
35
- * - getShadowStyle('base') → { boxShadow: "0px 1px 3px rgba(0, 0, 0, 0.1)" }
35
+ * - getShadowStyle('base') → { boxShadow: "0px 2px 6px rgba(0, 0, 0, 0.05), 0px 4px 12px rgba(0, 0, 0, 0.03)" }
36
36
  */
37
37
  export declare function useTheme(): ExtendedColorScheme & {
38
38
  toggleTheme: () => void;