@mrmeg/expo-ui 0.27.0 → 0.28.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 (145) hide show
  1. package/CHANGELOG.md +712 -0
  2. package/LLM_USAGE.md +260 -32
  3. package/README.md +558 -101
  4. package/dist/components/Accordion.js +12 -6
  5. package/dist/components/AnimatedView.d.ts +1 -1
  6. package/dist/components/AnimatedView.js +5 -2
  7. package/dist/components/Avatar.d.ts +1 -1
  8. package/dist/components/Avatar.js +2 -2
  9. package/dist/components/Badge.js +5 -1
  10. package/dist/components/BottomSheet.d.ts +66 -9
  11. package/dist/components/BottomSheet.js +143 -39
  12. package/dist/components/Button.d.ts +20 -4
  13. package/dist/components/Button.js +64 -43
  14. package/dist/components/Card.d.ts +1 -1
  15. package/dist/components/Card.js +24 -4
  16. package/dist/components/Carousel.js +2 -2
  17. package/dist/components/Checkbox.js +51 -44
  18. package/dist/components/Collapsible.js +7 -3
  19. package/dist/components/Dialog.js +178 -29
  20. package/dist/components/Drawer.d.ts +1 -1
  21. package/dist/components/Drawer.js +65 -55
  22. package/dist/components/DropdownMenu.js +11 -7
  23. package/dist/components/EmptyState.d.ts +2 -2
  24. package/dist/components/EmptyState.js +5 -2
  25. package/dist/components/Hydrated.d.ts +31 -0
  26. package/dist/components/Hydrated.js +30 -0
  27. package/dist/components/Icon.d.ts +17 -5
  28. package/dist/components/Icon.js +31 -10
  29. package/dist/components/InputOTP.js +7 -3
  30. package/dist/components/Item.d.ts +103 -4
  31. package/dist/components/Item.js +168 -15
  32. package/dist/components/KeyboardAvoidingView.d.ts +1 -1
  33. package/dist/components/KeyboardAvoidingView.js +4 -1
  34. package/dist/components/Label.js +3 -2
  35. package/dist/components/MaxWidthContainer.js +1 -1
  36. package/dist/components/Notification.js +45 -27
  37. package/dist/components/Popover.d.ts +18 -5
  38. package/dist/components/Popover.js +72 -8
  39. package/dist/components/Progress.js +8 -6
  40. package/dist/components/RadioGroup.js +52 -49
  41. package/dist/components/Screen.d.ts +61 -0
  42. package/dist/components/Screen.js +62 -0
  43. package/dist/components/SectionHeader.d.ts +1 -1
  44. package/dist/components/SegmentedControl.d.ts +12 -4
  45. package/dist/components/SegmentedControl.js +94 -4
  46. package/dist/components/Select.js +21 -26
  47. package/dist/components/Skeleton.js +24 -10
  48. package/dist/components/Slider.d.ts +11 -3
  49. package/dist/components/Slider.js +74 -6
  50. package/dist/components/StatCard.d.ts +4 -2
  51. package/dist/components/StatCard.js +3 -1
  52. package/dist/components/StyledText.d.ts +7 -1
  53. package/dist/components/StyledText.js +5 -3
  54. package/dist/components/Switch.d.ts +1 -1
  55. package/dist/components/Switch.js +23 -32
  56. package/dist/components/Tabs.d.ts +2 -2
  57. package/dist/components/Tabs.js +13 -24
  58. package/dist/components/TextInput.d.ts +1 -1
  59. package/dist/components/TextInput.js +73 -27
  60. package/dist/components/Toggle.d.ts +2 -42
  61. package/dist/components/Toggle.js +38 -27
  62. package/dist/components/ToggleGroup.d.ts +1 -1
  63. package/dist/components/ToggleGroup.js +37 -32
  64. package/dist/components/Tooltip.d.ts +1 -1
  65. package/dist/components/Tooltip.js +9 -3
  66. package/dist/components/UIProvider.d.ts +22 -2
  67. package/dist/components/UIProvider.js +15 -3
  68. package/dist/components/bottomSheetDismiss.d.ts +19 -0
  69. package/dist/components/bottomSheetDismiss.js +44 -0
  70. package/dist/components/bottomSheetHostSupport.d.ts +46 -0
  71. package/dist/components/bottomSheetHostSupport.js +67 -0
  72. package/dist/components/index.d.ts +46 -44
  73. package/dist/components/index.js +2 -0
  74. package/dist/components/keyboardDismiss.d.ts +25 -0
  75. package/dist/components/keyboardDismiss.js +60 -3
  76. package/dist/components/nativeTextField.android.js +13 -9
  77. package/dist/constants/colors.d.ts +40 -5
  78. package/dist/constants/colors.js +26 -8
  79. package/dist/constants/fonts.d.ts +41 -2
  80. package/dist/constants/fonts.js +46 -7
  81. package/dist/constants/index.d.ts +5 -4
  82. package/dist/constants/index.js +1 -0
  83. package/dist/constants/interaction.d.ts +18 -0
  84. package/dist/constants/interaction.js +18 -0
  85. package/dist/constants/spacing.d.ts +2 -1
  86. package/dist/constants/spacing.js +2 -1
  87. package/dist/design-system.json +154 -2
  88. package/dist/hooks/index.d.ts +12 -7
  89. package/dist/hooks/index.js +5 -0
  90. package/dist/hooks/useDimensions.d.ts +13 -8
  91. package/dist/hooks/useDimensions.js +123 -48
  92. package/dist/hooks/useFocusVisible.d.ts +30 -0
  93. package/dist/hooks/useFocusVisible.js +45 -0
  94. package/dist/hooks/useFontStyle.d.ts +2 -2
  95. package/dist/hooks/useFontStyle.js +3 -2
  96. package/dist/hooks/useHydrated.d.ts +13 -0
  97. package/dist/hooks/useHydrated.js +19 -0
  98. package/dist/hooks/useNotificationOffset.d.ts +20 -0
  99. package/dist/hooks/useNotificationOffset.js +34 -0
  100. package/dist/hooks/useResources.d.ts +21 -20
  101. package/dist/hooks/useResources.js +72 -68
  102. package/dist/hooks/useScalePress.d.ts +8 -5
  103. package/dist/hooks/useScalePress.js +16 -9
  104. package/dist/hooks/useShape.d.ts +24 -0
  105. package/dist/hooks/useShape.js +24 -0
  106. package/dist/hooks/useStaggeredEntrance.js +9 -7
  107. package/dist/hooks/useTheme.d.ts +3 -3
  108. package/dist/hooks/useTheme.js +177 -66
  109. package/dist/hooks/useWindowInsets.d.ts +13 -0
  110. package/dist/hooks/useWindowInsets.js +23 -0
  111. package/dist/index.d.ts +5 -5
  112. package/dist/lib/composeRefs.d.ts +8 -0
  113. package/dist/lib/composeRefs.js +20 -0
  114. package/dist/lib/haptics.d.ts +12 -0
  115. package/dist/lib/haptics.js +21 -0
  116. package/dist/lib/index.d.ts +6 -4
  117. package/dist/lib/index.js +2 -0
  118. package/dist/lib/interFonts.d.ts +15 -0
  119. package/dist/lib/interFonts.js +1 -0
  120. package/dist/lib/interFonts.native.d.ts +3 -0
  121. package/dist/lib/interFonts.native.js +20 -0
  122. package/dist/lib/popoverPlacement.d.ts +36 -0
  123. package/dist/lib/popoverPlacement.js +29 -0
  124. package/dist/lib/stateSurface.d.ts +28 -0
  125. package/dist/lib/stateSurface.js +30 -0
  126. package/dist/lib/themedStyles.d.ts +1 -1
  127. package/dist/lib/useAnimatedValue.d.ts +15 -0
  128. package/dist/lib/useAnimatedValue.js +19 -0
  129. package/dist/lib/webHitTarget.d.ts +40 -0
  130. package/dist/lib/webHitTarget.js +37 -0
  131. package/dist/state/feedbackStore.d.ts +24 -0
  132. package/dist/state/feedbackStore.js +16 -0
  133. package/dist/state/globalUIStore.d.ts +27 -0
  134. package/dist/state/globalUIStore.js +23 -0
  135. package/dist/state/index.d.ts +7 -5
  136. package/dist/state/index.js +2 -0
  137. package/dist/state/initialScheme.d.ts +27 -0
  138. package/dist/state/initialScheme.js +29 -0
  139. package/dist/state/notify.d.ts +1 -1
  140. package/dist/state/themeColorScope.d.ts +1 -1
  141. package/dist/state/themeStore.d.ts +69 -16
  142. package/dist/state/themeStore.js +65 -38
  143. package/llms-full.md +56 -8
  144. package/llms.txt +10 -1
  145. package/package.json +69 -2
