@mrmeg/expo-ui 0.27.1 → 0.29.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 (155) hide show
  1. package/CHANGELOG.md +666 -0
  2. package/LLM_USAGE.md +241 -37
  3. package/README.md +591 -136
  4. package/bin/expo-ui-icons.mjs +509 -0
  5. package/dist/components/Accordion.js +12 -6
  6. package/dist/components/AnimatedView.d.ts +1 -1
  7. package/dist/components/AnimatedView.js +5 -2
  8. package/dist/components/Avatar.d.ts +1 -1
  9. package/dist/components/Avatar.js +2 -2
  10. package/dist/components/Badge.js +5 -1
  11. package/dist/components/BottomSheet.d.ts +29 -1
  12. package/dist/components/BottomSheet.js +107 -30
  13. package/dist/components/Button.d.ts +20 -4
  14. package/dist/components/Button.js +60 -43
  15. package/dist/components/Card.d.ts +1 -1
  16. package/dist/components/Card.js +24 -4
  17. package/dist/components/Carousel.js +2 -2
  18. package/dist/components/Checkbox.js +51 -44
  19. package/dist/components/Collapsible.js +7 -3
  20. package/dist/components/Dialog.js +87 -16
  21. package/dist/components/Drawer.d.ts +1 -1
  22. package/dist/components/Drawer.js +65 -55
  23. package/dist/components/DropdownMenu.js +11 -7
  24. package/dist/components/EmptyState.d.ts +2 -2
  25. package/dist/components/EmptyState.js +5 -2
  26. package/dist/components/Hydrated.d.ts +31 -0
  27. package/dist/components/Hydrated.js +30 -0
  28. package/dist/components/Icon.d.ts +71 -28
  29. package/dist/components/Icon.js +70 -28
  30. package/dist/components/IconGlyph.d.ts +15 -0
  31. package/dist/components/IconGlyph.js +32 -0
  32. package/dist/components/IconGlyph.native.d.ts +10 -0
  33. package/dist/components/IconGlyph.native.js +30 -0
  34. package/dist/components/InputOTP.js +5 -2
  35. package/dist/components/Item.d.ts +103 -4
  36. package/dist/components/Item.js +168 -15
  37. package/dist/components/KeyboardAvoidingView.d.ts +1 -1
  38. package/dist/components/KeyboardAvoidingView.js +4 -1
  39. package/dist/components/Label.js +3 -2
  40. package/dist/components/MaxWidthContainer.js +1 -1
  41. package/dist/components/Notification.js +45 -27
  42. package/dist/components/Popover.d.ts +18 -5
  43. package/dist/components/Popover.js +72 -8
  44. package/dist/components/Progress.js +8 -6
  45. package/dist/components/RadioGroup.js +52 -49
  46. package/dist/components/Screen.d.ts +61 -0
  47. package/dist/components/Screen.js +62 -0
  48. package/dist/components/SectionHeader.d.ts +1 -1
  49. package/dist/components/SegmentedControl.d.ts +12 -4
  50. package/dist/components/SegmentedControl.js +94 -4
  51. package/dist/components/Select.js +21 -26
  52. package/dist/components/Skeleton.js +24 -10
  53. package/dist/components/Slider.d.ts +11 -3
  54. package/dist/components/Slider.js +73 -6
  55. package/dist/components/StatCard.d.ts +4 -2
  56. package/dist/components/StatCard.js +3 -1
  57. package/dist/components/StyledText.d.ts +7 -1
  58. package/dist/components/StyledText.js +5 -3
  59. package/dist/components/Switch.d.ts +1 -1
  60. package/dist/components/Switch.js +20 -30
  61. package/dist/components/Tabs.d.ts +2 -2
  62. package/dist/components/Tabs.js +13 -24
  63. package/dist/components/TextInput.d.ts +1 -1
  64. package/dist/components/TextInput.js +71 -26
  65. package/dist/components/ThemeSelector.d.ts +52 -0
  66. package/dist/components/ThemeSelector.js +90 -0
  67. package/dist/components/Toggle.d.ts +5 -49
  68. package/dist/components/Toggle.js +53 -29
  69. package/dist/components/ToggleGroup.d.ts +4 -8
  70. package/dist/components/ToggleGroup.js +52 -34
  71. package/dist/components/Tooltip.d.ts +1 -1
  72. package/dist/components/Tooltip.js +9 -3
  73. package/dist/components/UIProvider.d.ts +22 -2
  74. package/dist/components/UIProvider.js +15 -3
  75. package/dist/components/bottomSheetDismiss.d.ts +19 -0
  76. package/dist/components/bottomSheetDismiss.js +44 -0
  77. package/dist/components/iconBuiltins.generated.d.ts +143 -0
  78. package/dist/components/iconBuiltins.generated.js +25 -0
  79. package/dist/components/iconTypes.d.ts +37 -0
  80. package/dist/components/iconTypes.js +1 -0
  81. package/dist/components/index.d.ts +47 -44
  82. package/dist/components/index.js +3 -0
  83. package/dist/components/keyboardDismiss.js +12 -4
  84. package/dist/components/nativeTextField.android.js +13 -9
  85. package/dist/constants/colors.d.ts +40 -5
  86. package/dist/constants/colors.js +26 -8
  87. package/dist/constants/fonts.d.ts +41 -2
  88. package/dist/constants/fonts.js +46 -7
  89. package/dist/constants/index.d.ts +5 -4
  90. package/dist/constants/index.js +1 -0
  91. package/dist/constants/interaction.d.ts +18 -0
  92. package/dist/constants/interaction.js +18 -0
  93. package/dist/constants/spacing.d.ts +2 -1
  94. package/dist/constants/spacing.js +2 -1
  95. package/dist/design-system.json +429 -132
  96. package/dist/hooks/index.d.ts +12 -7
  97. package/dist/hooks/index.js +5 -0
  98. package/dist/hooks/useDimensions.d.ts +13 -8
  99. package/dist/hooks/useDimensions.js +123 -48
  100. package/dist/hooks/useFocusVisible.d.ts +30 -0
  101. package/dist/hooks/useFocusVisible.js +45 -0
  102. package/dist/hooks/useFontStyle.d.ts +2 -2
  103. package/dist/hooks/useFontStyle.js +3 -2
  104. package/dist/hooks/useHydrated.d.ts +13 -0
  105. package/dist/hooks/useHydrated.js +19 -0
  106. package/dist/hooks/useNotificationOffset.d.ts +20 -0
  107. package/dist/hooks/useNotificationOffset.js +34 -0
  108. package/dist/hooks/useResources.d.ts +21 -20
  109. package/dist/hooks/useResources.js +72 -68
  110. package/dist/hooks/useScalePress.d.ts +8 -5
  111. package/dist/hooks/useScalePress.js +16 -9
  112. package/dist/hooks/useShape.d.ts +24 -0
  113. package/dist/hooks/useShape.js +24 -0
  114. package/dist/hooks/useStaggeredEntrance.js +9 -7
  115. package/dist/hooks/useTheme.d.ts +3 -3
  116. package/dist/hooks/useTheme.js +177 -66
  117. package/dist/hooks/useWindowInsets.d.ts +13 -0
  118. package/dist/hooks/useWindowInsets.js +23 -0
  119. package/dist/icons/LICENSE.lucide +43 -0
  120. package/dist/icons/lucide.json +2144 -0
  121. package/dist/index.d.ts +5 -5
  122. package/dist/lib/composeRefs.d.ts +8 -0
  123. package/dist/lib/composeRefs.js +20 -0
  124. package/dist/lib/haptics.d.ts +12 -0
  125. package/dist/lib/haptics.js +21 -0
  126. package/dist/lib/index.d.ts +6 -5
  127. package/dist/lib/index.js +1 -0
  128. package/dist/lib/interFonts.d.ts +15 -0
  129. package/dist/lib/interFonts.js +1 -0
  130. package/dist/lib/interFonts.native.d.ts +3 -0
  131. package/dist/lib/interFonts.native.js +20 -0
  132. package/dist/lib/popoverPlacement.d.ts +36 -0
  133. package/dist/lib/popoverPlacement.js +29 -0
  134. package/dist/lib/themedStyles.d.ts +1 -1
  135. package/dist/lib/useAnimatedValue.d.ts +15 -0
  136. package/dist/lib/useAnimatedValue.js +19 -0
  137. package/dist/lib/webHitTarget.d.ts +40 -0
  138. package/dist/lib/webHitTarget.js +37 -0
  139. package/dist/state/feedbackStore.d.ts +24 -0
  140. package/dist/state/feedbackStore.js +16 -0
  141. package/dist/state/globalUIStore.d.ts +27 -0
  142. package/dist/state/globalUIStore.js +23 -0
  143. package/dist/state/index.d.ts +7 -5
  144. package/dist/state/index.js +2 -0
  145. package/dist/state/initialScheme.d.ts +27 -0
  146. package/dist/state/initialScheme.js +29 -0
  147. package/dist/state/notify.d.ts +1 -1
  148. package/dist/state/themeColorScope.d.ts +1 -1
  149. package/dist/state/themeStore.d.ts +69 -16
  150. package/dist/state/themeStore.js +65 -38
  151. package/llms-full.md +59 -10
  152. package/llms.txt +10 -1
  153. package/package.json +79 -4
  154. package/dist/components/iconRegistry.generated.d.ts +0 -148
  155. package/dist/components/iconRegistry.generated.js +0 -296
