@mrmeg/expo-ui 0.1.5 → 0.1.7
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/LLM_USAGE.md +28 -0
- package/README.md +34 -17
- package/dist/components/Accordion.js +19 -16
- package/dist/components/Badge.js +5 -4
- package/dist/components/Button.js +84 -51
- package/dist/components/Card.js +4 -3
- package/dist/components/Checkbox.js +6 -4
- package/dist/components/Collapsible.js +15 -14
- package/dist/components/Dialog.js +6 -6
- package/dist/components/Drawer.js +5 -5
- package/dist/components/DropdownMenu.js +119 -112
- package/dist/components/EmptyState.js +5 -3
- package/dist/components/InputOTP.js +3 -3
- package/dist/components/Label.js +5 -2
- package/dist/components/Notification.js +3 -3
- package/dist/components/Popover.js +2 -2
- package/dist/components/RadioGroup.js +6 -4
- package/dist/components/Select.js +35 -25
- package/dist/components/Slider.js +34 -24
- package/dist/components/StyledText.d.ts +13 -2
- package/dist/components/StyledText.js +28 -7
- package/dist/components/Switch.js +28 -28
- package/dist/components/Tabs.js +6 -3
- package/dist/components/TextInput.js +8 -10
- package/dist/components/Toggle.js +4 -2
- package/dist/components/ToggleGroup.js +3 -2
- package/dist/components/Tooltip.js +4 -4
- package/dist/constants/colors.d.ts +4 -0
- package/dist/constants/colors.js +9 -1
- package/dist/constants/spacing.d.ts +2 -1
- package/dist/constants/spacing.js +2 -1
- package/dist/hooks/useTheme.d.ts +9 -6
- package/dist/hooks/useTheme.js +99 -22
- package/package.json +6 -7
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { Icon } from "./Icon.js";
|
|
3
|
-
import { TextClassContext, TextColorContext } from "./StyledText.js";
|
|
3
|
+
import { TextClassContext, TextColorContext, TextSelectabilityContext } from "./StyledText.js";
|
|
4
4
|
import { spacing } from "../constants/spacing.js";
|
|
5
5
|
import { useTheme } from "../hooks/useTheme.js";
|
|
6
6
|
import * as ToggleGroupPrimitive from "@rn-primitives/toggle-group";
|
|
@@ -175,12 +175,13 @@ function ToggleGroupItem({ isFirst = false, isLast = false, children, ...props }
|
|
|
175
175
|
...(Platform.OS === "web" && {
|
|
176
176
|
cursor: props.disabled ? "not-allowed" : "pointer",
|
|
177
177
|
transition: "all 150ms",
|
|
178
|
+
userSelect: "none",
|
|
178
179
|
// Ensure proper z-index on focus
|
|
179
180
|
...(isSelected && {
|
|
180
181
|
zIndex: 10,
|
|
181
182
|
}),
|
|
182
183
|
}),
|
|
183
|
-
}, hitSlop: DEFAULT_HIT_SLOP, children: children }) }) }));
|
|
184
|
+
}, hitSlop: DEFAULT_HIT_SLOP, children: typeof children === "function" ? ((state) => (_jsx(TextSelectabilityContext.Provider, { value: false, children: children(state) }))) : (_jsx(TextSelectabilityContext.Provider, { value: false, children: children })) }) }) }));
|
|
184
185
|
}
|
|
185
186
|
function ToggleGroupIcon({ name, size, color }) {
|
|
186
187
|
const contextColor = React.useContext(TextColorContext);
|
|
@@ -2,7 +2,7 @@ import { jsx as _jsx } from "react/jsx-runtime";
|
|
|
2
2
|
import * as React from "react";
|
|
3
3
|
import { Platform, StyleSheet, View } from "react-native";
|
|
4
4
|
import { AnimatedView } from "./AnimatedView.js";
|
|
5
|
-
import { TextClassContext, TextColorContext } from "./StyledText.js";
|
|
5
|
+
import { TextClassContext, TextColorContext, TextSelectabilityContext } from "./StyledText.js";
|
|
6
6
|
import { useTheme } from "../hooks/useTheme.js";
|
|
7
7
|
import { spacing } from "../constants/spacing.js";
|
|
8
8
|
import * as TooltipPrimitive from "@rn-primitives/tooltip";
|
|
@@ -50,9 +50,9 @@ function TooltipContent({ side = "top", align = "center", sideOffset = 4, portal
|
|
|
50
50
|
case "default":
|
|
51
51
|
default:
|
|
52
52
|
return {
|
|
53
|
-
background: theme.colors.
|
|
53
|
+
background: theme.colors.popover,
|
|
54
54
|
border: theme.colors.border,
|
|
55
|
-
text: getContrastingColor(theme.colors.
|
|
55
|
+
text: getContrastingColor(theme.colors.popover, palette.white, palette.black),
|
|
56
56
|
};
|
|
57
57
|
}
|
|
58
58
|
};
|
|
@@ -69,7 +69,7 @@ function TooltipContent({ side = "top", align = "center", sideOffset = 4, portal
|
|
|
69
69
|
...getShadowStyle("soft"),
|
|
70
70
|
},
|
|
71
71
|
]);
|
|
72
|
-
return (_jsx(TooltipPrimitive.Portal, { hostName: portalHost, children: _jsx(FullWindowOverlay, { children: _jsx(TooltipPrimitive.Overlay, { style: Platform.select({ native: StyleSheet.absoluteFill }), children: _jsx(AnimatedView, { type: "fade", enterDuration: 150, children: _jsx(TextColorContext.Provider, { value: colors.text, children: _jsx(TextClassContext.Provider, { value: "", children: _jsx(TooltipPrimitive.Content, { side: side, align: align, sideOffset: sideOffset, style: contentStyle, ...props }) }) }) }) }) }) }));
|
|
72
|
+
return (_jsx(TooltipPrimitive.Portal, { hostName: portalHost, children: _jsx(FullWindowOverlay, { children: _jsx(TooltipPrimitive.Overlay, { style: Platform.select({ native: StyleSheet.absoluteFill }), children: _jsx(AnimatedView, { type: "fade", enterDuration: 150, children: _jsx(TextColorContext.Provider, { value: colors.text, children: _jsx(TextClassContext.Provider, { value: "", children: _jsx(TextSelectabilityContext.Provider, { value: false, children: _jsx(TooltipPrimitive.Content, { side: side, align: align, sideOffset: sideOffset, style: contentStyle, ...props }) }) }) }) }) }) }) }));
|
|
73
73
|
}
|
|
74
74
|
function TooltipBody({ children, style, ...props }) {
|
|
75
75
|
return (_jsx(View, { style: [
|
|
@@ -44,6 +44,8 @@ export interface ThemeColors {
|
|
|
44
44
|
foreground: string;
|
|
45
45
|
card: string;
|
|
46
46
|
cardForeground: string;
|
|
47
|
+
popover: string;
|
|
48
|
+
popoverForeground: string;
|
|
47
49
|
text: string;
|
|
48
50
|
textDim: string;
|
|
49
51
|
primary: string;
|
|
@@ -59,6 +61,8 @@ export interface ThemeColors {
|
|
|
59
61
|
success: string;
|
|
60
62
|
warning: string;
|
|
61
63
|
border: string;
|
|
64
|
+
input: string;
|
|
65
|
+
ring: string;
|
|
62
66
|
overlay: string;
|
|
63
67
|
}
|
|
64
68
|
export interface Theme {
|
package/dist/constants/colors.js
CHANGED
|
@@ -46,8 +46,10 @@ const lightTheme = {
|
|
|
46
46
|
colors: {
|
|
47
47
|
background: palette.white,
|
|
48
48
|
foreground: palette.gray950,
|
|
49
|
-
card: palette.
|
|
49
|
+
card: palette.white,
|
|
50
50
|
cardForeground: palette.gray950,
|
|
51
|
+
popover: palette.white,
|
|
52
|
+
popoverForeground: palette.gray950,
|
|
51
53
|
text: palette.gray950,
|
|
52
54
|
textDim: palette.gray500,
|
|
53
55
|
primary: palette.gray900,
|
|
@@ -63,6 +65,8 @@ const lightTheme = {
|
|
|
63
65
|
success: palette.green500,
|
|
64
66
|
warning: palette.amber500,
|
|
65
67
|
border: palette.gray200,
|
|
68
|
+
input: palette.gray200,
|
|
69
|
+
ring: palette.gray400,
|
|
66
70
|
overlay: "rgba(0, 0, 0, 0.5)",
|
|
67
71
|
},
|
|
68
72
|
navigation: {
|
|
@@ -82,6 +86,8 @@ const darkTheme = {
|
|
|
82
86
|
foreground: palette.dark100,
|
|
83
87
|
card: palette.dark800,
|
|
84
88
|
cardForeground: palette.dark100,
|
|
89
|
+
popover: palette.dark800,
|
|
90
|
+
popoverForeground: palette.dark100,
|
|
85
91
|
text: palette.dark100,
|
|
86
92
|
textDim: palette.dark400,
|
|
87
93
|
primary: palette.gray50,
|
|
@@ -97,6 +103,8 @@ const darkTheme = {
|
|
|
97
103
|
success: palette.green400,
|
|
98
104
|
warning: palette.amber400,
|
|
99
105
|
border: palette.dark700,
|
|
106
|
+
input: palette.dark700,
|
|
107
|
+
ring: palette.dark400,
|
|
100
108
|
overlay: "rgba(0, 0, 0, 0.7)",
|
|
101
109
|
},
|
|
102
110
|
navigation: {
|
|
@@ -22,6 +22,7 @@ export declare const spacing: {
|
|
|
22
22
|
readonly cardPadding: 16;
|
|
23
23
|
readonly sectionSpacing: 32;
|
|
24
24
|
readonly listItemSpacing: 8;
|
|
25
|
+
readonly touchTarget: 44;
|
|
25
26
|
readonly radiusNone: 0;
|
|
26
27
|
readonly radiusXs: 2;
|
|
27
28
|
readonly radiusSm: 4;
|
|
@@ -36,5 +37,5 @@ export declare const spacing: {
|
|
|
36
37
|
readonly iconLg: 32;
|
|
37
38
|
readonly iconXl: 48;
|
|
38
39
|
};
|
|
39
|
-
export declare const base: 8, xxs: 2, xs: 4, sm: 8, md: 16, lg: 24, xl: 32, xxl: 48, xxxl: 64, gutter: 16, gutterVertical: 24, screenPadding: 16, buttonPadding: 10, inputPadding: 10, cardPadding: 16, sectionSpacing: 32, listItemSpacing: 8, radiusNone: 0, radiusXs: 2, radiusSm: 4, radiusMd: 6, radiusLg: 8, radiusXl: 12, radius2xl: 16, radiusFull: 9999, iconXs: 12, iconSm: 16, iconMd: 24, iconLg: 32, iconXl: 48;
|
|
40
|
+
export declare const base: 8, xxs: 2, xs: 4, sm: 8, md: 16, lg: 24, xl: 32, xxl: 48, xxxl: 64, gutter: 16, gutterVertical: 24, screenPadding: 16, buttonPadding: 10, inputPadding: 10, cardPadding: 16, sectionSpacing: 32, listItemSpacing: 8, touchTarget: 44, radiusNone: 0, radiusXs: 2, radiusSm: 4, radiusMd: 6, radiusLg: 8, radiusXl: 12, radius2xl: 16, radiusFull: 9999, iconXs: 12, iconSm: 16, iconMd: 24, iconLg: 32, iconXl: 48;
|
|
40
41
|
export declare const space: (multiplier: number) => number;
|
|
@@ -26,6 +26,7 @@ export const spacing = {
|
|
|
26
26
|
cardPadding: 16, // Default card padding
|
|
27
27
|
sectionSpacing: 32, // Space between major sections
|
|
28
28
|
listItemSpacing: 8, // Space between list items
|
|
29
|
+
touchTarget: 44, // Minimum comfortable native touch target
|
|
29
30
|
// Border radius — shadcn-inspired scale (radiusMd = 6px default)
|
|
30
31
|
radiusNone: 0,
|
|
31
32
|
radiusXs: 2,
|
|
@@ -43,6 +44,6 @@ export const spacing = {
|
|
|
43
44
|
iconXl: 48,
|
|
44
45
|
};
|
|
45
46
|
// Export individual constants for convenience
|
|
46
|
-
export const { base, xxs, xs, sm, md, lg, xl, xxl, xxxl, gutter, gutterVertical, screenPadding, buttonPadding, inputPadding, cardPadding, sectionSpacing, listItemSpacing, radiusNone, radiusXs, radiusSm, radiusMd, radiusLg, radiusXl, radius2xl, radiusFull, iconXs, iconSm, iconMd, iconLg, iconXl, } = spacing;
|
|
47
|
+
export const { base, xxs, xs, sm, md, lg, xl, xxl, xxxl, gutter, gutterVertical, screenPadding, buttonPadding, inputPadding, cardPadding, sectionSpacing, listItemSpacing, touchTarget, radiusNone, radiusXs, radiusSm, radiusMd, radiusLg, radiusXl, radius2xl, radiusFull, iconXs, iconSm, iconMd, iconLg, iconXl, } = spacing;
|
|
47
48
|
// Helper function to multiply base spacing
|
|
48
49
|
export const space = (multiplier) => spacing.base * multiplier;
|
package/dist/hooks/useTheme.d.ts
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { Colors } from "../constants/colors";
|
|
2
2
|
import { ViewStyle, StyleSheet } from "react-native";
|
|
3
3
|
import { spacing as spacingConstants } from "../constants/spacing";
|
|
4
|
-
type ShadowType = "base" | "soft" | "sharp" | "subtle";
|
|
4
|
+
type ShadowType = "base" | "soft" | "sharp" | "subtle" | "elevated" | "glow" | "glass" | "card" | "cardHover" | "cardSubtle";
|
|
5
5
|
interface ExtendedColorScheme {
|
|
6
6
|
theme: Colors["light" | "dark"];
|
|
7
7
|
scheme: "light" | "dark";
|
|
8
8
|
getShadowStyle: (type: ShadowType) => ViewStyle;
|
|
9
|
+
getFocusRingStyle: (offset?: number) => ViewStyle;
|
|
9
10
|
getContrastingColor: (backgroundColor: string, color1?: string, color2?: string) => string;
|
|
10
11
|
getTextColorForBackground: (backgroundColor: string) => "light" | "dark";
|
|
11
12
|
withAlpha: (color: string, alpha: number) => string;
|
|
@@ -21,6 +22,7 @@ interface ExtendedColorScheme {
|
|
|
21
22
|
* - theme: active theme colors (light or dark)
|
|
22
23
|
* - scheme: "light" | "dark"
|
|
23
24
|
* - getShadowStyle(type): returns cross-platform shadow style object
|
|
25
|
+
* - getFocusRingStyle(offset): returns web focus ring style object
|
|
24
26
|
* - getContrastingColor(bg, color1?, color2?): pick best contrast of two options
|
|
25
27
|
* - getTextColorForBackground(bg): returns "light" or "dark"
|
|
26
28
|
* - withAlpha(color, alpha): adds transparency
|
|
@@ -43,6 +45,7 @@ export declare function useTheme(): ExtendedColorScheme & {
|
|
|
43
45
|
interface StyleContext {
|
|
44
46
|
theme: Colors["light" | "dark"];
|
|
45
47
|
spacing: typeof spacingConstants;
|
|
48
|
+
withAlpha: (color: string, alpha: number) => string;
|
|
46
49
|
}
|
|
47
50
|
/**
|
|
48
51
|
* Return type for useStyles hook
|
|
@@ -56,22 +59,22 @@ type UseStylesReturn<T extends StyleSheet.NamedStyles<T>> = {
|
|
|
56
59
|
* useStyles
|
|
57
60
|
*
|
|
58
61
|
* A hook that combines useTheme with StyleSheet.create for theme-aware styling.
|
|
59
|
-
* Provides access to theme colors
|
|
62
|
+
* Provides access to theme colors, spacing constants, and color helpers within the style factory.
|
|
60
63
|
*
|
|
61
|
-
* @param factory - A function that receives { theme, spacing } and returns style definitions
|
|
64
|
+
* @param factory - A function that receives { theme, spacing, withAlpha } and returns style definitions
|
|
62
65
|
* @returns { styles, theme, spacing, ...themeUtilities }
|
|
63
66
|
*
|
|
64
67
|
* @example
|
|
65
68
|
* ```tsx
|
|
66
69
|
* function MyComponent() {
|
|
67
|
-
* const { styles, theme } = useStyles(({ theme, spacing }) => ({
|
|
70
|
+
* const { styles, theme } = useStyles(({ theme, spacing, withAlpha }) => ({
|
|
68
71
|
* container: {
|
|
69
|
-
* backgroundColor: theme.colors.
|
|
72
|
+
* backgroundColor: withAlpha(theme.colors.primary, 0.08),
|
|
70
73
|
* padding: spacing.md,
|
|
71
74
|
* borderRadius: spacing.radiusMd,
|
|
72
75
|
* },
|
|
73
76
|
* text: {
|
|
74
|
-
* color: theme.colors.
|
|
77
|
+
* color: theme.colors.text,
|
|
75
78
|
* fontSize: 16,
|
|
76
79
|
* },
|
|
77
80
|
* }));
|
package/dist/hooks/useTheme.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { useEffect } from "react";
|
|
1
|
+
import { useCallback, useEffect, useMemo } from "react";
|
|
2
2
|
import { colors } from "../constants/colors.js";
|
|
3
3
|
import { useColorScheme as useColorSchemeDefault, Platform, StyleSheet } from "react-native";
|
|
4
4
|
import { useThemeStore } from "../state/themeStore.js";
|
|
@@ -31,6 +31,7 @@ function getCachedOrCompute(key, compute) {
|
|
|
31
31
|
* - theme: active theme colors (light or dark)
|
|
32
32
|
* - scheme: "light" | "dark"
|
|
33
33
|
* - getShadowStyle(type): returns cross-platform shadow style object
|
|
34
|
+
* - getFocusRingStyle(offset): returns web focus ring style object
|
|
34
35
|
* - getContrastingColor(bg, color1?, color2?): pick best contrast of two options
|
|
35
36
|
* - getTextColorForBackground(bg): returns "light" or "dark"
|
|
36
37
|
* - withAlpha(color, alpha): adds transparency
|
|
@@ -61,7 +62,7 @@ export function useTheme() {
|
|
|
61
62
|
}
|
|
62
63
|
}, [effectiveScheme]);
|
|
63
64
|
// Toggle between light, dark, and system themes
|
|
64
|
-
const toggleTheme = () => {
|
|
65
|
+
const toggleTheme = useCallback(() => {
|
|
65
66
|
if (userTheme === "light") {
|
|
66
67
|
setTheme("dark");
|
|
67
68
|
}
|
|
@@ -71,14 +72,14 @@ export function useTheme() {
|
|
|
71
72
|
else {
|
|
72
73
|
setTheme("light");
|
|
73
74
|
}
|
|
74
|
-
};
|
|
75
|
+
}, [setTheme, userTheme]);
|
|
75
76
|
/**
|
|
76
77
|
* getShadowStyle
|
|
77
78
|
* Returns platform-appropriate shadow styles
|
|
78
|
-
* - Web:
|
|
79
|
+
* - Web: uses CSS boxShadow through React Native Web
|
|
79
80
|
* - Native: uses shadowColor, shadowOffset, shadowOpacity, shadowRadius, elevation
|
|
80
81
|
*/
|
|
81
|
-
const getShadowStyle = (type) => {
|
|
82
|
+
const getShadowStyle = useCallback((type) => {
|
|
82
83
|
const shadowConfigs = {
|
|
83
84
|
base: {
|
|
84
85
|
shadowColor: theme.colors.overlay,
|
|
@@ -108,15 +109,79 @@ export function useTheme() {
|
|
|
108
109
|
shadowRadius: 2,
|
|
109
110
|
elevation: 1,
|
|
110
111
|
},
|
|
112
|
+
elevated: {
|
|
113
|
+
shadowColor: theme.colors.overlay,
|
|
114
|
+
shadowOffset: { width: 0, height: 20 },
|
|
115
|
+
shadowOpacity: 0.15,
|
|
116
|
+
shadowRadius: 40,
|
|
117
|
+
elevation: 16,
|
|
118
|
+
},
|
|
119
|
+
glow: {
|
|
120
|
+
shadowColor: theme.colors.primary,
|
|
121
|
+
shadowOffset: { width: 0, height: 4 },
|
|
122
|
+
shadowOpacity: 0.4,
|
|
123
|
+
shadowRadius: 20,
|
|
124
|
+
elevation: 10,
|
|
125
|
+
},
|
|
126
|
+
glass: {
|
|
127
|
+
shadowColor: theme.colors.overlay,
|
|
128
|
+
shadowOffset: { width: 0, height: 4 },
|
|
129
|
+
shadowOpacity: 0.05,
|
|
130
|
+
shadowRadius: 30,
|
|
131
|
+
elevation: 4,
|
|
132
|
+
},
|
|
133
|
+
card: {
|
|
134
|
+
shadowColor: theme.colors.overlay,
|
|
135
|
+
shadowOffset: { width: 0, height: 2 },
|
|
136
|
+
shadowOpacity: 0.08,
|
|
137
|
+
shadowRadius: 8,
|
|
138
|
+
elevation: 4,
|
|
139
|
+
},
|
|
140
|
+
cardHover: {
|
|
141
|
+
shadowColor: theme.colors.overlay,
|
|
142
|
+
shadowOffset: { width: 0, height: 8 },
|
|
143
|
+
shadowOpacity: 0.12,
|
|
144
|
+
shadowRadius: 24,
|
|
145
|
+
elevation: 8,
|
|
146
|
+
},
|
|
147
|
+
cardSubtle: {
|
|
148
|
+
shadowColor: theme.colors.overlay,
|
|
149
|
+
shadowOffset: { width: 0, height: 1 },
|
|
150
|
+
shadowOpacity: 0.08,
|
|
151
|
+
shadowRadius: 3,
|
|
152
|
+
elevation: 2,
|
|
153
|
+
},
|
|
111
154
|
};
|
|
112
155
|
const config = shadowConfigs[type];
|
|
156
|
+
if (Platform.OS === "web") {
|
|
157
|
+
const webShadows = {
|
|
158
|
+
base: { boxShadow: theme.dark ? "0 1px 2px rgba(0, 0, 0, 0.45)" : "0 1px 2px rgba(0, 0, 0, 0.08)" },
|
|
159
|
+
soft: { boxShadow: theme.dark ? "0 8px 24px rgba(0, 0, 0, 0.36)" : "0 8px 24px rgba(0, 0, 0, 0.10)" },
|
|
160
|
+
sharp: { boxShadow: theme.dark ? "0 1px 1px rgba(0, 0, 0, 0.55)" : "0 1px 1px rgba(0, 0, 0, 0.12)" },
|
|
161
|
+
subtle: { boxShadow: theme.dark ? "0 1px 2px rgba(0, 0, 0, 0.32)" : "0 1px 2px rgba(0, 0, 0, 0.05)" },
|
|
162
|
+
elevated: { boxShadow: theme.dark ? "0 20px 40px rgba(0, 0, 0, 0.38)" : "0 20px 40px rgba(0, 0, 0, 0.15)" },
|
|
163
|
+
glow: { boxShadow: `0 0 20px ${theme.colors.primary}` },
|
|
164
|
+
glass: { boxShadow: theme.dark ? "0 4px 30px rgba(0, 0, 0, 0.32)" : "0 4px 30px rgba(0, 0, 0, 0.05)" },
|
|
165
|
+
card: { boxShadow: theme.dark ? "0 1px 2px rgba(0, 0, 0, 0.32)" : "0 1px 3px rgba(0, 0, 0, 0.08)" },
|
|
166
|
+
cardHover: { boxShadow: theme.dark ? "0 8px 24px rgba(0, 0, 0, 0.36)" : "0 8px 24px rgba(0, 0, 0, 0.12)" },
|
|
167
|
+
cardSubtle: { boxShadow: theme.dark ? "0 1px 2px rgba(0, 0, 0, 0.32)" : "0 1px 3px rgba(0, 0, 0, 0.05)" },
|
|
168
|
+
};
|
|
169
|
+
return webShadows[type];
|
|
170
|
+
}
|
|
113
171
|
return Platform.select({
|
|
114
|
-
web: {}, // Empty on web - boxShadow causes crashes
|
|
115
172
|
default: config,
|
|
116
173
|
});
|
|
117
|
-
};
|
|
174
|
+
}, [theme]);
|
|
175
|
+
const getFocusRingStyle = useCallback((offset = 2) => {
|
|
176
|
+
if (Platform.OS !== "web") {
|
|
177
|
+
return {};
|
|
178
|
+
}
|
|
179
|
+
return {
|
|
180
|
+
boxShadow: `0 0 0 ${offset}px ${theme.colors.background}, 0 0 0 ${offset + 2}px ${theme.colors.ring}`,
|
|
181
|
+
};
|
|
182
|
+
}, [theme.colors.background, theme.colors.ring]);
|
|
118
183
|
// Helper to calculate contrast ratio between two colors
|
|
119
|
-
const getContrastRatio = (color1, color2) => {
|
|
184
|
+
const getContrastRatio = useCallback((color1, color2) => {
|
|
120
185
|
const rgb1 = parseColor(color1);
|
|
121
186
|
const rgb2 = parseColor(color2);
|
|
122
187
|
if (!rgb1 || !rgb2) {
|
|
@@ -126,17 +191,18 @@ export function useTheme() {
|
|
|
126
191
|
const lum1 = calculateLuminance(rgb1[0], rgb1[1], rgb1[2]);
|
|
127
192
|
const lum2 = calculateLuminance(rgb2[0], rgb2[1], rgb2[2]);
|
|
128
193
|
return calculateContrastRatio(lum1, lum2);
|
|
129
|
-
};
|
|
194
|
+
}, []);
|
|
130
195
|
// Cached version of getContrastingColor for performance
|
|
131
196
|
// Uses module-level cache to prevent memory leak
|
|
132
|
-
const getCachedContrastingColor = (backgroundColor, color1, color2) => {
|
|
197
|
+
const getCachedContrastingColor = useCallback((backgroundColor, color1, color2) => {
|
|
133
198
|
const cacheKey = `${backgroundColor}-${color1}-${color2}`;
|
|
134
199
|
return getCachedOrCompute(cacheKey, () => getBetterContrast(backgroundColor, color1, color2));
|
|
135
|
-
};
|
|
136
|
-
return {
|
|
200
|
+
}, []);
|
|
201
|
+
return useMemo(() => ({
|
|
137
202
|
theme,
|
|
138
203
|
scheme: theme.dark ? "dark" : "light",
|
|
139
204
|
getShadowStyle,
|
|
205
|
+
getFocusRingStyle,
|
|
140
206
|
toggleTheme,
|
|
141
207
|
setTheme,
|
|
142
208
|
currentTheme: userTheme,
|
|
@@ -144,7 +210,16 @@ export function useTheme() {
|
|
|
144
210
|
getTextColorForBackground,
|
|
145
211
|
withAlpha,
|
|
146
212
|
getContrastRatio,
|
|
147
|
-
}
|
|
213
|
+
}), [
|
|
214
|
+
getCachedContrastingColor,
|
|
215
|
+
getContrastRatio,
|
|
216
|
+
getFocusRingStyle,
|
|
217
|
+
getShadowStyle,
|
|
218
|
+
setTheme,
|
|
219
|
+
theme,
|
|
220
|
+
toggleTheme,
|
|
221
|
+
userTheme,
|
|
222
|
+
]);
|
|
148
223
|
}
|
|
149
224
|
/**
|
|
150
225
|
* Parses a color string (hex or rgba) and returns RGB values.
|
|
@@ -277,22 +352,22 @@ function withAlpha(color, alpha) {
|
|
|
277
352
|
* useStyles
|
|
278
353
|
*
|
|
279
354
|
* A hook that combines useTheme with StyleSheet.create for theme-aware styling.
|
|
280
|
-
* Provides access to theme colors
|
|
355
|
+
* Provides access to theme colors, spacing constants, and color helpers within the style factory.
|
|
281
356
|
*
|
|
282
|
-
* @param factory - A function that receives { theme, spacing } and returns style definitions
|
|
357
|
+
* @param factory - A function that receives { theme, spacing, withAlpha } and returns style definitions
|
|
283
358
|
* @returns { styles, theme, spacing, ...themeUtilities }
|
|
284
359
|
*
|
|
285
360
|
* @example
|
|
286
361
|
* ```tsx
|
|
287
362
|
* function MyComponent() {
|
|
288
|
-
* const { styles, theme } = useStyles(({ theme, spacing }) => ({
|
|
363
|
+
* const { styles, theme } = useStyles(({ theme, spacing, withAlpha }) => ({
|
|
289
364
|
* container: {
|
|
290
|
-
* backgroundColor: theme.colors.
|
|
365
|
+
* backgroundColor: withAlpha(theme.colors.primary, 0.08),
|
|
291
366
|
* padding: spacing.md,
|
|
292
367
|
* borderRadius: spacing.radiusMd,
|
|
293
368
|
* },
|
|
294
369
|
* text: {
|
|
295
|
-
* color: theme.colors.
|
|
370
|
+
* color: theme.colors.text,
|
|
296
371
|
* fontSize: 16,
|
|
297
372
|
* },
|
|
298
373
|
* }));
|
|
@@ -307,16 +382,18 @@ function withAlpha(color, alpha) {
|
|
|
307
382
|
*/
|
|
308
383
|
export function useStyles(factory) {
|
|
309
384
|
const themeContext = useTheme();
|
|
310
|
-
const styles = StyleSheet.create(factory({
|
|
385
|
+
const styles = useMemo(() => StyleSheet.create(factory({
|
|
311
386
|
theme: themeContext.theme,
|
|
312
387
|
spacing: spacingConstants,
|
|
313
|
-
|
|
314
|
-
|
|
388
|
+
withAlpha: themeContext.withAlpha,
|
|
389
|
+
})), [factory, themeContext.theme, themeContext.withAlpha]);
|
|
390
|
+
return useMemo(() => ({
|
|
315
391
|
styles,
|
|
316
392
|
theme: themeContext.theme,
|
|
317
393
|
spacing: spacingConstants,
|
|
318
394
|
scheme: themeContext.scheme,
|
|
319
395
|
getShadowStyle: themeContext.getShadowStyle,
|
|
396
|
+
getFocusRingStyle: themeContext.getFocusRingStyle,
|
|
320
397
|
getContrastingColor: themeContext.getContrastingColor,
|
|
321
398
|
getTextColorForBackground: themeContext.getTextColorForBackground,
|
|
322
399
|
withAlpha: themeContext.withAlpha,
|
|
@@ -324,5 +401,5 @@ export function useStyles(factory) {
|
|
|
324
401
|
toggleTheme: themeContext.toggleTheme,
|
|
325
402
|
setTheme: themeContext.setTheme,
|
|
326
403
|
currentTheme: themeContext.currentTheme,
|
|
327
|
-
};
|
|
404
|
+
}), [styles, themeContext]);
|
|
328
405
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mrmeg/expo-ui",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.7",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Reusable Expo and React Native UI primitives for MrMeg projects.",
|
|
6
6
|
"keywords": [
|
|
@@ -79,11 +79,7 @@
|
|
|
79
79
|
"publish:dry-run": "bun pm pack --dry-run"
|
|
80
80
|
},
|
|
81
81
|
"dependencies": {
|
|
82
|
-
"@rn-primitives/portal": "~1.4.0"
|
|
83
|
-
},
|
|
84
|
-
"peerDependencies": {
|
|
85
82
|
"@expo/vector-icons": ">=15.0.0 <16.0.0",
|
|
86
|
-
"@react-native-async-storage/async-storage": ">=2.2.0 <2.3.0",
|
|
87
83
|
"@rn-primitives/accordion": "~1.4.0",
|
|
88
84
|
"@rn-primitives/alert-dialog": "~1.4.0",
|
|
89
85
|
"@rn-primitives/checkbox": "~1.4.0",
|
|
@@ -92,6 +88,7 @@
|
|
|
92
88
|
"@rn-primitives/dropdown-menu": "~1.4.0",
|
|
93
89
|
"@rn-primitives/label": "~1.4.0",
|
|
94
90
|
"@rn-primitives/popover": "~1.4.0",
|
|
91
|
+
"@rn-primitives/portal": "~1.4.0",
|
|
95
92
|
"@rn-primitives/radio-group": "~1.4.0",
|
|
96
93
|
"@rn-primitives/select": "~1.4.0",
|
|
97
94
|
"@rn-primitives/separator": "~1.4.0",
|
|
@@ -100,8 +97,10 @@
|
|
|
100
97
|
"@rn-primitives/tabs": "~1.4.0",
|
|
101
98
|
"@rn-primitives/toggle": "~1.4.0",
|
|
102
99
|
"@rn-primitives/toggle-group": "~1.4.0",
|
|
103
|
-
"@rn-primitives/tooltip": "~1.4.0"
|
|
104
|
-
|
|
100
|
+
"@rn-primitives/tooltip": "~1.4.0"
|
|
101
|
+
},
|
|
102
|
+
"peerDependencies": {
|
|
103
|
+
"@react-native-async-storage/async-storage": ">=2.2.0 <2.3.0",
|
|
105
104
|
"expo": "~55.0.0",
|
|
106
105
|
"expo-font": "~55.0.0",
|
|
107
106
|
"expo-haptics": "~55.0.0",
|