@@ -1,25 +1,26 @@
1
+ import type { SerifPreset } from "../constants/fonts.js";
1
2
  interface LoadResourcesResult {
2
3
  loaded: boolean;
3
4
  error: Error | null;
4
5
  }
5
- /**
6
- * Loads essential app resources on startup.
7
- *
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.
12
- *
13
- * Icons need nothing here: `Icon` renders `lucide-react-native` SVGs, which
14
- * have no font face to register on any platform, so server-rendered HTML
15
- * carries the glyph markup and hydrates without a font round trip.
16
- *
17
- * A host app that overrides the sans-serif families via `setFonts` owns
18
- * loading its own faces (typically through `expo-font`), so the Inter fetch
19
- * is skipped entirely — nothing would reference those files. For the skip to
20
- * apply, call `setFonts` before this hook mounts (module scope or ahead of
21
- * rendering the root); a later call still re-skins text, it just doesn't
22
- * un-download Inter.
23
- */
24
- export declare const useResources: () => LoadResourcesResult;
6
+ export interface UseResourcesOptions {
7
+ /**
8
+ * Which serif to load and switch the `serif` variant to. `"georgia"`
9
+ * (default) loads nothing: the system face. `"newsreader"` injects the
10
+ * Google Fonts Newsreader stylesheet on web and, on native, registers the
11
+ * files passed as `serifFonts`, then sets the theme store's `serifPreset`
12
+ * so serif text resolves to Newsreader. An app `setFonts` serif override
13
+ * skips the preset entirely (the override wins anyway).
14
+ */
15
+ serif?: SerifPreset;
16
+ /**
17
+ * Native font map for the serif preset, from the app's own dependency:
18
+ * `@expo-google-fonts/newsreader/400Regular` … `700Bold` and
19
+ * `400Regular_Italic` (the names `newsreaderFamilies.native` expects). The
20
+ * package does not depend on the font package, so an app that never opts in
21
+ * ships none of it; on web this is ignored (the stylesheet serves the faces).
22
+ */
23
+ serifFonts?: Record<string, number | string> | null;
24
+ }
25
+ export declare const useResources: (options?: UseResourcesOptions) => LoadResourcesResult;
25
26
  export {};