@@ -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;
@@ -1,8 +1,10 @@
1
1
  import { useCallback, useEffect, useMemo } from "react";
2
- import { colors, resolveRawColor } from "../constants/colors.js";
2
+ import { useShallow } from "zustand/react/shallow";
3
+ import { colors, palette, resolveRawColor } from "../constants/colors.js";
3
4
  import { Platform, StyleSheet } from "react-native";
4
5
  import { resolveThemePreference, useThemeStore } from "../state/themeStore.js";
5
6
  import { useThemeColorScope } from "../state/themeColorScope.js";
7
+ import { useInitialScheme } from "../state/initialScheme.js";
6
8
  import { spacing as spacingConstants } from "../constants/spacing.js";
7
9
  // Module-level cache for contrast calculations to avoid memory leak
8
10
  // and share across components
@@ -22,6 +24,144 @@ function getCachedOrCompute(key, compute) {
22
24
  contrastCache.set(key, result);
23
25
  return result;
24
26
  }
27
+ /**
28
+ * Web: keeps `<html data-theme>` and `color-scheme` — what the CSS variables
29
+ * from `getThemeCssVariables()` key on — on the store's resolved scheme.
30
+ *
31
+ * One store subscription for the whole app, started by the first `useTheme`
32
+ * consumer to mount (the same moment the old per-consumer effect first
33
+ * wrote), writing only when the resolved scheme changes. It used to be an
34
+ * effect in every consumer, which re-ran in each of them on every scheme
35
+ * switch.
36
+ */
37
+ let documentThemeSyncStarted = false;
38
+ let documentScheme = null;
39
+ function writeDocumentScheme(state) {
40
+ const scheme = resolveThemePreference(state.userTheme, state.systemTheme);
41
+ if (scheme === documentScheme)
42
+ return;
43
+ documentScheme = scheme;
44
+ document.documentElement.dataset.theme = scheme;
45
+ document.documentElement.style.colorScheme = scheme;
46
+ }
47
+ function startDocumentThemeSync() {
48
+ if (documentThemeSyncStarted || Platform.OS !== "web" || typeof document === "undefined") {
49
+ return;
50
+ }
51
+ documentThemeSyncStarted = true;
52
+ writeDocumentScheme(useThemeStore.getState());
53
+ useThemeStore.subscribe(writeDocumentScheme);
54
+ }
55
+ /**
56
+ * Shadow presets, per scheme.
57
+ *
58
+ * Light: the shadow does the work, at about twice the alpha the original
59
+ * presets used (0.04 became 0.08). Dark: the original presets tripled a 0.04
60
+ * alpha, which is invisible on the near-black base; these are black at real
61
+ * alpha, with the edge highlight in front. `subtle`, `soft` and `elevated`
62
+ * were tuned in the Surface Lab (`client/showcase/SurfaceLabScreen.tsx`); the
63
+ * other raised presets sit between them. `sharp` keeps its original light
64
+ * alpha and its original tripled dark alpha; `glow` is one vivid layer in both.
65
+ */
66
+ const SHADOW_CONFIGS = {
67
+ subtle: {
68
+ light: [
69
+ { x: 0, y: 1, blur: 3, opacity: 0.08 },
70
+ { x: 0, y: 2, blur: 8, opacity: 0.06 },
71
+ ],
72
+ dark: [
73
+ { x: 0, y: 1, blur: 2, opacity: 0.4 },
74
+ { x: 0, y: 8, blur: 24, opacity: 0.35 },
75
+ ],
76
+ highlight: 0.05,
77
+ },
78
+ base: {
79
+ light: [
80
+ { x: 0, y: 2, blur: 6, opacity: 0.1 },
81
+ { x: 0, y: 4, blur: 12, opacity: 0.06 },
82
+ ],
83
+ dark: [
84
+ { x: 0, y: 2, blur: 3, opacity: 0.4 },
85
+ { x: 0, y: 10, blur: 28, opacity: 0.38 },
86
+ ],
87
+ },
88
+ soft: {
89
+ light: [
90
+ { x: 0, y: 4, blur: 10, opacity: 0.1 },
91
+ { x: 0, y: 8, blur: 20, opacity: 0.06 },
92
+ ],
93
+ dark: [
94
+ { x: 0, y: 2, blur: 4, opacity: 0.45 },
95
+ { x: 0, y: 14, blur: 36, opacity: 0.45 },
96
+ ],
97
+ highlight: 0.06,
98
+ },
99
+ card: {
100
+ light: [
101
+ { x: 0, y: 4, blur: 12, opacity: 0.1 },
102
+ { x: 0, y: 8, blur: 24, opacity: 0.06 },
103
+ ],
104
+ dark: [
105
+ { x: 0, y: 2, blur: 4, opacity: 0.42 },
106
+ { x: 0, y: 10, blur: 30, opacity: 0.4 },
107
+ ],
108
+ highlight: 0.05,
109
+ },
110
+ cardSubtle: {
111
+ light: [
112
+ { x: 0, y: 1, blur: 3, opacity: 0.1 },
113
+ { x: 0, y: 3, blur: 9, opacity: 0.06 },
114
+ ],
115
+ dark: [
116
+ { x: 0, y: 1, blur: 2, opacity: 0.4 },
117
+ { x: 0, y: 4, blur: 12, opacity: 0.3 },
118
+ ],
119
+ highlight: 0.05,
120
+ },
121
+ cardHover: {
122
+ light: [
123
+ { x: 0, y: 8, blur: 24, opacity: 0.12 },
124
+ { x: 0, y: 16, blur: 48, opacity: 0.08 },
125
+ ],
126
+ dark: [
127
+ { x: 0, y: 3, blur: 6, opacity: 0.48 },
128
+ { x: 0, y: 18, blur: 48, opacity: 0.5 },
129
+ ],
130
+ highlight: 0.06,
131
+ },
132
+ elevated: {
133
+ light: [
134
+ { x: 0, y: 16, blur: 48, opacity: 0.16 },
135
+ { x: 0, y: 32, blur: 96, opacity: 0.1 },
136
+ ],
137
+ dark: [
138
+ { x: 0, y: 4, blur: 8, opacity: 0.5 },
139
+ { x: 0, y: 24, blur: 56, opacity: 0.55 },
140
+ ],
141
+ highlight: 0.06,
142
+ },
143
+ glass: {
144
+ light: [
145
+ { x: 0, y: 4, blur: 30, opacity: 0.1 },
146
+ { x: 0, y: 8, blur: 60, opacity: 0.06 },
147
+ ],
148
+ dark: [
149
+ { x: 0, y: 4, blur: 30, opacity: 0.4 },
150
+ { x: 0, y: 8, blur: 60, opacity: 0.3 },
151
+ ],
152
+ highlight: 0.05,
153
+ },
154
+ // Single layer — intentional crispness, not a soft dual-layer shadow.
155
+ sharp: {
156
+ light: [{ x: 0, y: 1, blur: 1, opacity: 0.15 }],
157
+ dark: [{ x: 0, y: 1, blur: 1, opacity: 0.45 }],
158
+ },
159
+ // Single layer — already a deliberate, vivid accent glow.
160
+ glow: {
161
+ light: [{ x: 0, y: 4, blur: 20, opacity: 0.4 }],
162
+ dark: [{ x: 0, y: 4, blur: 20, opacity: 0.4 }],
163
+ },
164
+ };
25
165
  /**
26
166
  * useTheme
27
167
  *
@@ -42,16 +182,26 @@ function getCachedOrCompute(key, compute) {
42
182
  * - getTextColorForBackground("#000") → "light"
43
183
  * - getContrastingColor("#f4f4f4", "#222", "#fff") → "#222"
44
184
  * - withAlpha("#336699", 0.6) → "rgba(51,102,153,0.6)"
45
- * - getShadowStyle('base') → { boxShadow: "0px 2px 6px rgba(0, 0, 0, 0.05), 0px 4px 12px rgba(0, 0, 0, 0.03)" }
185
+ * - getShadowStyle('base') → { boxShadow: "0px 2px 6px rgba(0, 0, 0, 0.1), 0px 4px 12px rgba(0, 0, 0, 0.06)" } (light)
46
186
  */
