@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,8 +1,12 @@
|
|
|
1
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
1
|
+
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Icon } from "./Icon.js";
|
|
3
|
-
import { TextClassContext, TextColorContext, TextSelectabilityContext } from "./StyledText.context";
|
|
3
|
+
import { TextClassContext, TextColorContext, TextSelectabilityContext } from "./StyledText.context.js";
|
|
4
4
|
import { spacing } from "../constants/spacing.js";
|
|
5
|
+
import { getWebHitInsets, WebHitTarget } from "../lib/webHitTarget.js";
|
|
6
|
+
import { interaction } from "../constants/interaction.js";
|
|
7
|
+
import { hapticSelection } from "../lib/haptics.js";
|
|
5
8
|
import { useTheme } from "../hooks/useTheme.js";
|
|
9
|
+
import { useFocusVisible } from "../hooks/useFocusVisible.js";
|
|
6
10
|
import { useScalePress } from "../hooks/useScalePress.js";
|
|
7
11
|
import * as ToggleGroupPrimitive from "@rn-primitives/toggle-group";
|
|
8
12
|
import * as React from "react";
|
|
@@ -67,17 +71,26 @@ function useToggleGroupContext() {
|
|
|
67
71
|
function ToggleGroup({ variant = "default", size = "default", children, ...props }) {
|
|
68
72
|
const { getShadowStyle } = useTheme();
|
|
69
73
|
const contextValue = React.useMemo(() => ({ variant, size }), [variant, size]);
|
|
74
|
+
// The primitive reports a change once per user selection (single or
|
|
75
|
+
// multiple), which is the selection-haptic moment; forward it unchanged.
|
|
76
|
+
const { onValueChange } = props;
|
|
77
|
+
const handleValueChange = React.useCallback((value) => {
|
|
78
|
+
hapticSelection();
|
|
79
|
+
onValueChange?.(value);
|
|
80
|
+
}, [onValueChange]);
|
|
70
81
|
// Count valid children for first/last detection
|
|
71
82
|
const childrenArray = React.Children.toArray(children);
|
|
72
83
|
const validChildren = childrenArray.filter((child) => React.isValidElement(child) && child.type === ToggleGroupItem);
|
|
73
84
|
const childCount = validChildren.length;
|
|
74
|
-
// Clone children with position props
|
|
75
|
-
let
|
|
85
|
+
// Clone children with position props. The running item index lives on an
|
|
86
|
+
// object: the React Compiler can't compile a reassigned `let` captured by
|
|
87
|
+
// the callback, and skipped the component.
|
|
88
|
+
const position = { itemIndex: 0 };
|
|
76
89
|
const enhancedChildren = React.Children.map(children, (child) => {
|
|
77
90
|
if (React.isValidElement(child) && child.type === ToggleGroupItem) {
|
|
78
|
-
const isFirst = itemIndex === 0;
|
|
79
|
-
const isLast = itemIndex === childCount - 1;
|
|
80
|
-
itemIndex
|
|
91
|
+
const isFirst = position.itemIndex === 0;
|
|
92
|
+
const isLast = position.itemIndex === childCount - 1;
|
|
93
|
+
position.itemIndex += 1;
|
|
81
94
|
return React.cloneElement(child, {
|
|
82
95
|
isFirst,
|
|
83
96
|
isLast,
|
|
@@ -85,7 +98,12 @@ function ToggleGroup({ variant = "default", size = "default", children, ...props
|
|
|
85
98
|
}
|
|
86
99
|
return child;
|
|
87
100
|
});
|
|
88
|
-
|
|
101
|
+
// The web primitive's Root/Item render generic roles (Radix's `group`, then
|
|
102
|
+
// `role='button'` on every item) while Radix still emits `aria-checked` for a
|
|
103
|
+
// single-select item — invalid on a button. Name the roles the way native
|
|
104
|
+
// already does: a radiogroup of radios, or a group of (aria-pressed) buttons.
|
|
105
|
+
const webRoles = Platform.OS === "web" ? { role: props.type === "single" ? "radiogroup" : "group" } : undefined;
|
|
106
|
+
return (_jsx(ToggleGroupPrimitive.Root, { ...props, ...webRoles, onValueChange: handleValueChange, style: {
|
|
89
107
|
flexDirection: "row",
|
|
90
108
|
alignItems: "center",
|
|
91
109
|
borderRadius: spacing.radiusMd,
|
|
@@ -104,33 +122,17 @@ function ToggleGroup({ variant = "default", size = "default", children, ...props
|
|
|
104
122
|
function ToggleGroupItem({ isFirst = false, isLast = false, children, ...props }) {
|
|
105
123
|
const { theme, withAlpha, getContrastingColor, getFocusRingStyle } = useTheme();
|
|
106
124
|
const context = useToggleGroupContext();
|
|
107
|
-
const { value: groupValue } = ToggleGroupPrimitive.useRootContext();
|
|
125
|
+
const { value: groupValue, type: groupType } = ToggleGroupPrimitive.useRootContext();
|
|
126
|
+
// See ToggleGroup: give the web item the role its aria state is valid on.
|
|
127
|
+
const webRole = Platform.OS === "web" ? { role: groupType === "single" ? "radio" : "button" } : undefined;
|
|
108
128
|
const sizeConfig = TOGGLE_GROUP_SIZES[context.size];
|
|
109
129
|
const focusRingStyle = getFocusRingStyle();
|
|
110
|
-
const [focused, setFocused] = React.useState(false);
|
|
111
130
|
const { animatedStyle: scaleStyle, pressHandlers } = useScalePress({
|
|
112
131
|
disabled: !!props.disabled,
|
|
113
|
-
scaleTo:
|
|
132
|
+
scaleTo: interaction.pressedScale,
|
|
114
133
|
haptic: false,
|
|
115
134
|
});
|
|
116
|
-
const showFocusRing = (
|
|
117
|
-
let ringVisible = true;
|
|
118
|
-
if (Platform.OS === "web") {
|
|
119
|
-
const target = event?.nativeEvent?.target;
|
|
120
|
-
if (target && typeof target.matches === "function") {
|
|
121
|
-
try {
|
|
122
|
-
ringVisible = target.matches(":focus-visible");
|
|
123
|
-
}
|
|
124
|
-
catch {
|
|
125
|
-
ringVisible = true;
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
setFocused(ringVisible);
|
|
130
|
-
};
|
|
131
|
-
const hideFocusRing = () => {
|
|
132
|
-
setFocused(false);
|
|
133
|
-
};
|
|
135
|
+
const { focused, onFocus: showFocusRing, onBlur: hideFocusRing } = useFocusVisible();
|
|
134
136
|
// Check if this item is selected
|
|
135
137
|
const isSelected = ToggleGroupPrimitive.utils.getIsSelected(groupValue, props.value);
|
|
136
138
|
// Determine the actual background color for this item
|
|
@@ -145,7 +147,10 @@ function ToggleGroupItem({ isFirst = false, isLast = false, children, ...props }
|
|
|
145
147
|
const textColor = isSelected
|
|
146
148
|
? getContrastingColor(itemBgColor, theme.colors.foreground, theme.colors.background)
|
|
147
149
|
: theme.colors.foreground;
|
|
148
|
-
|
|
150
|
+
// Web pointer target, as in Toggle: 44pt tall through a transparent child
|
|
151
|
+
// inside the drawn box; native keeps DEFAULT_HIT_SLOP.
|
|
152
|
+
const hitTarget = (_jsx(WebHitTarget, { insets: Platform.OS === "web" ? getWebHitInsets(undefined, sizeConfig.height) : null, border: styles.item.borderWidth }));
|
|
153
|
+
return (_jsx(TextColorContext.Provider, { value: textColor, children: _jsx(TextClassContext.Provider, { value: "", children: _jsx(Animated.View, { style: scaleStyle, children: _jsx(ToggleGroupPrimitive.Item, { ...props, ...webRole, onPressIn: pressHandlers.onPressIn, onPressOut: pressHandlers.onPressOut, onFocus: showFocusRing, onBlur: hideFocusRing, style: {
|
|
149
154
|
...styles.item,
|
|
150
155
|
height: sizeConfig.height,
|
|
151
156
|
minWidth: sizeConfig.minWidth,
|
|
@@ -199,13 +204,13 @@ function ToggleGroupItem({ isFirst = false, isLast = false, children, ...props }
|
|
|
199
204
|
}),
|
|
200
205
|
}),
|
|
201
206
|
...(focused && !props.disabled ? focusRingStyle : null),
|
|
202
|
-
}, hitSlop: DEFAULT_HIT_SLOP, children: typeof children === "function" ? ((state) => (_jsx(TextSelectabilityContext.Provider, { value: false, children: children(state) }))) : (_jsx(TextSelectabilityContext.Provider, { value: false, children: children })) }) }) }) }));
|
|
207
|
+
}, hitSlop: DEFAULT_HIT_SLOP, children: typeof children === "function" ? ((state) => (_jsxs(_Fragment, { children: [_jsx(TextSelectabilityContext.Provider, { value: false, children: children(state) }), hitTarget] }))) : (_jsxs(_Fragment, { children: [_jsx(TextSelectabilityContext.Provider, { value: false, children: children }), hitTarget] })) }) }) }) }));
|
|
203
208
|
}
|
|
204
209
|
function ToggleGroupIcon({ name, size, color }) {
|
|
205
210
|
const contextColor = React.use(TextColorContext);
|
|
206
211
|
return _jsx(Icon, { name: name, size: size || spacing.iconMd, color: color || contextColor });
|
|
207
212
|
}
|
|
208
|
-
const styles = StyleSheet.create({
|
|
213
|
+
const styles = /*#__PURE__*/ StyleSheet.create({
|
|
209
214
|
item: {
|
|
210
215
|
flexDirection: "row",
|
|
211
216
|
alignItems: "center",
|
|
@@ -73,7 +73,7 @@ interface TooltipProps extends TooltipPrimitive.RootProps {
|
|
|
73
73
|
* // With positioning
|
|
74
74
|
* <Tooltip>
|
|
75
75
|
* <TooltipTrigger asChild>
|
|
76
|
-
* <Icon
|
|
76
|
+
* <Icon name="info" size={20} />
|
|
77
77
|
* </TooltipTrigger>
|
|
78
78
|
* <TooltipContent side="bottom" align="start">
|
|
79
79
|
* <StyledText>Information tooltip</StyledText>
|
|
@@ -2,12 +2,18 @@ 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, TextSelectabilityContext } from "./StyledText.context";
|
|
5
|
+
import { TextClassContext, TextColorContext, TextSelectabilityContext } from "./StyledText.context.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";
|
|
9
9
|
import { FullWindowOverlay as RNFullWindowOverlay } from "react-native-screens";
|
|
10
10
|
import { palette } from "../constants/colors.js";
|
|
11
|
+
// The overlay layer covers the screen but must not swallow touches around the
|
|
12
|
+
// content; a style, not the deprecated `pointerEvents` prop (web warns and
|
|
13
|
+
// lazily loads its warning module mid-render).
|
|
14
|
+
const styles = /*#__PURE__*/ StyleSheet.create({
|
|
15
|
+
passThrough: { pointerEvents: "box-none" },
|
|
16
|
+
});
|
|
11
17
|
/**
|
|
12
18
|
* Tooltip Trigger Component
|
|
13
19
|
* The element that triggers the tooltip to appear on hover (web) or press (native)
|
|
@@ -71,7 +77,7 @@ function TooltipContent({ side = "top", align = "center", sideOffset = 4, portal
|
|
|
71
77
|
...getShadowStyle("soft"),
|
|
72
78
|
},
|
|
73
79
|
]) ?? undefined;
|
|
74
|
-
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, style: StyleSheet.absoluteFill,
|
|
80
|
+
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, style: [StyleSheet.absoluteFill, styles.passThrough], 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 }) }) }) }) }) }) }) }));
|
|
75
81
|
}
|
|
76
82
|
function TooltipBody({ children, style, ...props }) {
|
|
77
83
|
return (_jsx(View, { style: [
|
|
@@ -101,7 +107,7 @@ function TooltipBody({ children, style, ...props }) {
|
|
|
101
107
|
* // With positioning
|
|
102
108
|
* <Tooltip>
|
|
103
109
|
* <TooltipTrigger asChild>
|
|
104
|
-
* <Icon
|
|
110
|
+
* <Icon name="info" size={20} />
|
|
105
111
|
* </TooltipTrigger>
|
|
106
112
|
* <TooltipContent side="bottom" align="start">
|
|
107
113
|
* <StyledText>Information tooltip</StyledText>
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
|
-
import { type
|
|
2
|
+
import { type HapticsSetting } from "../state/feedbackStore.js";
|
|
3
|
+
import type { ResolvedTheme } from "../state/themeStore.js";
|
|
4
|
+
import { type KeyboardAvoidingViewProps } from "./KeyboardAvoidingView.js";
|
|
3
5
|
export interface UIProviderProps {
|
|
4
6
|
children: React.ReactNode;
|
|
5
7
|
/**
|
|
@@ -30,5 +32,23 @@ export interface UIProviderProps {
|
|
|
30
32
|
* Props forwarded to the keyboard-avoiding root when enabled.
|
|
31
33
|
*/
|
|
32
34
|
keyboardAvoidingProps?: Omit<KeyboardAvoidingViewProps, "children">;
|
|
35
|
+
/**
|
|
36
|
+
* Which interactions may vibrate on native: `"off"`, `"selection"` (state
|
|
37
|
+
* changes on Switch, Checkbox, Toggle, ToggleGroup, SegmentedControl) or
|
|
38
|
+
* `"all"` (selection plus a light tap on press for Button, pressable Card
|
|
39
|
+
* and Item). Written to the feedback store on mount and whenever it changes;
|
|
40
|
+
* omit it to leave the store as it is. Web never vibrates.
|
|
41
|
+
*
|
|
42
|
+
* @default "selection"
|
|
43
|
+
*/
|
|
44
|
+
haptics?: HapticsSetting;
|
|
45
|
+
/**
|
|
46
|
+
* The scheme the first render should use before the persisted preference
|
|
47
|
+
* has been read (a web SSR hint — see `InitialSchemeProvider` in
|
|
48
|
+
* `@mrmeg/expo-ui/state`). Only reaches `useTheme()` calls BELOW this
|
|
49
|
+
* provider; a root layout that reads the theme above it should wrap itself
|
|
50
|
+
* in `InitialSchemeProvider` instead. Native ignores it.
|
|
51
|
+
*/
|
|
52
|
+
initialScheme?: ResolvedTheme;
|
|
33
53
|
}
|
|
34
|
-
export declare function UIProvider({ children, notification, portalHost, statusBar, keyboardAvoiding, keyboardAvoidingProps, }: UIProviderProps): React.JSX.Element;
|
|
54
|
+
export declare function UIProvider({ children, notification, portalHost, statusBar, keyboardAvoiding: keyboardAvoidingProp, keyboardAvoidingProps, haptics, initialScheme, }: UIProviderProps): React.JSX.Element;
|
|
@@ -1,11 +1,23 @@
|
|
|
1
|
-
import { jsx as _jsx,
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import * as React from "react";
|
|
2
3
|
import { Platform } from "react-native";
|
|
4
|
+
import { useFeedbackStore } from "../state/feedbackStore.js";
|
|
5
|
+
import { InitialSchemeProvider } from "../state/initialScheme.js";
|
|
3
6
|
import { PortalHost } from "@rn-primitives/portal";
|
|
4
7
|
import { Notification } from "./Notification.js";
|
|
5
8
|
import { StatusBar } from "./StatusBar.js";
|
|
6
9
|
import { KeyboardAvoidingView, } from "./KeyboardAvoidingView.js";
|
|
7
|
-
export function UIProvider({ children, notification = true, portalHost = true, statusBar = true, keyboardAvoiding
|
|
10
|
+
export function UIProvider({ children, notification = true, portalHost = true, statusBar = true, keyboardAvoiding: keyboardAvoidingProp, keyboardAvoidingProps, haptics, initialScheme, }) {
|
|
11
|
+
// Controls read the setting at event time (`hapticPress` / `hapticSelection`),
|
|
12
|
+
// so an effect is early enough; no child renders differently because of it.
|
|
13
|
+
React.useEffect(() => {
|
|
14
|
+
if (haptics !== undefined)
|
|
15
|
+
useFeedbackStore.getState().setHaptics(haptics);
|
|
16
|
+
}, [haptics]);
|
|
17
|
+
// Resolved in the body rather than as a default parameter: the React
|
|
18
|
+
// Compiler can't reorder a computed default, and skipped the component.
|
|
19
|
+
const keyboardAvoiding = keyboardAvoidingProp === undefined ? Platform.OS !== "web" : keyboardAvoidingProp;
|
|
8
20
|
const { style: keyboardAvoidingStyle, ...restKeyboardAvoidingProps } = keyboardAvoidingProps ?? {};
|
|
9
21
|
const content = keyboardAvoiding ? (_jsx(KeyboardAvoidingView, { style: [{ flex: 1 }, keyboardAvoidingStyle], ...restKeyboardAvoidingProps, children: children })) : (children);
|
|
10
|
-
return (_jsxs(
|
|
22
|
+
return (_jsxs(InitialSchemeProvider, { scheme: initialScheme, children: [content, notification ? _jsx(Notification, {}) : null, portalHost ? _jsx(PortalHost, {}) : null, statusBar ? _jsx(StatusBar, {}) : null] }));
|
|
11
23
|
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dismiss-complete detection for `BottomSheet.onDismissed` on web.
|
|
3
|
+
*
|
|
4
|
+
* `@expo/ui`'s web sheet is an HTML `<dialog>`. When the sheet's `index` goes
|
|
5
|
+
* to -1 it fires `onClose` synchronously, then plays its exit animation and
|
|
6
|
+
* calls `dialog.close()` on `animationend` (a 400 ms fallback timer covers a
|
|
7
|
+
* missed event; `prefers-reduced-motion` closes at once). The hosted RN column
|
|
8
|
+
* is a descendant of that element and stays mounted through the exit, so the
|
|
9
|
+
* element's standard `close` event is the moment the sheet is gone. iOS and
|
|
10
|
+
* Android need none of this: there `@expo/ui`'s close callback is already
|
|
11
|
+
* post-dismissal (see `BottomSheet.tsx`).
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* Call `callback` once the `<dialog>` enclosing `node` has closed. Fires on the
|
|
15
|
+
* same tick when there is no enclosing dialog (or no DOM node) or the dialog
|
|
16
|
+
* is already closed. Returns a cancel function that removes the pending
|
|
17
|
+
* listener without firing.
|
|
18
|
+
*/
|
|
19
|
+
export declare function awaitDialogClose(node: unknown, callback: () => void): () => void;
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dismiss-complete detection for `BottomSheet.onDismissed` on web.
|
|
3
|
+
*
|
|
4
|
+
* `@expo/ui`'s web sheet is an HTML `<dialog>`. When the sheet's `index` goes
|
|
5
|
+
* to -1 it fires `onClose` synchronously, then plays its exit animation and
|
|
6
|
+
* calls `dialog.close()` on `animationend` (a 400 ms fallback timer covers a
|
|
7
|
+
* missed event; `prefers-reduced-motion` closes at once). The hosted RN column
|
|
8
|
+
* is a descendant of that element and stays mounted through the exit, so the
|
|
9
|
+
* element's standard `close` event is the moment the sheet is gone. iOS and
|
|
10
|
+
* Android need none of this: there `@expo/ui`'s close callback is already
|
|
11
|
+
* post-dismissal (see `BottomSheet.tsx`).
|
|
12
|
+
*/
|
|
13
|
+
function findEnclosingDialog(node) {
|
|
14
|
+
const closest = node
|
|
15
|
+
?.closest;
|
|
16
|
+
if (typeof closest !== "function")
|
|
17
|
+
return null;
|
|
18
|
+
const dialog = closest.call(node, "dialog");
|
|
19
|
+
if (!dialog ||
|
|
20
|
+
typeof dialog.addEventListener !== "function" ||
|
|
21
|
+
typeof dialog.removeEventListener !== "function") {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
return dialog;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Call `callback` once the `<dialog>` enclosing `node` has closed. Fires on the
|
|
28
|
+
* same tick when there is no enclosing dialog (or no DOM node) or the dialog
|
|
29
|
+
* is already closed. Returns a cancel function that removes the pending
|
|
30
|
+
* listener without firing.
|
|
31
|
+
*/
|
|
32
|
+
export function awaitDialogClose(node, callback) {
|
|
33
|
+
const dialog = findEnclosingDialog(node);
|
|
34
|
+
if (!dialog || !dialog.open) {
|
|
35
|
+
callback();
|
|
36
|
+
return () => { };
|
|
37
|
+
}
|
|
38
|
+
const onClose = () => {
|
|
39
|
+
dialog.removeEventListener("close", onClose);
|
|
40
|
+
callback();
|
|
41
|
+
};
|
|
42
|
+
dialog.addEventListener("close", onClose);
|
|
43
|
+
return () => dialog.removeEventListener("close", onClose);
|
|
44
|
+
}
|
|
@@ -1,44 +1,46 @@
|
|
|
1
|
-
export * from "./Accordion";
|
|
2
|
-
export * from "./Alert";
|
|
3
|
-
export * from "./AnimatedView";
|
|
4
|
-
export * from "./Avatar";
|
|
5
|
-
export * from "./Badge";
|
|
6
|
-
export * from "./BottomSheet";
|
|
7
|
-
export * from "./Button";
|
|
8
|
-
export * from "./Card";
|
|
9
|
-
export * from "./Carousel";
|
|
10
|
-
export * from "./Checkbox";
|
|
11
|
-
export * from "./Collapsible";
|
|
12
|
-
export * from "./Dialog";
|
|
13
|
-
export * from "./DismissKeyboard";
|
|
14
|
-
export * from "./keyboardDismiss";
|
|
15
|
-
export * from "./Drawer";
|
|
16
|
-
export * from "./DropdownMenu";
|
|
17
|
-
export * from "./EmptyState";
|
|
18
|
-
export * from "./ErrorBoundary";
|
|
19
|
-
export * from "./
|
|
20
|
-
export * from "./
|
|
21
|
-
export * from "./
|
|
22
|
-
export * from "./
|
|
23
|
-
export * from "./
|
|
24
|
-
export * from "./
|
|
25
|
-
export * from "./
|
|
26
|
-
export * from "./
|
|
27
|
-
export * from "./
|
|
28
|
-
export * from "./
|
|
29
|
-
export * from "./
|
|
30
|
-
export * from "./
|
|
31
|
-
export * from "./
|
|
32
|
-
export * from "./
|
|
33
|
-
export * from "./
|
|
34
|
-
export * from "./
|
|
35
|
-
export * from "./
|
|
36
|
-
export * from "./
|
|
37
|
-
export * from "./
|
|
38
|
-
export * from "./
|
|
39
|
-
export * from "./
|
|
40
|
-
export * from "./
|
|
41
|
-
export * from "./
|
|
42
|
-
export * from "./
|
|
43
|
-
export * from "./
|
|
44
|
-
export * from "./
|
|
1
|
+
export * from "./Accordion.js";
|
|
2
|
+
export * from "./Alert.js";
|
|
3
|
+
export * from "./AnimatedView.js";
|
|
4
|
+
export * from "./Avatar.js";
|
|
5
|
+
export * from "./Badge.js";
|
|
6
|
+
export * from "./BottomSheet.js";
|
|
7
|
+
export * from "./Button.js";
|
|
8
|
+
export * from "./Card.js";
|
|
9
|
+
export * from "./Carousel.js";
|
|
10
|
+
export * from "./Checkbox.js";
|
|
11
|
+
export * from "./Collapsible.js";
|
|
12
|
+
export * from "./Dialog.js";
|
|
13
|
+
export * from "./DismissKeyboard.js";
|
|
14
|
+
export * from "./keyboardDismiss.js";
|
|
15
|
+
export * from "./Drawer.js";
|
|
16
|
+
export * from "./DropdownMenu.js";
|
|
17
|
+
export * from "./EmptyState.js";
|
|
18
|
+
export * from "./ErrorBoundary.js";
|
|
19
|
+
export * from "./Hydrated.js";
|
|
20
|
+
export * from "./Icon.js";
|
|
21
|
+
export * from "./InputOTP.js";
|
|
22
|
+
export * from "./Item.js";
|
|
23
|
+
export * from "./KeyboardAvoidingView.js";
|
|
24
|
+
export * from "./Label.js";
|
|
25
|
+
export * from "./MaxWidthContainer.js";
|
|
26
|
+
export * from "./Notification.js";
|
|
27
|
+
export * from "./Screen.js";
|
|
28
|
+
export * from "./Popover.js";
|
|
29
|
+
export * from "./Progress.js";
|
|
30
|
+
export * from "./RadioGroup.js";
|
|
31
|
+
export * from "./SectionHeader.js";
|
|
32
|
+
export * from "./SegmentedControl.js";
|
|
33
|
+
export * from "./Select.js";
|
|
34
|
+
export * from "./Separator.js";
|
|
35
|
+
export * from "./Skeleton.js";
|
|
36
|
+
export * from "./Slider.js";
|
|
37
|
+
export * from "./StatCard.js";
|
|
38
|
+
export * from "./StatusBar.js";
|
|
39
|
+
export * from "./StyledText.js";
|
|
40
|
+
export * from "./Switch.js";
|
|
41
|
+
export * from "./Tabs.js";
|
|
42
|
+
export * from "./TextInput.js";
|
|
43
|
+
export * from "./Toggle.js";
|
|
44
|
+
export * from "./ToggleGroup.js";
|
|
45
|
+
export * from "./Tooltip.js";
|
|
46
|
+
export * from "./UIProvider.js";
|
package/dist/components/index.js
CHANGED
|
@@ -16,6 +16,7 @@ export * from "./Drawer.js";
|
|
|
16
16
|
export * from "./DropdownMenu.js";
|
|
17
17
|
export * from "./EmptyState.js";
|
|
18
18
|
export * from "./ErrorBoundary.js";
|
|
19
|
+
export * from "./Hydrated.js";
|
|
19
20
|
export * from "./Icon.js";
|
|
20
21
|
export * from "./InputOTP.js";
|
|
21
22
|
export * from "./Item.js";
|
|
@@ -23,6 +24,7 @@ export * from "./KeyboardAvoidingView.js";
|
|
|
23
24
|
export * from "./Label.js";
|
|
24
25
|
export * from "./MaxWidthContainer.js";
|
|
25
26
|
export * from "./Notification.js";
|
|
27
|
+
export * from "./Screen.js";
|
|
26
28
|
export * from "./Popover.js";
|
|
27
29
|
export * from "./Progress.js";
|
|
28
30
|
export * from "./RadioGroup.js";
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
* slop; release coordinates also enforce it when move delivery was missed.
|
|
29
29
|
* Cancellation and multitouch drop the tap without rearming another finger.
|
|
30
30
|
*/
|
|
31
|
-
import { useMemo, useRef } from "react";
|
|
31
|
+
import { useLayoutEffect, useMemo, useRef } from "react";
|
|
32
32
|
import { Platform } from "react-native";
|
|
33
33
|
import { KeyboardController, useKeyboardState } from "./keyboardController";
|
|
34
34
|
import { dismissKeyboardFocusedInput, hasKeyboardFocusedInput } from "./keyboardFocusRegistry.js";
|
|
@@ -164,8 +164,12 @@ function createTapTracker() {
|
|
|
164
164
|
* for that.
|
|
165
165
|
*/
|
|
166
166
|
export function useTextInputSurfaceResponder(focus) {
|
|
167
|
+
// Latest `focus` for the memoized handlers, synced after each commit (before
|
|
168
|
+
// any touch can be dispatched) instead of written during render.
|
|
167
169
|
const focusRef = useRef(focus);
|
|
168
|
-
|
|
170
|
+
useLayoutEffect(() => {
|
|
171
|
+
focusRef.current = focus;
|
|
172
|
+
});
|
|
169
173
|
return useMemo(() => {
|
|
170
174
|
const tap = createTapTracker();
|
|
171
175
|
return {
|
|
@@ -219,7 +223,9 @@ export function resetAncestorClaimWarningForTests() {
|
|
|
219
223
|
*/
|
|
220
224
|
export function useAncestorClaimWarning(inner, message) {
|
|
221
225
|
const messageRef = useRef(message);
|
|
222
|
-
|
|
226
|
+
useLayoutEffect(() => {
|
|
227
|
+
messageRef.current = message;
|
|
228
|
+
});
|
|
223
229
|
return useMemo(() => {
|
|
224
230
|
if (!__DEV__ || Platform.OS !== "android")
|
|
225
231
|
return inner;
|
|
@@ -249,7 +255,9 @@ export function useAncestorClaimWarning(inner, message) {
|
|
|
249
255
|
export function useKeyboardDismissResponder() {
|
|
250
256
|
const isVisible = useKeyboardState((state) => state.isVisible);
|
|
251
257
|
const isVisibleRef = useRef(isVisible);
|
|
252
|
-
|
|
258
|
+
useLayoutEffect(() => {
|
|
259
|
+
isVisibleRef.current = isVisible;
|
|
260
|
+
});
|
|
253
261
|
return useMemo(() => {
|
|
254
262
|
if (Platform.OS === "web")
|
|
255
263
|
return {};
|
|
@@ -34,7 +34,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
34
34
|
* Only `@expo/ui/jetpack-compose` and `@expo/ui/jetpack-compose/modifiers` are
|
|
35
35
|
* imported, both in the peer's export map since SDK 56.
|
|
36
36
|
*/
|
|
37
|
-
import { useImperativeHandle, useRef } from "react";
|
|
37
|
+
import { useImperativeHandle, useRef, useState } from "react";
|
|
38
38
|
import { BasicTextField, Box, Text, useNativeState, } from "@expo/ui/jetpack-compose";
|
|
39
39
|
import { fillMaxWidth, onSizeChanged, padding, paddingAll, semantics, testID as testIDModifier, } from "@expo/ui/jetpack-compose/modifiers";
|
|
40
40
|
// region Prop mapping (ported verbatim from @expo/ui's TextInput/utils.ts)
|
|
@@ -145,8 +145,10 @@ export function NativeTextField({ ref, value, onChangeText, placeholder, autoFoc
|
|
|
145
145
|
const numberOfLines = numberOfLinesProp ?? rows;
|
|
146
146
|
const keyboardType = keyboardTypeProp ?? inputModeToKeyboardType(inputMode);
|
|
147
147
|
const returnKeyType = returnKeyTypeProp ?? enterKeyHintToReturnKeyType(enterKeyHint);
|
|
148
|
-
|
|
149
|
-
|
|
148
|
+
// Pinned to the first render's `defaultValue`, as an uncontrolled default
|
|
149
|
+
// is, whatever `useNativeState` does with a later argument.
|
|
150
|
+
const [initialFallback] = useState(defaultValue ?? "");
|
|
151
|
+
const fallback = useNativeState(initialFallback);
|
|
150
152
|
const state = (value ?? fallback);
|
|
151
153
|
const innerRef = useRef(null);
|
|
152
154
|
const isFocusedRef = useRef(false);
|
|
@@ -176,12 +178,14 @@ export function NativeTextField({ ref, value, onChangeText, placeholder, autoFoc
|
|
|
176
178
|
// Sticky for the life of the mounted field: revealing a password
|
|
177
179
|
// (`secureTextEntry` -> false on the same element) must keep the password
|
|
178
180
|
// keyboard options unchanged so the IME neither restarts nor starts
|
|
179
|
-
// suggesting/learning the now-visible text.
|
|
180
|
-
//
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
181
|
+
// suggesting/learning the now-visible text. The latch is state adjusted
|
|
182
|
+
// during render (React re-runs this render before committing it), and the
|
|
183
|
+
// `||` makes the very render that turns secure on already read as a password
|
|
184
|
+
// field; a fresh mount starts from the prop.
|
|
185
|
+
const [everSecure, setEverSecure] = useState(!!secureTextEntry);
|
|
186
|
+
if (secureTextEntry && !everSecure)
|
|
187
|
+
setEverSecure(true);
|
|
188
|
+
const passwordField = everSecure || !!secureTextEntry;
|
|
185
189
|
const mappedKeyboardType = keyboardType ? mapKeyboardType(keyboardType) : undefined;
|
|
186
190
|
const resolvedKeyboardType = passwordField
|
|
187
191
|
? passwordKeyboardType(mappedKeyboardType)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { navigationFonts } from "./fonts";
|
|
1
|
+
import { navigationFonts } from "./fonts.js";
|
|
2
2
|
/**
|
|
3
3
|
* Raw color palette - use semantic colors in components instead
|
|
4
4
|
*
|
|
@@ -38,10 +38,36 @@ declare const palette: {
|
|
|
38
38
|
readonly red500: "#EF4444";
|
|
39
39
|
readonly red400: "#F87171";
|
|
40
40
|
};
|
|
41
|
+
/**
|
|
42
|
+
* Extra color tokens an app declares for itself, so it stops keeping a side
|
|
43
|
+
* palette. Empty here; an app augments it once and the keys become part of
|
|
44
|
+
* `ThemeColors` everywhere — `setColors`, `ThemeColorScope`,
|
|
45
|
+
* `useTheme().theme.colors`, `getThemeCssVariables`:
|
|
46
|
+
*
|
|
47
|
+
* ```ts
|
|
48
|
+
* declare module "@mrmeg/expo-ui/constants" {
|
|
49
|
+
* interface ThemeColorExtensions {
|
|
50
|
+
* brandGold: string;
|
|
51
|
+
* }
|
|
52
|
+
* }
|
|
53
|
+
* useThemeStore.getState().setColors({
|
|
54
|
+
* light: { brandGold: "#c9a227" },
|
|
55
|
+
* dark: { brandGold: "#ffe066" },
|
|
56
|
+
* });
|
|
57
|
+
* ```
|
|
58
|
+
*
|
|
59
|
+
* The package ships no value for an extension key: provide it in both schemes
|
|
60
|
+
* or the token is `undefined` at runtime in the scheme that lacks it. On web
|
|
61
|
+
* the built-in tokens are `var(--c-*)` references and extension values are
|
|
62
|
+
* literals per scheme, so with any override `theme.colors` is a new object
|
|
63
|
+
* when the scheme changes (the identity caveat every override already has).
|
|
64
|
+
*/
|
|
65
|
+
export interface ThemeColorExtensions {
|
|
66
|
+
}
|
|
41
67
|
/**
|
|
42
68
|
* Semantic color interface - what colors mean in the UI
|
|
43
69
|
*/
|
|
44
|
-
export interface ThemeColors {
|
|
70
|
+
export interface ThemeColors extends ThemeColorExtensions {
|
|
45
71
|
surfaceSunken: string;
|
|
46
72
|
background: string;
|
|
47
73
|
foreground: string;
|
|
@@ -86,14 +112,21 @@ export interface Colors {
|
|
|
86
112
|
light: Theme;
|
|
87
113
|
dark: Theme;
|
|
88
114
|
}
|
|
115
|
+
/**
|
|
116
|
+
* The package's own tokens: `ThemeColors` minus whatever an app added through
|
|
117
|
+
* `ThemeColorExtensions`. The default palettes, the CSS-variable table and
|
|
118
|
+
* `resolveRawColor` are typed on this, because the package has no value for an
|
|
119
|
+
* extension key.
|
|
120
|
+
*/
|
|
121
|
+
export type ThemeColorTokens = Omit<ThemeColors, keyof ThemeColorExtensions>;
|
|
89
122
|
/**
|
|
90
123
|
* Raw hex/rgba theme colors, always literal strings on every platform. Use
|
|
91
124
|
* these for sinks that cannot take CSS `var()` — e.g. `<meta name="theme-color">`,
|
|
92
125
|
* color parsing/math — and for generating the CSS variable definitions below.
|
|
93
126
|
*/
|
|
94
127
|
export declare const rawThemeColors: {
|
|
95
|
-
light:
|
|
96
|
-
dark:
|
|
128
|
+
light: ThemeColorTokens;
|
|
129
|
+
dark: ThemeColorTokens;
|
|
97
130
|
};
|
|
98
131
|
/**
|
|
99
132
|
* Resolves a theme color value to a literal color string. On web, theme
|
|
@@ -114,7 +147,9 @@ export declare function resolveRawColor(color: string, scheme: "light" | "dark")
|
|
|
114
147
|
* paint before any theme script stamps `data-theme`.
|
|
115
148
|
*
|
|
116
149
|
* Apps that override brand colors can pass those overrides so the emitted
|
|
117
|
-
* CSS matches their theme.
|
|
150
|
+
* CSS matches their theme. Extension tokens (`ThemeColorExtensions`) present
|
|
151
|
+
* in the overrides get a variable too, per scheme that names them; the
|
|
152
|
+
* package has no default to fall back to for those.
|
|
118
153
|
*/
|
|
119
154
|
export declare function getThemeCssVariables(overrides?: {
|
|
120
155
|
light?: Partial<ThemeColors>;
|
package/dist/constants/colors.js
CHANGED
|
@@ -139,6 +139,7 @@ const themeColorTokens = Object.keys(lightTheme.colors);
|
|
|
139
139
|
function cssVarName(token) {
|
|
140
140
|
return `--c-${token.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`)}`;
|
|
141
141
|
}
|
|
142
|
+
const themeColorTokenSet = new Set(themeColorTokens);
|
|
142
143
|
// Reverse lookup: "var(--c-surface-sunken)" → "surfaceSunken"
|
|
143
144
|
const varToToken = new Map(themeColorTokens.map((token) => [`var(${cssVarName(token)})`, token]));
|
|
144
145
|
/**
|
|
@@ -176,17 +177,27 @@ function toRgbTriplet(value) {
|
|
|
176
177
|
* paint before any theme script stamps `data-theme`.
|
|
177
178
|
*
|
|
178
179
|
* Apps that override brand colors can pass those overrides so the emitted
|
|
179
|
-
* CSS matches their theme.
|
|
180
|
+
* CSS matches their theme. Extension tokens (`ThemeColorExtensions`) present
|
|
181
|
+
* in the overrides get a variable too, per scheme that names them; the
|
|
182
|
+
* package has no default to fall back to for those.
|
|
180
183
|
*/
|
|
181
184
|
export function getThemeCssVariables(overrides) {
|
|
182
|
-
const
|
|
183
|
-
.
|
|
184
|
-
|
|
185
|
+
const extensionTokens = [
|
|
186
|
+
...Object.keys(overrides?.light ?? {}),
|
|
187
|
+
...Object.keys(overrides?.dark ?? {}),
|
|
188
|
+
].filter((token, index, all) => !themeColorTokenSet.has(token) && all.indexOf(token) === index);
|
|
189
|
+
const declaration = (token, value) => {
|
|
185
190
|
const name = cssVarName(token);
|
|
186
191
|
const rgb = toRgbTriplet(value);
|
|
187
192
|
return `${name}: ${value};${rgb ? ` ${name}-rgb: ${rgb};` : ""}`;
|
|
188
|
-
}
|
|
189
|
-
|
|
193
|
+
};
|
|
194
|
+
const declarations = (scheme) => [
|
|
195
|
+
...themeColorTokens.map((token) => declaration(token, overrides?.[scheme]?.[token] ?? rawThemeColors[scheme][token])),
|
|
196
|
+
...extensionTokens.flatMap((token) => {
|
|
197
|
+
const value = overrides?.[scheme]?.[token];
|
|
198
|
+
return typeof value === "string" ? [declaration(token, value)] : [];
|
|
199
|
+
}),
|
|
200
|
+
].join("\n ");
|
|
190
201
|
return `
|
|
191
202
|
:root {
|
|
192
203
|
${declarations("light")}
|
|
@@ -209,10 +220,17 @@ export function getThemeCssVariables(overrides) {
|
|
|
209
220
|
// values (no CSS engine). The `navigation` maps stay literal on both
|
|
210
221
|
// platforms: @react-navigation internals color-parse their theme values.
|
|
211
222
|
const webVarColors = Object.fromEntries(themeColorTokens.map((token) => [token, `var(${cssVarName(token)})`]));
|
|
223
|
+
// Web: both schemes hold the same `var(--c-*)` references, so they share ONE
|
|
224
|
+
// `colors` object. A scheme switch then keeps `theme.colors` identical, so
|
|
225
|
+
// anything memoized on it keeps its result; the CSS variables do the
|
|
226
|
+
// re-theming. `colors.light` and `colors.dark` stay distinct objects:
|
|
227
|
+
// `dark`, `navigation`, and `fonts` differ per scheme.
|
|
228
|
+
// The base themes carry the package tokens only; extension keys (typed on
|
|
229
|
+
// `ThemeColors` once an app augments it) arrive through `setColors`.
|
|
212
230
|
export const colors = Platform.OS === "web"
|
|
213
231
|
? {
|
|
214
|
-
light: { ...lightTheme, colors:
|
|
215
|
-
dark: { ...darkTheme, colors:
|
|
232
|
+
light: { ...lightTheme, colors: webVarColors },
|
|
233
|
+
dark: { ...darkTheme, colors: webVarColors },
|
|
216
234
|
}
|
|
217
235
|
: {
|
|
218
236
|
light: lightTheme,
|