@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.
- package/CHANGELOG.md +666 -0
- package/LLM_USAGE.md +241 -37
- package/README.md +591 -136
- package/bin/expo-ui-icons.mjs +509 -0
- 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 +71 -28
- package/dist/components/Icon.js +70 -28
- package/dist/components/IconGlyph.d.ts +15 -0
- package/dist/components/IconGlyph.js +32 -0
- package/dist/components/IconGlyph.native.d.ts +10 -0
- package/dist/components/IconGlyph.native.js +30 -0
- 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/ThemeSelector.d.ts +52 -0
- package/dist/components/ThemeSelector.js +90 -0
- package/dist/components/Toggle.d.ts +5 -49
- package/dist/components/Toggle.js +53 -29
- package/dist/components/ToggleGroup.d.ts +4 -8
- package/dist/components/ToggleGroup.js +52 -34
- 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/iconBuiltins.generated.d.ts +143 -0
- package/dist/components/iconBuiltins.generated.js +25 -0
- package/dist/components/iconTypes.d.ts +37 -0
- package/dist/components/iconTypes.js +1 -0
- package/dist/components/index.d.ts +47 -44
- package/dist/components/index.js +3 -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 +429 -132
- 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/icons/LICENSE.lucide +43 -0
- package/dist/icons/lucide.json +2144 -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 +59 -10
- package/llms.txt +10 -1
- package/package.json +79 -4
- package/dist/components/iconRegistry.generated.d.ts +0 -148
- package/dist/components/iconRegistry.generated.js +0 -296
|
@@ -6,10 +6,12 @@ interface ScalePressOptions {
|
|
|
6
6
|
*/
|
|
7
7
|
scaleTo?: number;
|
|
8
8
|
/**
|
|
9
|
-
*
|
|
10
|
-
*
|
|
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
|
|
33
|
-
*
|
|
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
|
|
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
|
+
}
|
|
@@ -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.
|