47
187
  export function useTheme() {
48
- const userTheme = useThemeStore((s) => s.userTheme);
49
- const systemTheme = useThemeStore((s) => s.systemTheme);
50
- const setTheme = useThemeStore((s) => s.setTheme);
51
- const colorOverrides = useThemeStore((s) => s.colorOverrides);
188
+ // One store subscription per consumer, re-rendering only when one of these
189
+ // four fields changes (shallow compare), instead of four subscriptions.
190
+ const { userTheme, systemTheme, hasLoadedTheme, setTheme, colorOverrides } = useThemeStore(useShallow((s) => ({
191
+ userTheme: s.userTheme,
192
+ systemTheme: s.systemTheme,
193
+ hasLoadedTheme: s.hasLoadedTheme,
194
+ setTheme: s.setTheme,
195
+ colorOverrides: s.colorOverrides,
196
+ })));
52
197
  const scoped = useThemeColorScope();
53
- // Determine which theme to use (user preference or system)
54
- const effectiveScheme = resolveThemePreference(userTheme, systemTheme);
198
+ const initialScheme = useInitialScheme();
199
+ // Determine which theme to use: the app's first-render hint until the
200
+ // persisted preference has been read (server render, hydration), then the
201
+ // user preference or system scheme. See state/initialScheme.tsx.
202
+ const effectiveScheme = initialScheme !== undefined && !hasLoadedTheme
203
+ ? initialScheme
204
+ : resolveThemePreference(userTheme, systemTheme);
55
205
  const base = colors[effectiveScheme];
56
206
  // Layer overrides on top of the package defaults for the active scheme, in
57
207
  // precedence order: package default → global store (app brand) → scoped
@@ -72,13 +222,9 @@ export function useTheme() {
72
222
  colors: { ...base.colors, ...storeOverride, ...scopedOverride },
73
223
  };
74
224
  }, [base, storeOverride, scopedOverride]);