@@ -1,96 +1,100 @@
1
1
  import { useEffect, useState } from "react";
2
2
  import * as Font from "expo-font";
3
- import { Inter_400Regular, Inter_500Medium, Inter_600SemiBold, Inter_700Bold, } from "@expo-google-fonts/inter";
4
3
  import { Platform } from "react-native";
4
+ import { interFontMap } from "../lib/interFonts";
5
5
  import { useThemeStore } from "../state/themeStore.js";
6
- // The four static Inter weights StyledText's native family keys point at
7
- // (see constants/fonts.ts). Native-only: web never renders these family names
8
- // ("Inter_400Regular" etc.) — fontFamilies.sansSerif resolves every weight to
9
- // the single "Inter" CSS family on web (loaded via ensureWebFontStylesheet
10
- // below), so fetching these .ttf assets there would just be ~1.3MB of dead
11
- // weight with nothing pointing at them.
12
- const interFontMap = {
13
- Inter_400Regular,
14
- Inter_500Medium,
15
- Inter_600SemiBold,
16
- Inter_700Bold,
17
- };
18
6
  function loadNativeInterFonts() {
19
- if (Platform.OS === "web") {
7
+ if (Platform.OS === "web" || !interFontMap) {
20
8
  return Promise.resolve();
21
9
  }
22
10
  return Font.loadAsync(interFontMap);
23
11
  }
24
12
  const INTER_STYLESHEET_ID = "mrmeg-expo-ui-inter";
25
- const INTER_STYLESHEET_URL = "https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap";
26
- function ensureWebFontStylesheet() {
13
+ // Four upright weights plus the 400 italic, so `italic` renders a real italic
14
+ // on web at body weight (other weights synthesize).
15
+ const INTER_STYLESHEET_URL = "https://fonts.googleapis.com/css2?family=Inter:ital,wght@0,400;0,500;0,600;0,700;1,400&display=swap";
16
+ const NEWSREADER_STYLESHEET_ID = "mrmeg-expo-ui-newsreader";
17
+ const NEWSREADER_STYLESHEET_URL = "https://fonts.googleapis.com/css2?family=Newsreader:ital,wght@0,400;0,500;0,600;0,700;1,400&display=swap";
18
+ function ensureWebFontStylesheet(id, href, label) {
27
19
  if (Platform.OS !== "web" || typeof document === "undefined") {
28
20
  return Promise.resolve();
29
21
  }
30
- if (document.getElementById(INTER_STYLESHEET_ID)) {
22
+ if (document.getElementById(id)) {
31
23
  return Promise.resolve();
32
24
  }
33
25
  return new Promise((resolve, reject) => {
34
26
  const link = document.createElement("link");
35
- link.id = INTER_STYLESHEET_ID;
27
+ link.id = id;
36
28
  link.rel = "stylesheet";
37
- link.href = INTER_STYLESHEET_URL;
29
+ link.href = href;
38
30
  link.onload = () => resolve();
39
- link.onerror = () => reject(new Error("Inter stylesheet failed to load"));
31
+ link.onerror = () => reject(new Error(`${label} stylesheet failed to load`));
40
32
  document.head.appendChild(link);
41
33
  });
42
34
  }
35
+ let warnedMissingSerifFonts = false;
43
36
  /**
44
- * Loads essential app resources on startup.
45
- *
46
- * Native platforms load four static Inter weights (via
47
- * @expo-google-fonts/inter) so StyledText's weight range resolves to real
48
- * font files. Web loads Inter from Google Fonts as a single CSS family;
49
- * weight differentiation there comes from a numeric fontWeight instead.
50
- *
51
- * Icons need nothing here: `Icon` renders `lucide-react-native` SVGs, which
52
- * have no font face to register on any platform, so server-rendered HTML
53
- * carries the glyph markup and hydrates without a font round trip.
54
- *
55
- * A host app that overrides the sans-serif families via `setFonts` owns
56
- * loading its own faces (typically through `expo-font`), so the Inter fetch
57
- * is skipped entirely — nothing would reference those files. For the skip to
58
- * apply, call `setFonts` before this hook mounts (module scope or ahead of
59
- * rendering the root); a later call still re-skins text, it just doesn't
60
- * un-download Inter.
37
+ * The Newsreader preset: the stylesheet on web (the preset switches at once;
38
+ * the stack falls back to Georgia until the faces arrive), the app-supplied
39
+ * files on native (the preset switches after they register, since a native
40
+ * font name that is not loaded is an error, not a fallback).
61
41
  */
62
- export const useResources = () => {
42
+ async function loadSerifPreset(options) {
43
+ if (options.serif !== "newsreader")
44
+ return;
45
+ if (useThemeStore.getState().fontOverrides.families?.serif)
46
+ return;
47
+ if (Platform.OS === "web") {
48
+ useThemeStore.getState().setSerifPreset("newsreader");
49
+ await ensureWebFontStylesheet(NEWSREADER_STYLESHEET_ID, NEWSREADER_STYLESHEET_URL, "Newsreader");
50
+ return;
51
+ }
52
+ if (!options.serifFonts) {
53
+ if (!warnedMissingSerifFonts) {
54
+ warnedMissingSerifFonts = true;
55
+ console.warn("useResources({ serif: \"newsreader\" }) needs `serifFonts` on native: pass the map of " +
56
+ "@expo-google-fonts/newsreader faces (400Regular, 500Medium, 600SemiBold, 700Bold, " +
57
+ "400Regular_Italic). Keeping Georgia.");
58
+ }
59
+ return;
60
+ }
61
+ await Font.loadAsync(options.serifFonts);
62
+ useThemeStore.getState().setSerifPreset("newsreader");
63
+ }
64
+ export const useResources = (options = {}) => {
63
65
  const [loaded, setLoaded] = useState(false);
64
66
  const [error, setError] = useState(null);
65
- useEffect(() => {
66
- let timeoutId;
67
- // Read once at mount, not subscribed: font loading is a one-shot startup
68
- // effect and cannot be undone by a later override.
69
- const sansSerifOverridden = !!useThemeStore.getState().fontOverrides.families?.sansSerif;
70
- async function loadResourcesAndDataAsync() {
71
- try {
72
- const fontPromise = Promise.all([
73
- sansSerifOverridden ? Promise.resolve() : loadNativeInterFonts(),
74
- sansSerifOverridden ? Promise.resolve() : ensureWebFontStylesheet(),
75
- ]);
76
- // Timeout after 5 seconds — proceed with system fallback fonts
77
- const timeoutPromise = new Promise((_, reject) => {
78
- timeoutId = setTimeout(() => reject(new Error("Font loading timed out after 5s")), 5000);
79
- });
80
- await Promise.race([fontPromise, timeoutPromise]);
81
- }
82
- catch (e) {
83
- const error = e instanceof Error ? e : new Error(String(e));
84
- console.warn("Font loading issue (proceeding with fallback):", error.message);
85
- setError(error);
86
- }
87
- finally {
88
- clearTimeout(timeoutId);
89
- setLoaded(true);
90
- }
91
- }
92
- loadResourcesAndDataAsync();
93
- return () => clearTimeout(timeoutId);
94
- }, []);
67
+ const { serif, serifFonts } = options;
68
+ useEffect(() => startResourceLoad({ serif, serifFonts }, setError, () => setLoaded(true)), [serif, serifFonts]);
95
69
  return { loaded, error };
96
70
  };
71
+ function startResourceLoad(options, onError, onLoaded) {
72
+ let timeoutId;
73
+ const sansSerifOverridden = !!useThemeStore.getState().fontOverrides.families?.sansSerif;
74
+ async function loadResourcesAndDataAsync() {
75
+ try {
76
+ const fontPromise = Promise.all([
77
+ sansSerifOverridden ? Promise.resolve() : loadNativeInterFonts(),
78
+ sansSerifOverridden
79
+ ? Promise.resolve()
80
+ : ensureWebFontStylesheet(INTER_STYLESHEET_ID, INTER_STYLESHEET_URL, "Inter"),
81
+ loadSerifPreset(options),
82
+ ]);
83
+ const timeoutPromise = new Promise((_, reject) => {
84
+ timeoutId = setTimeout(() => reject(new Error("Font loading timed out after 5s")), 5000);
85
+ });
86
+ await Promise.race([fontPromise, timeoutPromise]);
87
+ }
88
+ catch (e) {
89
+ const error = e instanceof Error ? e : new Error(String(e));
90
+ console.warn("Font loading issue (proceeding with fallback):", error.message);
91
+ onError(error);
92
+ }
93
+ finally {
94
+ clearTimeout(timeoutId);
95
+ onLoaded();
96
+ }
97
+ }
98
+ loadResourcesAndDataAsync();
99
+ return () => clearTimeout(timeoutId);
100
+ }
@@ -6,10 +6,12 @@ interface ScalePressOptions {
6
6
  */
7
7
  scaleTo?: number;
8
8
  /**
9
- * Whether to fire haptic feedback on press
10
- * @default true
9
+ * Haptic on press-in. `"setting"` follows the provider-level `haptics`
10
+ * setting (a light tap only under `"all"`); `true` always taps; `false`
11
+ * never does.
12
+ * @default "setting"
11
13
  */
12
- haptic?: boolean;
14
+ haptic?: boolean | "setting";
13
15
  /**
14
16
  * Spring damping for bounce-back
15
17
  * @default 20
@@ -29,8 +31,9 @@ interface ScalePressOptions {
29
31
  /**
30
32
  * Hook for press-feedback scale animation using React Native Animated.
31
33
  *
32
- * Returns an animated style and onPressIn/onPressOut handlers to spread onto a Pressable.
33
- * Respects reduced motion preferences.
34
+ * Returns an animated style and onPressIn/onPressOut handlers to spread onto a
35
+ * Pressable. Under reduced motion the scale stays at 1 — the pressed opacity
36
+ * the components layer on carries the feedback instead.
34
37
  *
35
38
  * @example
36
39
  * ```tsx
@@ -1,12 +1,15 @@
1
- import { useCallback, useMemo, useRef } from "react";
1
+ import { useCallback, useMemo } from "react";
2
2
  import { Animated } from "react-native";
3
- import { hapticLight } from "../lib/haptics.js";
3
+ import { hapticLight, hapticPress } from "../lib/haptics.js";
4
+ import { useAnimatedValue } from "../lib/useAnimatedValue.js";
5
+ import { shouldUseNativeDriver } from "../lib/animations.js";
4
6
  import { useReducedMotion } from "./useReduceMotion.js";
5
7
  /**
6
8
  * Hook for press-feedback scale animation using React Native Animated.
7
9
  *
8
- * Returns an animated style and onPressIn/onPressOut handlers to spread onto a Pressable.
9
- * Respects reduced motion preferences.
10
+ * Returns an animated style and onPressIn/onPressOut handlers to spread onto a
11
+ * Pressable. Under reduced motion the scale stays at 1 — the pressed opacity
12
+ * the components layer on carries the feedback instead.
10
13
  *
11
14
  * @example
12
15
  * ```tsx
@@ -20,27 +23,31 @@ import { useReducedMotion } from "./useReduceMotion.js";
20
23
  * ```
21
24
  */
22
25
  export function useScalePress(options = {}) {
23
- const { scaleTo = 0.97, haptic = true, damping = 20, stiffness = 300, disabled = false, } = options;
26
+ const { scaleTo = 0.97, haptic = "setting", damping = 20, stiffness = 300, disabled = false, } = options;
24
27
  const reduceMotion = useReducedMotion();
25
- const scale = useRef(new Animated.Value(1)).current;
28
+ const scale = useAnimatedValue(1);
26
29
  const animateTo = useCallback((toValue) => {
27
30
  scale.stopAnimation();
31
+ // Reduced motion: no scale change at all (not even an instant jump), so
32
+ // the surface stays still while its pressed opacity does the signalling.
28
33
  if (reduceMotion) {
29
- scale.setValue(toValue);
34
+ scale.setValue(1);
30
35
  return;
31
36
  }
32
37
  Animated.spring(scale, {
33
38
  toValue,
34
39
  damping,
35
40
  stiffness,
36
- useNativeDriver: true,
41
+ useNativeDriver: shouldUseNativeDriver,
37
42
  }).start();
38
43
  }, [damping, reduceMotion, scale, stiffness]);
39
44
  const onPressIn = useCallback(() => {
40
45
  if (disabled)
41
46
  return;
42
- if (haptic)
47
+ if (haptic === true)
43
48
  hapticLight();
49
+ else if (haptic === "setting")
50
+ hapticPress();
44
51
  animateTo(scaleTo);
45
52
  }, [animateTo, disabled, haptic, scaleTo]);
46
53
  const onPressOut = useCallback(() => {
@@ -0,0 +1,24 @@
1
+ import { type ShapeOverrides, type ShapeSlot } from "../state/themeStore.js";
2
+ /**
3
+ * The host app's `setShape` override for one slot, or `undefined` when the
4
+ * slot is untouched. One store subscription per component, re-rendering only
5
+ * when that slot changes.
6
+ *
7
+ * Layer it right after the component's static radius and before the caller's
8
+ * `style`, so the precedence stays caller → app → package:
9
+ *
10
+ * ```tsx
11
+ * const cardShape = useShape("card");
12
+ * <View style={[styles.card, shapeRadius(cardShape), style]} />
13
+ * ```
14
+ */
15
+ export declare function useShape<S extends ShapeSlot>(slot: S): ShapeOverrides[S] | undefined;
16
+ /**
17
+ * `{ borderRadius }` when the override sets one (zero included — square
18
+ * corners are a choice), otherwise `undefined` so a style array skips it.
19
+ */
20
+ export declare function shapeRadius(override: {
21
+ borderRadius?: number;
22
+ } | undefined): {
23
+ borderRadius: number;
24
+ } | undefined;
@@ -0,0 +1,24 @@
1
+ import { useThemeStore } from "../state/themeStore.js";
2
+ /**
3
+ * The host app's `setShape` override for one slot, or `undefined` when the
4
+ * slot is untouched. One store subscription per component, re-rendering only
5
+ * when that slot changes.
6
+ *
7
+ * Layer it right after the component's static radius and before the caller's
8
+ * `style`, so the precedence stays caller → app → package:
9
+ *
10
+ * ```tsx
11
+ * const cardShape = useShape("card");
12
+ * <View style={[styles.card, shapeRadius(cardShape), style]} />
13
+ * ```
14
+ */
15
+ export function useShape(slot) {
16
+ return useThemeStore((s) => s.shapeOverrides[slot]);
17
+ }
18
+ /**
19
+ * `{ borderRadius }` when the override sets one (zero included — square
20
+ * corners are a choice), otherwise `undefined` so a style array skips it.
21
+ */
22
+ export function shapeRadius(override) {
23
+ return override?.borderRadius === undefined ? undefined : { borderRadius: override.borderRadius };
24
+ }
@@ -1,7 +1,9 @@
1
- import { useEffect, useMemo, useRef } from "react";
1
+ import { useEffect, useMemo } from "react";
2
2
  import { Animated, Easing } from "react-native";
3
3
  import { useReducedMotion } from "./useReduceMotion.js";
4
4
  import { durations } from "../constants/motion.js";
5
+ import { useAnimatedValue } from "../lib/useAnimatedValue.js";
6
+ import { shouldUseNativeDriver } from "../lib/animations.js";
5
7
  /**
6
8
  * Hook for entrance animations with stagger support using React Native Animated.
7
9
  *
@@ -24,15 +26,15 @@ import { durations } from "../constants/motion.js";
24
26
  export function useStaggeredEntrance(options = {}) {
25
27
  const { type = "fadeSlideUp", delay = 0, duration = durations.normal, slideDistance = 8, initialScale = 0.95, } = options;
26
28
  const reduceMotion = useReducedMotion();
27
- const opacity = useRef(new Animated.Value(reduceMotion ? 1 : 0)).current;
28
- const translateY = useRef(new Animated.Value(reduceMotion
29
+ const opacity = useAnimatedValue(reduceMotion ? 1 : 0);
30
+ const translateY = useAnimatedValue(reduceMotion
29
31
  ? 0
30
32
  : type === "fadeSlideUp"
31
33
  ? slideDistance
32
34
  : type === "fadeSlideDown"
33
35
  ? -slideDistance
34
- : 0)).current;
35
- const scale = useRef(new Animated.Value(reduceMotion ? 1 : type === "scale" ? initialScale : 1)).current;
36
+ : 0);
37
+ const scale = useAnimatedValue(reduceMotion ? 1 : type === "scale" ? initialScale : 1);
36
38
  useEffect(() => {
37
39
  if (reduceMotion) {
38
40
  opacity.setValue(1);
@@ -50,7 +52,7 @@ export function useStaggeredEntrance(options = {}) {
50
52
  const timingConfig = {
51
53
  duration,
52
54
  easing: Easing.out(Easing.cubic),
53
- useNativeDriver: true,
55
+ useNativeDriver: shouldUseNativeDriver,
54
56
  };
55
57
  const animations = [
56
58
  Animated.timing(opacity, {
@@ -69,7 +71,7 @@ export function useStaggeredEntrance(options = {}) {
69
71
  toValue: 1,
70
72
  damping: 14,
71
73
  stiffness: 250,
72
- useNativeDriver: true,
74
+ useNativeDriver: shouldUseNativeDriver,
73
75
  }));
74
76
  }
75
77
  const animation = delay > 0
@@ -1,6 +1,6 @@
1
- import { Colors } from "../constants/colors";
1
+ import { Colors } from "../constants/colors.js";
2
2
  import { ImageStyle, TextStyle, ViewStyle } from "react-native";
3
- import { spacing as spacingConstants } from "../constants/spacing";
3
+ import { spacing as spacingConstants } from "../constants/spacing.js";
4
4
  type ShadowType = "base" | "soft" | "sharp" | "subtle" | "elevated" | "glow" | "glass" | "card" | "cardHover" | "cardSubtle";
5
5
  interface ExtendedColorScheme {
6
6
  theme: Colors["light" | "dark"];
@@ -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 2px 6px rgba(0, 0, 0, 0.05), 0px 4px 12px rgba(0, 0, 0, 0.03)" }
35
+ * - getShadowStyle('base') → { boxShadow: "0px 2px 6px rgba(0, 0, 0, 0.1), 0px 4px 12px rgba(0, 0, 0, 0.06)" } (light)
36
36
  */
37
37
  export declare function useTheme(): ExtendedColorScheme & {
38
38
  toggleTheme: () => void;