@mrmeg/expo-ui 0.27.1 → 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.
- package/CHANGELOG.md +598 -0
- package/LLM_USAGE.md +233 -35
- package/README.md +504 -103
- package/dist/components/Accordion.js +12 -6
- package/dist/components/AnimatedView.d.ts +1 -1
- package/dist/components/AnimatedView.js +5 -2
- package/dist/components/Avatar.d.ts +1 -1
- package/dist/components/Avatar.js +2 -2
- package/dist/components/Badge.js +5 -1
- package/dist/components/BottomSheet.d.ts +29 -1
- package/dist/components/BottomSheet.js +107 -30
- package/dist/components/Button.d.ts +20 -4
- package/dist/components/Button.js +60 -43
- package/dist/components/Card.d.ts +1 -1
- package/dist/components/Card.js +24 -4
- package/dist/components/Carousel.js +2 -2
- package/dist/components/Checkbox.js +51 -44
- package/dist/components/Collapsible.js +7 -3
- package/dist/components/Dialog.js +87 -16
- package/dist/components/Drawer.d.ts +1 -1
- package/dist/components/Drawer.js +65 -55
- package/dist/components/DropdownMenu.js +11 -7
- package/dist/components/EmptyState.d.ts +2 -2
- package/dist/components/EmptyState.js +5 -2
- package/dist/components/Hydrated.d.ts +31 -0
- package/dist/components/Hydrated.js +30 -0
- package/dist/components/Icon.d.ts +17 -5
- package/dist/components/Icon.js +31 -10
- package/dist/components/InputOTP.js +5 -2
- package/dist/components/Item.d.ts +103 -4
- package/dist/components/Item.js +168 -15
- package/dist/components/KeyboardAvoidingView.d.ts +1 -1
- package/dist/components/KeyboardAvoidingView.js +4 -1
- package/dist/components/Label.js +3 -2
- package/dist/components/MaxWidthContainer.js +1 -1
- package/dist/components/Notification.js +45 -27
- package/dist/components/Popover.d.ts +18 -5
- package/dist/components/Popover.js +72 -8
- package/dist/components/Progress.js +8 -6
- package/dist/components/RadioGroup.js +52 -49
- package/dist/components/Screen.d.ts +61 -0
- package/dist/components/Screen.js +62 -0
- package/dist/components/SectionHeader.d.ts +1 -1
- package/dist/components/SegmentedControl.d.ts +12 -4
- package/dist/components/SegmentedControl.js +94 -4
- package/dist/components/Select.js +21 -26
- package/dist/components/Skeleton.js +24 -10
- package/dist/components/Slider.d.ts +11 -3
- package/dist/components/Slider.js +73 -6
- package/dist/components/StatCard.d.ts +4 -2
- package/dist/components/StatCard.js +3 -1
- package/dist/components/StyledText.d.ts +7 -1
- package/dist/components/StyledText.js +5 -3
- package/dist/components/Switch.d.ts +1 -1
- package/dist/components/Switch.js +20 -30
- package/dist/components/Tabs.d.ts +2 -2
- package/dist/components/Tabs.js +13 -24
- package/dist/components/TextInput.d.ts +1 -1
- package/dist/components/TextInput.js +71 -26
- package/dist/components/Toggle.d.ts +2 -42
- package/dist/components/Toggle.js +38 -27
- package/dist/components/ToggleGroup.d.ts +1 -1
- package/dist/components/ToggleGroup.js +37 -32
- package/dist/components/Tooltip.d.ts +1 -1
- package/dist/components/Tooltip.js +9 -3
- package/dist/components/UIProvider.d.ts +22 -2
- package/dist/components/UIProvider.js +15 -3
- package/dist/components/bottomSheetDismiss.d.ts +19 -0
- package/dist/components/bottomSheetDismiss.js +44 -0
- package/dist/components/index.d.ts +46 -44
- package/dist/components/index.js +2 -0
- package/dist/components/keyboardDismiss.js +12 -4
- package/dist/components/nativeTextField.android.js +13 -9
- package/dist/constants/colors.d.ts +40 -5
- package/dist/constants/colors.js +26 -8
- package/dist/constants/fonts.d.ts +41 -2
- package/dist/constants/fonts.js +46 -7
- package/dist/constants/index.d.ts +5 -4
- package/dist/constants/index.js +1 -0
- package/dist/constants/interaction.d.ts +18 -0
- package/dist/constants/interaction.js +18 -0
- package/dist/constants/spacing.d.ts +2 -1
- package/dist/constants/spacing.js +2 -1
- package/dist/design-system.json +154 -2
- package/dist/hooks/index.d.ts +12 -7
- package/dist/hooks/index.js +5 -0
- package/dist/hooks/useDimensions.d.ts +13 -8
- package/dist/hooks/useDimensions.js +123 -48
- package/dist/hooks/useFocusVisible.d.ts +30 -0
- package/dist/hooks/useFocusVisible.js +45 -0
- package/dist/hooks/useFontStyle.d.ts +2 -2
- package/dist/hooks/useFontStyle.js +3 -2
- package/dist/hooks/useHydrated.d.ts +13 -0
- package/dist/hooks/useHydrated.js +19 -0
- package/dist/hooks/useNotificationOffset.d.ts +20 -0
- package/dist/hooks/useNotificationOffset.js +34 -0
- package/dist/hooks/useResources.d.ts +21 -20
- package/dist/hooks/useResources.js +72 -68
- package/dist/hooks/useScalePress.d.ts +8 -5
- package/dist/hooks/useScalePress.js +16 -9
- package/dist/hooks/useShape.d.ts +24 -0
- package/dist/hooks/useShape.js +24 -0
- package/dist/hooks/useStaggeredEntrance.js +9 -7
- package/dist/hooks/useTheme.d.ts +3 -3
- package/dist/hooks/useTheme.js +177 -66
- package/dist/hooks/useWindowInsets.d.ts +13 -0
- package/dist/hooks/useWindowInsets.js +23 -0
- package/dist/index.d.ts +5 -5
- package/dist/lib/composeRefs.d.ts +8 -0
- package/dist/lib/composeRefs.js +20 -0
- package/dist/lib/haptics.d.ts +12 -0
- package/dist/lib/haptics.js +21 -0
- package/dist/lib/index.d.ts +6 -5
- package/dist/lib/index.js +1 -0
- package/dist/lib/interFonts.d.ts +15 -0
- package/dist/lib/interFonts.js +1 -0
- package/dist/lib/interFonts.native.d.ts +3 -0
- package/dist/lib/interFonts.native.js +20 -0
- package/dist/lib/popoverPlacement.d.ts +36 -0
- package/dist/lib/popoverPlacement.js +29 -0
- package/dist/lib/themedStyles.d.ts +1 -1
- package/dist/lib/useAnimatedValue.d.ts +15 -0
- package/dist/lib/useAnimatedValue.js +19 -0
- package/dist/lib/webHitTarget.d.ts +40 -0
- package/dist/lib/webHitTarget.js +37 -0
- package/dist/state/feedbackStore.d.ts +24 -0
- package/dist/state/feedbackStore.js +16 -0
- package/dist/state/globalUIStore.d.ts +27 -0
- package/dist/state/globalUIStore.js +23 -0
- package/dist/state/index.d.ts +7 -5
- package/dist/state/index.js +2 -0
- package/dist/state/initialScheme.d.ts +27 -0
- package/dist/state/initialScheme.js +29 -0
- package/dist/state/notify.d.ts +1 -1
- package/dist/state/themeColorScope.d.ts +1 -1
- package/dist/state/themeStore.d.ts +69 -16
- package/dist/state/themeStore.js +65 -38
- package/llms-full.md +56 -8
- package/llms.txt +10 -1
- package/package.json +69 -2
|
@@ -1,12 +1,15 @@
|
|
|
1
|
-
import { useCallback, useMemo
|
|
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
|
|
9
|
-
*
|
|
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 =
|
|
26
|
+
const { scaleTo = 0.97, haptic = "setting", damping = 20, stiffness = 300, disabled = false, } = options;
|
|
24
27
|
const reduceMotion = useReducedMotion();
|
|
25
|
-
const scale =
|
|
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(
|
|
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:
|
|
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
|
|
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 =
|
|
28
|
-
const translateY =
|
|
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)
|
|
35
|
-
const scale =
|
|
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:
|
|
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:
|
|
74
|
+
useNativeDriver: shouldUseNativeDriver,
|
|
73
75
|
}));
|
|
74
76
|
}
|
|
75
77
|
const animation = delay > 0
|
package/dist/hooks/useTheme.d.ts
CHANGED
|
@@ -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.
|
|
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;
|
package/dist/hooks/useTheme.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
import { useCallback, useEffect, useMemo } from "react";
|
|
2
|
-
import {
|
|
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.
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
const setTheme = useThemeStore((s) =>
|
|
51
|
-
|
|
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
|
-
|
|
54
|
-
|
|
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
|
-
|
|
77
|
-
|
|
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
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
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:
|
|
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
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export * from "./components";
|
|
2
|
-
export * from "./constants";
|
|
3
|
-
export * from "./hooks";
|
|
4
|
-
export * from "./state";
|
|
5
|
-
export * from "./lib";
|
|
1
|
+
export * from "./components/index.js";
|
|
2
|
+
export * from "./constants/index.js";
|
|
3
|
+
export * from "./hooks/index.js";
|
|
4
|
+
export * from "./state/index.js";
|
|
5
|
+
export * from "./lib/index.js";
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { Ref, RefCallback } from "react";
|
|
2
|
+
/**
|
|
3
|
+
* One ref callback that feeds every ref given — object refs (`.current`) and
|
|
4
|
+
* callback refs alike, `null`/`undefined` skipped. For a component that needs
|
|
5
|
+
* the host node itself (e.g. to set a DOM attribute) while its consumer, or an
|
|
6
|
+
* `asChild` slot, also passes a `ref`.
|
|
7
|
+
*/
|
|
8
|
+
export declare function composeRefs<T>(...refs: Array<Ref<T> | undefined>): RefCallback<T>;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* One ref callback that feeds every ref given — object refs (`.current`) and
|
|
3
|
+
* callback refs alike, `null`/`undefined` skipped. For a component that needs
|
|
4
|
+
* the host node itself (e.g. to set a DOM attribute) while its consumer, or an
|
|
5
|
+
* `asChild` slot, also passes a `ref`.
|
|
6
|
+
*/
|
|
7
|
+
export function composeRefs(...refs) {
|
|
8
|
+
return (node) => {
|
|
9
|
+
for (const ref of refs) {
|
|
10
|
+
if (!ref)
|
|
11
|
+
continue;
|
|
12
|
+
if (typeof ref === "function") {
|
|
13
|
+
ref(node);
|
|
14
|
+
}
|
|
15
|
+
else {
|
|
16
|
+
ref.current = node;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
}
|
package/dist/lib/haptics.d.ts
CHANGED
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
/** A light tap, unconditionally. Prefer `hapticPress` / `hapticSelection`. */
|
|
1
2
|
export declare function hapticLight(): void;
|
|
2
3
|
export declare function hapticMedium(): void;
|
|
3
4
|
export declare function hapticSuccess(): void;
|
|
5
|
+
/**
|
|
6
|
+
* The tap a control fires when its state changes (Switch, Checkbox, Toggle,
|
|
7
|
+
* ToggleGroup, SegmentedControl). Fires under the `"selection"` (default) and
|
|
8
|
+
* `"all"` settings; silent under `"off"`.
|
|
9
|
+
*/
|
|
10
|
+
export declare function hapticSelection(): void;
|
|
11
|
+
/**
|
|
12
|
+
* The tap a pressable fires on press-in (Button, pressable Card and Item,
|
|
13
|
+
* `useScalePress` consumers). Fires only under the `"all"` setting.
|
|
14
|
+
*/
|
|
15
|
+
export declare function hapticPress(): void;
|
package/dist/lib/haptics.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Platform } from "react-native";
|
|
2
|
+
import { useFeedbackStore } from "../state/feedbackStore.js";
|
|
2
3
|
function runHaptic(fn) {
|
|
3
4
|
if (Platform.OS === "web")
|
|
4
5
|
return;
|
|
@@ -9,6 +10,7 @@ function runHaptic(fn) {
|
|
|
9
10
|
// Haptics not available
|
|
10
11
|
}
|
|
11
12
|
}
|
|
13
|
+
/** A light tap, unconditionally. Prefer `hapticPress` / `hapticSelection`. */
|
|
12
14
|
export function hapticLight() {
|
|
13
15
|
runHaptic(() => {
|
|
14
16
|
const H = require("expo-haptics");
|
|
@@ -27,3 +29,22 @@ export function hapticSuccess() {
|
|
|
27
29
|
H.notificationAsync(H.NotificationFeedbackType.Success);
|
|
28
30
|
});
|
|
29
31
|
}
|
|
32
|
+
/**
|
|
33
|
+
* The tap a control fires when its state changes (Switch, Checkbox, Toggle,
|
|
34
|
+
* ToggleGroup, SegmentedControl). Fires under the `"selection"` (default) and
|
|
35
|
+
* `"all"` settings; silent under `"off"`.
|
|
36
|
+
*/
|
|
37
|
+
export function hapticSelection() {
|
|
38
|
+
if (useFeedbackStore.getState().haptics === "off")
|
|
39
|
+
return;
|
|
40
|
+
hapticLight();
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* The tap a pressable fires on press-in (Button, pressable Card and Item,
|
|
44
|
+
* `useScalePress` consumers). Fires only under the `"all"` setting.
|
|
45
|
+
*/
|
|
46
|
+
export function hapticPress() {
|
|
47
|
+
if (useFeedbackStore.getState().haptics !== "all")
|
|
48
|
+
return;
|
|
49
|
+
hapticLight();
|
|
50
|
+
}
|
package/dist/lib/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
export * from "./animations";
|
|
2
|
-
export * from "./
|
|
3
|
-
export * from "./
|
|
4
|
-
export * from "./
|
|
5
|
-
export * from "./
|
|
1
|
+
export * from "./animations.js";
|
|
2
|
+
export * from "./composeRefs.js";
|
|
3
|
+
export * from "./haptics.js";
|
|
4
|
+
export * from "./i18n.js";
|
|
5
|
+
export * from "./stateSurface.js";
|
|
6
|
+
export * from "./themedStyles.js";
|
package/dist/lib/index.js
CHANGED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The packaged Inter faces `useResources` registers through `expo-font` —
|
|
3
|
+
* web variant: none.
|
|
4
|
+
*
|
|
5
|
+
* Web loads Inter as one multi-weight CSS family from Google Fonts (see
|
|
6
|
+
* `useResources`), and `constants/fonts.ts` points every web weight at that
|
|
7
|
+
* family, so no static `.ttf` would ever be referenced. Importing one here
|
|
8
|
+
* would still make the bundler emit it as a web asset, so this variant imports
|
|
9
|
+
* nothing. Metro resolves `interFonts.native.ts` on iOS and Android instead.
|
|
10
|
+
*
|
|
11
|
+
* Private module: not re-exported from `lib/index.ts`, and `lib/*` is not an
|
|
12
|
+
* exported subpath, so consumers can't reach it.
|
|
13
|
+
*/
|
|
14
|
+
export type InterFontMap = Readonly<Record<string, number>>;
|
|
15
|
+
export declare const interFontMap: InterFontMap | null;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const interFontMap = null;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The packaged Inter faces `useResources` registers through `expo-font` —
|
|
3
|
+
* native variant.
|
|
4
|
+
*
|
|
5
|
+
* Exactly the four static weights the native `sansSerif` family keys point at
|
|
6
|
+
* (see `constants/fonts.ts`), each imported from its own per-weight subpath.
|
|
7
|
+
* The `@expo-google-fonts/inter` root entry `require`s all 18 files (every
|
|
8
|
+
* weight, plus italics), and Metro ships every file a bundle requires, so
|
|
9
|
+
* importing it added 14 unused faces (~4.8 MB) to every native app.
|
|
10
|
+
*/
|
|
11
|
+
import { Inter_400Regular } from "@expo-google-fonts/inter/400Regular";
|
|
12
|
+
import { Inter_500Medium } from "@expo-google-fonts/inter/500Medium";
|
|
13
|
+
import { Inter_600SemiBold } from "@expo-google-fonts/inter/600SemiBold";
|
|
14
|
+
import { Inter_700Bold } from "@expo-google-fonts/inter/700Bold";
|
|
15
|
+
export const interFontMap = {
|
|
16
|
+
Inter_400Regular,
|
|
17
|
+
Inter_500Medium,
|
|
18
|
+
Inter_600SemiBold,
|
|
19
|
+
Inter_700Bold,
|
|
20
|
+
};
|