75
- // Sync theme to DOM so CSS in +html.tsx follows the app's runtime theme
76
- useEffect(() => {
77
- if (Platform.OS === "web" && typeof document !== "undefined") {
78
- document.documentElement.dataset.theme = effectiveScheme;
79
- document.documentElement.style.colorScheme = effectiveScheme;
80
- }
81
- }, [effectiveScheme]);
225
+ // Sync theme to DOM so CSS in +html.tsx follows the app's runtime theme.
226
+ // Starts the app-wide sync on the first mount; a no-op in every later one.
227
+ useEffect(startDocumentThemeSync, []);
82
228
  // Toggle between light, dark, and system themes
83
229
  const toggleTheme = useCallback(() => {
84
230
  if (userTheme === "light") {
@@ -105,61 +251,26 @@ export function useTheme() {
105
251
  * single-layer) is a dual-layer shadow: a tight "contact" layer plus a
106
252
  * wider, softer "ambient" layer, pitsi-ui-style. Layers are comma-joined
107
253
  * into one `boxShadow` value.
254
+ *
255
+ * Light and dark have their own layers. Light is the shadow alone. Dark is
256
+ * edge-lit: a 1px inset white highlight along the top edge, plus black
257
+ * layers at real alpha, deeper and larger than light's. On the near-black
258
+ * dark base a faint drop shadow is invisible, so the highlight carries the
259
+ * "lit from above" cue and the shadow only grounds the surface. The
260
+ * highlight is on the raised presets; `sharp` and `glow` have none.
108
261
  */
109
262
  const getShadowStyle = useCallback((type) => {
110
- // Each preset is one or more layers of [offsetX, offsetY, blurRadius, color, opacity].
111
- // Darker themes get a stronger alpha so shadows stay visible.
112
- const boost = theme.dark ? 3 : 1;
113
263
  const overlay = theme.colors.overlay;
114
- const shadowConfigs = {
115
- subtle: [
116
- { x: 0, y: 1, blur: 3, color: overlay, opacity: 0.04 },
117
- { x: 0, y: 2, blur: 8, color: overlay, opacity: 0.03 },
118
- ],
119
- base: [
120
- { x: 0, y: 2, blur: 6, color: overlay, opacity: 0.05 },
121
- { x: 0, y: 4, blur: 12, color: overlay, opacity: 0.03 },
122
- ],
123
- soft: [
124
- { x: 0, y: 4, blur: 10, color: overlay, opacity: 0.05 },
125
- { x: 0, y: 8, blur: 20, color: overlay, opacity: 0.03 },
126
- ],
127
- card: [
128
- { x: 0, y: 4, blur: 12, color: overlay, opacity: 0.05 },
129
- { x: 0, y: 8, blur: 24, color: overlay, opacity: 0.03 },
130
- ],
131
- cardSubtle: [
132
- { x: 0, y: 1, blur: 3, color: overlay, opacity: 0.05 },
133
- { x: 0, y: 3, blur: 9, color: overlay, opacity: 0.03 },
134
- ],
135
- cardHover: [
136
- { x: 0, y: 8, blur: 24, color: overlay, opacity: 0.06 },
137
- { x: 0, y: 16, blur: 48, color: overlay, opacity: 0.04 },
138
- ],
139
- elevated: [
140
- { x: 0, y: 16, blur: 48, color: overlay, opacity: 0.08 },
141
- { x: 0, y: 32, blur: 96, color: overlay, opacity: 0.05 },
142
- ],
143
- glass: [
144
- { x: 0, y: 4, blur: 30, color: overlay, opacity: 0.05 },
145
- { x: 0, y: 8, blur: 60, color: overlay, opacity: 0.03 },
146
- ],
147
- // Single layer — intentional crispness, not a soft dual-layer shadow.
148
- sharp: [
149
- { x: 0, y: 1, blur: 1, color: overlay, opacity: 0.15 },
150
- ],
151
- // Single layer — already a deliberate, vivid accent glow.
152
- glow: [
153
- { x: 0, y: 4, blur: 20, color: theme.colors.primary, opacity: 0.4 },
154
- ],
155
- };
156
- const layers = shadowConfigs[type].map(({ x, y, blur, color, opacity }) => {
157
- // Don't boost the glow accent — it's already a deliberate, vivid alpha.
158
- const alpha = color === theme.colors.primary ? opacity : Math.min(opacity * boost, 1);
159
- return `${x}px ${y}px ${blur}px ${withAlpha(color, alpha)}`;
160
- });
264
+ const config = SHADOW_CONFIGS[type];
265
+ const layers = theme.dark ? config.dark : config.light;
266
+ // Don't tint `glow` with the overlay: it is a deliberate, vivid accent glow.
267
+ const color = type === "glow" ? theme.colors.primary : overlay;
268
+ const parts = layers.map(({ x, y, blur, opacity }) => `${x}px ${y}px ${blur}px ${withAlpha(color, opacity)}`);
269
+ if (theme.dark && config.highlight !== undefined) {
270
+ parts.unshift(`inset 0px 1px 0px ${withAlpha(palette.white, config.highlight)}`);
271
+ }
161
272
  return {
162
- boxShadow: layers.join(", "),
273
+ boxShadow: parts.join(", "),
163
274
  };
164
275
  }, [theme]);
165
276
  const getFocusRingStyle = useCallback((offset = 2) => {
@@ -0,0 +1,13 @@
1
+ import { type EdgeInsets } from "react-native-safe-area-context";
2
+ /**
3
+ * The window's safe-area insets, wherever the component renders.
4
+ *
5
+ * `useSafeAreaInsets()` throws outside a `SafeAreaProvider` and reads zeros
6
+ * inside content the system presents outside the provider's view — an iOS
7
+ * `Modal`, a native bottom sheet, a `UIProvider` mounted above the app's
8
+ * provider. This reads the provider's context when it is there and falls back
9
+ * to `initialWindowMetrics` (the window the app launched in), edge by edge, so
10
+ * overlays, toasts and `Screen` clear the Dynamic Island and home indicator in
11
+ * every one of those cases. Zeros on web and in tests without a provider.
12
+ */
13
+ export declare function useWindowInsets(): EdgeInsets;
@@ -0,0 +1,23 @@
1
+ import { use } from "react";
2
+ import { SafeAreaInsetsContext, initialWindowMetrics, } from "react-native-safe-area-context";
3
+ /**
4
+ * The window's safe-area insets, wherever the component renders.
5
+ *
6
+ * `useSafeAreaInsets()` throws outside a `SafeAreaProvider` and reads zeros
7
+ * inside content the system presents outside the provider's view — an iOS
8
+ * `Modal`, a native bottom sheet, a `UIProvider` mounted above the app's
9
+ * provider. This reads the provider's context when it is there and falls back
10
+ * to `initialWindowMetrics` (the window the app launched in), edge by edge, so
11
+ * overlays, toasts and `Screen` clear the Dynamic Island and home indicator in
12
+ * every one of those cases. Zeros on web and in tests without a provider.
13
+ */
14
+ export function useWindowInsets() {
15
+ const context = use(SafeAreaInsetsContext);
16
+ const fallback = initialWindowMetrics?.insets;
17
+ return {
18
+ top: context?.top || fallback?.top || 0,
19
+ bottom: context?.bottom || fallback?.bottom || 0,
20
+ left: context?.left || fallback?.left || 0,
21
+ right: context?.right || fallback?.right || 0,
22
+ };
23
+ }
@@ -0,0 +1,43 @@
1
+ ISC License
2
+
3
+ Copyright (c) 2026 Lucide Icons and Contributors
4
+
5
+ Permission to use, copy, modify, and/or distribute this software for any
6
+ purpose with or without fee is hereby granted, provided that the above
7
+ copyright notice and this permission notice appear in all copies.
8
+
9
+ THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
10
+ WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
11
+ MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
12
+ ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
13
+ WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
14
+ ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
15
+ OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
16
+
17
+ ---
18
+
19
+ The following Lucide icons are derived from the Feather project:
20
+
21
+ airplay, alert-circle, alert-octagon, alert-triangle, aperture, arrow-down-circle, arrow-down-left, arrow-down-right, arrow-down, arrow-left-circle, arrow-left, arrow-right-circle, arrow-right, arrow-up-circle, arrow-up-left, arrow-up-right, arrow-up, at-sign, calendar, cast, check, chevron-down, chevron-left, chevron-right, chevron-up, chevrons-down, chevrons-left, chevrons-right, chevrons-up, circle, clipboard, clock, code, columns, command, compass, corner-down-left, corner-down-right, corner-left-down, corner-left-up, corner-right-down, corner-right-up, corner-up-left, corner-up-right, crosshair, database, divide-circle, divide-square, dollar-sign, download, external-link, feather, frown, hash, headphones, help-circle, info, italic, key, layout, life-buoy, link-2, link, loader, lock, log-in, log-out, maximize, meh, minimize, minimize-2, minus-circle, minus-square, minus, monitor, moon, more-horizontal, more-vertical, move, music, navigation-2, navigation, octagon, pause-circle, percent, plus-circle, plus-square, plus, power, radio, rss, search, server, share, shopping-bag, sidebar, smartphone, smile, square, table-2, tablet, target, terminal, trash-2, trash, triangle, tv, type, upload, x-circle, x-octagon, x-square, x, zoom-in, zoom-out
22
+
23
+ The MIT License (MIT) (for the icons listed above)
24
+
25
+ Copyright (c) 2013-present Cole Bemis
26
+
27
+ Permission is hereby granted, free of charge, to any person obtaining a copy
28
+ of this software and associated documentation files (the "Software"), to deal
29
+ in the Software without restriction, including without limitation the rights
30
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
31
+ copies of the Software, and to permit persons to whom the Software is
32
+ furnished to do so, subject to the following conditions:
33
+
34
+ The above copyright notice and this permission notice shall be included in all
35
+ copies or substantial portions of the Software.
36
+
37
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
38
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
39
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
40
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
41
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
42
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
43
+ SOFTWARE.