@mrmeg/expo-ui 0.8.0 → 0.10.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/LLM_USAGE.md +3 -2
- package/dist/components/Drawer.d.ts +52 -5
- package/dist/components/Drawer.js +180 -3
- package/dist/components/Icon.d.ts +6 -3
- package/dist/components/Icon.js +10 -5
- package/dist/components/ToggleGroup.js +2 -7
- package/dist/hooks/useTheme.d.ts +1 -1
- package/dist/hooks/useTheme.js +28 -93
- package/package.json +1 -1
package/LLM_USAGE.md
CHANGED
|
@@ -125,8 +125,9 @@ Token intent:
|
|
|
125
125
|
|
|
126
126
|
Use `getShadowStyle()` for package surfaces that need elevation. It supports
|
|
127
127
|
`base`, `soft`, `sharp`, `subtle`, `elevated`, `glow`, `glass`, `card`,
|
|
128
|
-
`cardHover`, and `cardSubtle`, returning
|
|
129
|
-
|
|
128
|
+
`cardHover`, and `cardSubtle`, returning a cross-platform `boxShadow` value
|
|
129
|
+
(RN 0.85 + react-native-web 0.21 deprecate the legacy `shadow*` props). Use
|
|
130
|
+
`getFocusRingStyle()` for web focus styling. Keep
|
|
130
131
|
web controls compact, but preserve mobile tap comfort with package controls
|
|
131
132
|
that already provide native hit slop or 44px touch rows.
|
|
132
133
|
|
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import React from "react";
|
|
2
2
|
import { ViewProps, StyleProp, ViewStyle } from "react-native";
|
|
3
3
|
type DrawerSide = "left" | "right";
|
|
4
|
+
/**
|
|
5
|
+
* Drawer presentation mode.
|
|
6
|
+
* - `"overlay"` (default): modal drawer that slides in over content with a backdrop.
|
|
7
|
+
* - `"rail"`: docked, always-mounted collapsible sidebar (icon strip that expands to
|
|
8
|
+
* a labeled panel). It is in-flow and pushes sibling content as it grows.
|
|
9
|
+
*/
|
|
10
|
+
type DrawerVariant = "overlay" | "rail";
|
|
4
11
|
interface DrawerProps {
|
|
5
12
|
/** Controlled open state */
|
|
6
13
|
open?: boolean;
|
|
@@ -10,10 +17,30 @@ interface DrawerProps {
|
|
|
10
17
|
defaultOpen?: boolean;
|
|
11
18
|
/** Which side the drawer appears from */
|
|
12
19
|
side?: DrawerSide;
|
|
13
|
-
/** Drawer width in pixels or percentage string */
|
|
20
|
+
/** Drawer width in pixels or percentage string (overlay mode only) */
|
|
14
21
|
width?: number | `${number}%`;
|
|
15
22
|
/** Whether to close when backdrop is pressed */
|
|
16
23
|
closeOnBackdropPress?: boolean;
|
|
24
|
+
/**
|
|
25
|
+
* Presentation mode. `"overlay"` (default) is the classic modal drawer;
|
|
26
|
+
* `"rail"` is a docked collapsible sidebar. See {@link DrawerVariant}.
|
|
27
|
+
*/
|
|
28
|
+
variant?: DrawerVariant;
|
|
29
|
+
/** Collapsed (icon-strip) width in pixels for rail mode. @default 72 */
|
|
30
|
+
collapsedWidth?: number;
|
|
31
|
+
/** Expanded (labeled-panel) width in pixels for rail mode. @default 240 */
|
|
32
|
+
expandedWidth?: number;
|
|
33
|
+
/**
|
|
34
|
+
* Whether the rail expands on hover (web only — native has no hover).
|
|
35
|
+
* @default true on web, false on native
|
|
36
|
+
*/
|
|
37
|
+
expandOnHover?: boolean;
|
|
38
|
+
/** Default expanded state for uncontrolled rail mode. @default false */
|
|
39
|
+
defaultExpanded?: boolean;
|
|
40
|
+
/** Controlled expanded state for rail mode */
|
|
41
|
+
expanded?: boolean;
|
|
42
|
+
/** Callback when rail expanded state changes */
|
|
43
|
+
onExpandedChange?: (expanded: boolean) => void;
|
|
17
44
|
/** Children components */
|
|
18
45
|
children: React.ReactNode;
|
|
19
46
|
}
|
|
@@ -46,9 +73,28 @@ interface DrawerBodyProps extends ViewProps {
|
|
|
46
73
|
interface DrawerFooterProps extends ViewProps {
|
|
47
74
|
children: React.ReactNode;
|
|
48
75
|
}
|
|
49
|
-
declare function DrawerRoot({ open: controlledOpen, onOpenChange: controlledOnOpenChange, defaultOpen, side, width, closeOnBackdropPress, children, }: DrawerProps): React.JSX.Element;
|
|
76
|
+
declare function DrawerRoot({ open: controlledOpen, onOpenChange: controlledOnOpenChange, defaultOpen, side, width, closeOnBackdropPress, variant, collapsedWidth, expandedWidth, expandOnHover, defaultExpanded, expanded: controlledExpanded, onExpandedChange: controlledOnExpandedChange, children, }: DrawerProps): React.JSX.Element;
|
|
50
77
|
declare function DrawerTrigger({ asChild, children, style: styleOverride }: DrawerTriggerProps): React.JSX.Element;
|
|
51
|
-
|
|
78
|
+
interface DrawerToggleCollapseProps {
|
|
79
|
+
/** Use child component as the toggle */
|
|
80
|
+
asChild?: boolean;
|
|
81
|
+
/** Children components */
|
|
82
|
+
children: React.ReactNode;
|
|
83
|
+
/** Optional style override */
|
|
84
|
+
style?: StyleProp<ViewStyle>;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Toggles the rail's expanded state. Native has no hover, so a rail needs an
|
|
88
|
+
* explicit expand/collapse control; this provides it. On web it works too and
|
|
89
|
+
* coexists with `expandOnHover`.
|
|
90
|
+
*/
|
|
91
|
+
declare function DrawerToggleCollapse({ asChild, children, style: styleOverride }: DrawerToggleCollapseProps): React.JSX.Element;
|
|
92
|
+
/**
|
|
93
|
+
* DrawerContent dispatches to the overlay or rail implementation based on the
|
|
94
|
+
* `variant` set on the Drawer root. Both implementations are separate components
|
|
95
|
+
* so their hooks never run conditionally.
|
|
96
|
+
*/
|
|
97
|
+
declare function DrawerContent(props: DrawerContentProps): React.JSX.Element;
|
|
52
98
|
declare function DrawerHeader({ children, style, ...props }: DrawerHeaderProps): React.JSX.Element;
|
|
53
99
|
declare function DrawerBody({ children, style, ...props }: DrawerBodyProps): React.JSX.Element;
|
|
54
100
|
declare function DrawerFooter({ children, style, ...props }: DrawerFooterProps): React.JSX.Element;
|
|
@@ -69,6 +115,7 @@ declare const Drawer: typeof DrawerRoot & {
|
|
|
69
115
|
Body: typeof DrawerBody;
|
|
70
116
|
Footer: typeof DrawerFooter;
|
|
71
117
|
Close: typeof DrawerClose;
|
|
118
|
+
ToggleCollapse: typeof DrawerToggleCollapse;
|
|
72
119
|
};
|
|
73
|
-
export { Drawer, DrawerTrigger, DrawerContent, DrawerHeader, DrawerBody, DrawerFooter, DrawerClose, useDrawerClose, };
|
|
74
|
-
export type { DrawerProps, DrawerTriggerProps, DrawerContentProps, DrawerHeaderProps, DrawerBodyProps, DrawerFooterProps, DrawerCloseProps, };
|
|
120
|
+
export { Drawer, DrawerTrigger, DrawerContent, DrawerHeader, DrawerBody, DrawerFooter, DrawerClose, DrawerToggleCollapse, useDrawerClose, };
|
|
121
|
+
export type { DrawerProps, DrawerVariant, DrawerTriggerProps, DrawerContentProps, DrawerHeaderProps, DrawerBodyProps, DrawerFooterProps, DrawerCloseProps, DrawerToggleCollapseProps, };
|
|
@@ -78,11 +78,31 @@ function drawerReducer(state, action) {
|
|
|
78
78
|
// ============================================================================
|
|
79
79
|
// Drawer Root Component
|
|
80
80
|
// ============================================================================
|
|
81
|
-
function DrawerRoot({ open: controlledOpen, onOpenChange: controlledOnOpenChange, defaultOpen = false, side = "left", width = 300, closeOnBackdropPress = true, children, }) {
|
|
81
|
+
function DrawerRoot({ open: controlledOpen, onOpenChange: controlledOnOpenChange, defaultOpen = false, side = "left", width = 300, closeOnBackdropPress = true, variant = "overlay", collapsedWidth = 72, expandedWidth = 240, expandOnHover = Platform.OS === "web", defaultExpanded = false, expanded: controlledExpanded, onExpandedChange: controlledOnExpandedChange, children, }) {
|
|
82
82
|
// Use reducer for stable state management - dispatch is stable and reducer always gets current state
|
|
83
83
|
const [internalOpen, dispatch] = useReducer(drawerReducer, defaultOpen);
|
|
84
84
|
const isControlled = controlledOpen !== undefined;
|
|
85
85
|
const open = isControlled ? controlledOpen : internalOpen;
|
|
86
|
+
// Rail expand/collapse state (mirrors the open machinery: controlled or uncontrolled)
|
|
87
|
+
const [internalExpanded, expandedDispatch] = useReducer(drawerReducer, defaultExpanded);
|
|
88
|
+
const isExpandedControlled = controlledExpanded !== undefined;
|
|
89
|
+
const expanded = isExpandedControlled ? controlledExpanded : internalExpanded;
|
|
90
|
+
const setExpanded = (newExpanded) => {
|
|
91
|
+
if (isExpandedControlled) {
|
|
92
|
+
controlledOnExpandedChange?.(newExpanded);
|
|
93
|
+
}
|
|
94
|
+
else {
|
|
95
|
+
expandedDispatch({ type: newExpanded ? "OPEN" : "CLOSE" });
|
|
96
|
+
}
|
|
97
|
+
};
|
|
98
|
+
const toggleExpanded = () => {
|
|
99
|
+
if (isExpandedControlled) {
|
|
100
|
+
controlledOnExpandedChange?.(!controlledExpanded);
|
|
101
|
+
}
|
|
102
|
+
else {
|
|
103
|
+
expandedDispatch({ type: "TOGGLE" });
|
|
104
|
+
}
|
|
105
|
+
};
|
|
86
106
|
// Stable toggle function - dispatch is stable across renders
|
|
87
107
|
const toggle = () => {
|
|
88
108
|
if (isControlled) {
|
|
@@ -113,6 +133,13 @@ function DrawerRoot({ open: controlledOpen, onOpenChange: controlledOnOpenChange
|
|
|
113
133
|
side,
|
|
114
134
|
width: parsedWidth,
|
|
115
135
|
closeOnBackdropPress,
|
|
136
|
+
variant,
|
|
137
|
+
expanded,
|
|
138
|
+
setExpanded,
|
|
139
|
+
toggleExpanded,
|
|
140
|
+
collapsedWidth,
|
|
141
|
+
expandedWidth,
|
|
142
|
+
expandOnHover,
|
|
116
143
|
};
|
|
117
144
|
return (_jsx(DrawerContext.Provider, { value: contextValue, children: children }));
|
|
118
145
|
}
|
|
@@ -142,10 +169,50 @@ function DrawerTrigger({ asChild, children, style: styleOverride }) {
|
|
|
142
169
|
styleOverride,
|
|
143
170
|
], children: children }));
|
|
144
171
|
}
|
|
172
|
+
/**
|
|
173
|
+
* Toggles the rail's expanded state. Native has no hover, so a rail needs an
|
|
174
|
+
* explicit expand/collapse control; this provides it. On web it works too and
|
|
175
|
+
* coexists with `expandOnHover`.
|
|
176
|
+
*/
|
|
177
|
+
function DrawerToggleCollapse({ asChild, children, style: styleOverride }) {
|
|
178
|
+
const { expanded, toggleExpanded } = useDrawerContext();
|
|
179
|
+
const accessibilityLabel = expanded ? "Collapse sidebar" : "Expand sidebar";
|
|
180
|
+
const handlePress = () => {
|
|
181
|
+
toggleExpanded();
|
|
182
|
+
};
|
|
183
|
+
if (asChild && React.isValidElement(children)) {
|
|
184
|
+
return React.cloneElement(children, {
|
|
185
|
+
onPress: handlePress,
|
|
186
|
+
accessibilityRole: "button",
|
|
187
|
+
accessibilityLabel,
|
|
188
|
+
style: [
|
|
189
|
+
children.props.style,
|
|
190
|
+
Platform.OS === "web" && { cursor: "pointer" },
|
|
191
|
+
styleOverride,
|
|
192
|
+
],
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
return (_jsx(Pressable, { onPress: handlePress, accessibilityRole: "button", accessibilityLabel: accessibilityLabel, style: [
|
|
196
|
+
Platform.OS === "web" && { cursor: "pointer" },
|
|
197
|
+
styleOverride,
|
|
198
|
+
], children: children }));
|
|
199
|
+
}
|
|
145
200
|
// ============================================================================
|
|
146
201
|
// Drawer Content Component
|
|
147
202
|
// ============================================================================
|
|
148
|
-
|
|
203
|
+
/**
|
|
204
|
+
* DrawerContent dispatches to the overlay or rail implementation based on the
|
|
205
|
+
* `variant` set on the Drawer root. Both implementations are separate components
|
|
206
|
+
* so their hooks never run conditionally.
|
|
207
|
+
*/
|
|
208
|
+
function DrawerContent(props) {
|
|
209
|
+
const { variant } = useDrawerContext();
|
|
210
|
+
if (variant === "rail") {
|
|
211
|
+
return _jsx(DrawerRailContent, { ...props });
|
|
212
|
+
}
|
|
213
|
+
return _jsx(DrawerOverlayContent, { ...props });
|
|
214
|
+
}
|
|
215
|
+
function DrawerOverlayContent({ swipeEnabled = true, swipeThreshold = 0.3, velocityThreshold = 500, style: styleOverride, children, ...props }) {
|
|
149
216
|
const drawerContext = useDrawerContext();
|
|
150
217
|
const { open, onOpenChange, side, width, closeOnBackdropPress } = drawerContext;
|
|
151
218
|
const { theme, getShadowStyle } = useTheme();
|
|
@@ -358,6 +425,115 @@ function DrawerContent({ swipeEnabled = true, swipeThreshold = 0.3, velocityThre
|
|
|
358
425
|
return contentElement;
|
|
359
426
|
}
|
|
360
427
|
// ============================================================================
|
|
428
|
+
// Drawer Rail Content Component
|
|
429
|
+
// ============================================================================
|
|
430
|
+
/**
|
|
431
|
+
* Rail variant of DrawerContent: a docked, always-mounted collapsible sidebar.
|
|
432
|
+
*
|
|
433
|
+
* Native open model: the rail is always docked and collapsed; `Drawer.ToggleCollapse`
|
|
434
|
+
* (or hover on web) expands it. It is decoupled from the overlay `open` state — there
|
|
435
|
+
* is no slide-in/unmount.
|
|
436
|
+
*
|
|
437
|
+
* Layout model: the rail is **in-flow** and pushes sibling content. The panel itself
|
|
438
|
+
* occupies layout width (`collapsedWidth` → `expandedWidth`); animating that width
|
|
439
|
+
* reflows whatever renders beside it. Put the rail and the content in a
|
|
440
|
+
* `flexDirection: "row"` container so the content claims the remaining space.
|
|
441
|
+
*/
|
|
442
|
+
function DrawerRailContent({
|
|
443
|
+
// Overlay-only props are accepted (shared DrawerContentProps) but ignored in rail mode.
|
|
444
|
+
swipeEnabled: _swipeEnabled, swipeThreshold: _swipeThreshold, velocityThreshold: _velocityThreshold, style: styleOverride, children, ...props }) {
|
|
445
|
+
const { side, expanded, collapsedWidth, expandedWidth, expandOnHover } = useDrawerContext();
|
|
446
|
+
const { theme, getShadowStyle } = useTheme();
|
|
447
|
+
const insets = useSafeAreaInsets();
|
|
448
|
+
// Hover is a transient "peek" tracked locally; the pinned state comes from
|
|
449
|
+
// `expanded` (toggle / controlled prop). The rail is open when either is true,
|
|
450
|
+
// so hovering then leaving never clears a pin the toggle set — they don't
|
|
451
|
+
// share one piece of state. Hover is web-only (native has no pointer).
|
|
452
|
+
const [hovered, setHovered] = useState(false);
|
|
453
|
+
// If the rail is explicitly collapsed (toggle / controlled prop flips
|
|
454
|
+
// `expanded` true→false) while the pointer is still over it, the active hover
|
|
455
|
+
// would instantly re-expand it and the collapse would look like a no-op.
|
|
456
|
+
// Suppress the current hover session in that case; a fresh mouse-enter clears
|
|
457
|
+
// the suppression so peek-on-hover works again. Tracked in a ref, read during
|
|
458
|
+
// the re-render that the `expanded` change already triggers (same pattern as
|
|
459
|
+
// `lastExpandedRef` below).
|
|
460
|
+
const hoverSuppressedRef = useRef(false);
|
|
461
|
+
const prevExpandedRef = useRef(expanded);
|
|
462
|
+
if (prevExpandedRef.current !== expanded) {
|
|
463
|
+
const wasExpanded = prevExpandedRef.current;
|
|
464
|
+
prevExpandedRef.current = expanded;
|
|
465
|
+
if (wasExpanded && !expanded && hovered) {
|
|
466
|
+
hoverSuppressedRef.current = true;
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
const effectiveExpanded = expanded || (expandOnHover && hovered && !hoverSuppressedRef.current);
|
|
470
|
+
const textColor = theme.colors.foreground;
|
|
471
|
+
const targetWidth = effectiveExpanded ? expandedWidth : collapsedWidth;
|
|
472
|
+
// Native animates width via Animated.Value (layout prop → useNativeDriver: false).
|
|
473
|
+
// Web sets the width directly and lets the inline CSS `transition` animate it.
|
|
474
|
+
const widthRef = useRef(null);
|
|
475
|
+
if (widthRef.current === null) {
|
|
476
|
+
widthRef.current = new Animated.Value(targetWidth);
|
|
477
|
+
}
|
|
478
|
+
const widthAnim = widthRef.current;
|
|
479
|
+
// Trigger the native width animation during render when expansion changes,
|
|
480
|
+
// mirroring the overlay's lastOpenRef pattern above. Skip the first render:
|
|
481
|
+
// the Animated.Value is already initialized to the current target, so there is
|
|
482
|
+
// nothing to animate toward on mount.
|
|
483
|
+
const lastExpandedRef = useRef(null);
|
|
484
|
+
if (Platform.OS !== "web" && effectiveExpanded !== lastExpandedRef.current) {
|
|
485
|
+
const previousExpanded = lastExpandedRef.current;
|
|
486
|
+
lastExpandedRef.current = effectiveExpanded;
|
|
487
|
+
if (previousExpanded !== null) {
|
|
488
|
+
Animated.timing(widthAnim, {
|
|
489
|
+
toValue: targetWidth,
|
|
490
|
+
duration: 180,
|
|
491
|
+
useNativeDriver: false,
|
|
492
|
+
}).start();
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
const shadowStyle = effectiveExpanded
|
|
496
|
+
? StyleSheet.flatten(getShadowStyle("elevated"))
|
|
497
|
+
: undefined;
|
|
498
|
+
// The rail is in-flow: its own width is what content sits beside, so growing it
|
|
499
|
+
// pushes that content. No absolute positioning, no spacer.
|
|
500
|
+
const panelStyle = {
|
|
501
|
+
width: Platform.OS === "web" ? targetWidth : widthAnim,
|
|
502
|
+
overflow: "hidden",
|
|
503
|
+
backgroundColor: theme.colors.background,
|
|
504
|
+
borderColor: theme.colors.border,
|
|
505
|
+
...(side === "left" ? { borderRightWidth: 1 } : { borderLeftWidth: 1 }),
|
|
506
|
+
paddingTop: insets.top,
|
|
507
|
+
paddingBottom: insets.bottom,
|
|
508
|
+
...(Platform.OS === "web" && {
|
|
509
|
+
transition: "width 0.18s ease, box-shadow 0.18s ease",
|
|
510
|
+
}),
|
|
511
|
+
};
|
|
512
|
+
// Hover-to-expand is web-only; native relies on Drawer.ToggleCollapse. These
|
|
513
|
+
// only toggle the transient hover state — they never touch the pinned
|
|
514
|
+
// `expanded`, so leaving the rail can't collapse a toggle-pinned panel. A
|
|
515
|
+
// fresh mouse-enter clears any hover suppression left by an in-place collapse.
|
|
516
|
+
const hoverHandlers = Platform.OS === "web" && expandOnHover
|
|
517
|
+
? {
|
|
518
|
+
onMouseEnter: () => {
|
|
519
|
+
hoverSuppressedRef.current = false;
|
|
520
|
+
setHovered(true);
|
|
521
|
+
},
|
|
522
|
+
onMouseLeave: () => {
|
|
523
|
+
hoverSuppressedRef.current = false;
|
|
524
|
+
setHovered(false);
|
|
525
|
+
},
|
|
526
|
+
}
|
|
527
|
+
: {};
|
|
528
|
+
return (_jsx(Animated.View, { style: [
|
|
529
|
+
panelStyle,
|
|
530
|
+
shadowStyle,
|
|
531
|
+
styleOverride && typeof styleOverride !== "function"
|
|
532
|
+
? StyleSheet.flatten(styleOverride)
|
|
533
|
+
: undefined,
|
|
534
|
+
], ...(Platform.OS === "web" && { role: "navigation", ...hoverHandlers }), ...props, children: _jsx(TextColorContext.Provider, { value: textColor, children: _jsx(TextClassContext.Provider, { value: "", children: children }) }) }));
|
|
535
|
+
}
|
|
536
|
+
// ============================================================================
|
|
361
537
|
// Drawer Header Component
|
|
362
538
|
// ============================================================================
|
|
363
539
|
function DrawerHeader({ children, style, ...props }) {
|
|
@@ -430,5 +606,6 @@ const Drawer = Object.assign(DrawerRoot, {
|
|
|
430
606
|
Body: DrawerBody,
|
|
431
607
|
Footer: DrawerFooter,
|
|
432
608
|
Close: DrawerClose,
|
|
609
|
+
ToggleCollapse: DrawerToggleCollapse,
|
|
433
610
|
});
|
|
434
|
-
export { Drawer, DrawerTrigger, DrawerContent, DrawerHeader, DrawerBody, DrawerFooter, DrawerClose, useDrawerClose, };
|
|
611
|
+
export { Drawer, DrawerTrigger, DrawerContent, DrawerHeader, DrawerBody, DrawerFooter, DrawerClose, DrawerToggleCollapse, useDrawerClose, };
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import * as React from "react";
|
|
2
2
|
import type { StyleProp, TextProps, TextStyle } from "react-native";
|
|
3
3
|
import Feather from "@expo/vector-icons/Feather";
|
|
4
|
+
import type { ThemeColors } from "../constants/colors";
|
|
4
5
|
/**
|
|
5
|
-
* Theme color names that can be used as shortcuts
|
|
6
|
-
*
|
|
6
|
+
* Theme color names that can be used as shortcuts.
|
|
7
|
+
*
|
|
8
|
+
* Derived from {@link ThemeColors} so it always covers every semantic token
|
|
9
|
+
* (`foreground`, `accent`, `border`, …) and can never drift from the theme.
|
|
7
10
|
*/
|
|
8
|
-
export type ThemeColorName =
|
|
11
|
+
export type ThemeColorName = keyof ThemeColors;
|
|
9
12
|
export type IconName = React.ComponentProps<typeof Feather>["name"];
|
|
10
13
|
type IconBaseProps = {
|
|
11
14
|
/** Size of the icon in pixels */
|
package/dist/components/Icon.js
CHANGED
|
@@ -1,14 +1,19 @@
|
|
|
1
1
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { useTheme } from "../hooks/useTheme.js";
|
|
3
3
|
import Feather from "@expo/vector-icons/Feather";
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
4
|
+
/**
|
|
5
|
+
* Resolve an icon color against the active theme.
|
|
6
|
+
*
|
|
7
|
+
* A string that names an existing theme color resolves to that semantic color;
|
|
8
|
+
* anything else is treated as a literal color value (hex, `rgb()`, or a CSS
|
|
9
|
+
* named color). Checking the live theme object — rather than a hand-maintained
|
|
10
|
+
* list — means new tokens are usable as icon colors automatically, and a token
|
|
11
|
+
* name never silently falls through as an invalid literal.
|
|
12
|
+
*/
|
|
8
13
|
function resolveIconColor(color, themeColors) {
|
|
9
14
|
if (!color)
|
|
10
15
|
return themeColors.text;
|
|
11
|
-
if (
|
|
16
|
+
if (Object.prototype.hasOwnProperty.call(themeColors, color)) {
|
|
12
17
|
return themeColors[color];
|
|
13
18
|
}
|
|
14
19
|
return color;
|
|
@@ -64,7 +64,7 @@ function useToggleGroupContext() {
|
|
|
64
64
|
* ```
|
|
65
65
|
*/
|
|
66
66
|
function ToggleGroup({ variant = "default", size = "default", children, ...props }) {
|
|
67
|
-
const {
|
|
67
|
+
const { getShadowStyle } = useTheme();
|
|
68
68
|
const contextValue = React.useMemo(() => ({ variant, size }), [variant, size]);
|
|
69
69
|
// Count valid children for first/last detection
|
|
70
70
|
const childrenArray = React.Children.toArray(children);
|
|
@@ -89,12 +89,7 @@ function ToggleGroup({ variant = "default", size = "default", children, ...props
|
|
|
89
89
|
alignItems: "center",
|
|
90
90
|
borderRadius: spacing.radiusMd,
|
|
91
91
|
// No shadow on Android - causes text background artifact
|
|
92
|
-
...(variant === "outline" && Platform.OS === "ios" &&
|
|
93
|
-
shadowColor: theme.colors.overlay,
|
|
94
|
-
shadowOffset: { width: 0, height: 1 },
|
|
95
|
-
shadowOpacity: 0.05,
|
|
96
|
-
shadowRadius: 2,
|
|
97
|
-
}),
|
|
92
|
+
...(variant === "outline" && Platform.OS === "ios" && getShadowStyle("subtle")),
|
|
98
93
|
...(Platform.OS === "web" && {
|
|
99
94
|
width: "fit-content",
|
|
100
95
|
}),
|
package/dist/hooks/useTheme.d.ts
CHANGED
|
@@ -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') → {
|
|
35
|
+
* - getShadowStyle('base') → { boxShadow: "0px 1px 3px rgba(0, 0, 0, 0.1)" }
|
|
36
36
|
*/
|
|
37
37
|
export declare function useTheme(): ExtendedColorScheme & {
|
|
38
38
|
toggleTheme: () => void;
|
package/dist/hooks/useTheme.js
CHANGED
|
@@ -42,7 +42,7 @@ function getCachedOrCompute(key, compute) {
|
|
|
42
42
|
* - getTextColorForBackground("#000") → "light"
|
|
43
43
|
* - getContrastingColor("#f4f4f4", "#222", "#fff") → "#222"
|
|
44
44
|
* - withAlpha("#336699", 0.6) → "rgba(51,102,153,0.6)"
|
|
45
|
-
* - getShadowStyle('base') → {
|
|
45
|
+
* - getShadowStyle('base') → { boxShadow: "0px 1px 3px rgba(0, 0, 0, 0.1)" }
|
|
46
46
|
*/
|
|
47
47
|
export function useTheme() {
|
|
48
48
|
const userTheme = useThemeStore((s) => s.userTheme);
|
|
@@ -93,102 +93,37 @@ export function useTheme() {
|
|
|
93
93
|
}, [setTheme, userTheme]);
|
|
94
94
|
/**
|
|
95
95
|
* getShadowStyle
|
|
96
|
-
* Returns platform
|
|
97
|
-
*
|
|
98
|
-
* -
|
|
96
|
+
* Returns a cross-platform shadow style using the `boxShadow` style prop.
|
|
97
|
+
*
|
|
98
|
+
* RN 0.85 + react-native-web 0.21 deprecate the legacy `shadow*` props in
|
|
99
|
+
* favor of `boxShadow`, which is supported on both native and web. Because
|
|
100
|
+
* `boxShadow` has no separate opacity field, each preset's opacity is folded
|
|
101
|
+
* into the color's alpha via `withAlpha`. `elevation` is dropped — `boxShadow`
|
|
102
|
+
* renders shadows on Android in 0.85+.
|
|
99
103
|
*/
|
|
100
104
|
const getShadowStyle = useCallback((type) => {
|
|
105
|
+
// Each preset: [offsetX, offsetY, blurRadius, color, opacity].
|
|
106
|
+
// Darker themes get a stronger alpha so shadows stay visible.
|
|
107
|
+
const boost = theme.dark ? 3 : 1;
|
|
108
|
+
const overlay = theme.colors.overlay;
|
|
101
109
|
const shadowConfigs = {
|
|
102
|
-
base: {
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
},
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
shadowOffset: { width: 0, height: 1 },
|
|
119
|
-
shadowOpacity: 0.15,
|
|
120
|
-
shadowRadius: 1,
|
|
121
|
-
elevation: 2,
|
|
122
|
-
},
|
|
123
|
-
subtle: {
|
|
124
|
-
shadowColor: theme.colors.overlay,
|
|
125
|
-
shadowOffset: { width: 0, height: 1 },
|
|
126
|
-
shadowOpacity: 0.05,
|
|
127
|
-
shadowRadius: 2,
|
|
128
|
-
elevation: 1,
|
|
129
|
-
},
|
|
130
|
-
elevated: {
|
|
131
|
-
shadowColor: theme.colors.overlay,
|
|
132
|
-
shadowOffset: { width: 0, height: 20 },
|
|
133
|
-
shadowOpacity: 0.15,
|
|
134
|
-
shadowRadius: 40,
|
|
135
|
-
elevation: 16,
|
|
136
|
-
},
|
|
137
|
-
glow: {
|
|
138
|
-
shadowColor: theme.colors.primary,
|
|
139
|
-
shadowOffset: { width: 0, height: 4 },
|
|
140
|
-
shadowOpacity: 0.4,
|
|
141
|
-
shadowRadius: 20,
|
|
142
|
-
elevation: 10,
|
|
143
|
-
},
|
|
144
|
-
glass: {
|
|
145
|
-
shadowColor: theme.colors.overlay,
|
|
146
|
-
shadowOffset: { width: 0, height: 4 },
|
|
147
|
-
shadowOpacity: 0.05,
|
|
148
|
-
shadowRadius: 30,
|
|
149
|
-
elevation: 4,
|
|
150
|
-
},
|
|
151
|
-
card: {
|
|
152
|
-
shadowColor: theme.colors.overlay,
|
|
153
|
-
shadowOffset: { width: 0, height: 2 },
|
|
154
|
-
shadowOpacity: 0.08,
|
|
155
|
-
shadowRadius: 8,
|
|
156
|
-
elevation: 4,
|
|
157
|
-
},
|
|
158
|
-
cardHover: {
|
|
159
|
-
shadowColor: theme.colors.overlay,
|
|
160
|
-
shadowOffset: { width: 0, height: 8 },
|
|
161
|
-
shadowOpacity: 0.12,
|
|
162
|
-
shadowRadius: 24,
|
|
163
|
-
elevation: 8,
|
|
164
|
-
},
|
|
165
|
-
cardSubtle: {
|
|
166
|
-
shadowColor: theme.colors.overlay,
|
|
167
|
-
shadowOffset: { width: 0, height: 1 },
|
|
168
|
-
shadowOpacity: 0.08,
|
|
169
|
-
shadowRadius: 3,
|
|
170
|
-
elevation: 2,
|
|
171
|
-
},
|
|
110
|
+
base: { x: 0, y: 1, blur: 3, color: overlay, opacity: 0.1 },
|
|
111
|
+
soft: { x: 0, y: 4, blur: 6, color: overlay, opacity: 0.1 },
|
|
112
|
+
sharp: { x: 0, y: 1, blur: 1, color: overlay, opacity: 0.15 },
|
|
113
|
+
subtle: { x: 0, y: 1, blur: 2, color: overlay, opacity: 0.05 },
|
|
114
|
+
elevated: { x: 0, y: 20, blur: 40, color: overlay, opacity: 0.15 },
|
|
115
|
+
glow: { x: 0, y: 4, blur: 20, color: theme.colors.primary, opacity: 0.4 },
|
|
116
|
+
glass: { x: 0, y: 4, blur: 30, color: overlay, opacity: 0.05 },
|
|
117
|
+
card: { x: 0, y: 2, blur: 8, color: overlay, opacity: 0.08 },
|
|
118
|
+
cardHover: { x: 0, y: 8, blur: 24, color: overlay, opacity: 0.12 },
|
|
119
|
+
cardSubtle: { x: 0, y: 1, blur: 3, color: overlay, opacity: 0.08 },
|
|
120
|
+
};
|
|
121
|
+
const { x, y, blur, color, opacity } = shadowConfigs[type];
|
|
122
|
+
// Don't boost the glow accent — it's already a deliberate, vivid alpha.
|
|
123
|
+
const alpha = color === theme.colors.primary ? opacity : Math.min(opacity * boost, 1);
|
|
124
|
+
return {
|
|
125
|
+
boxShadow: `${x}px ${y}px ${blur}px ${withAlpha(color, alpha)}`,
|
|
172
126
|
};
|
|
173
|
-
const config = shadowConfigs[type];
|
|
174
|
-
if (Platform.OS === "web") {
|
|
175
|
-
const webShadows = {
|
|
176
|
-
base: { boxShadow: theme.dark ? "0 1px 2px rgba(0, 0, 0, 0.45)" : "0 1px 2px rgba(0, 0, 0, 0.08)" },
|
|
177
|
-
soft: { boxShadow: theme.dark ? "0 8px 24px rgba(0, 0, 0, 0.36)" : "0 8px 24px rgba(0, 0, 0, 0.10)" },
|
|
178
|
-
sharp: { boxShadow: theme.dark ? "0 1px 1px rgba(0, 0, 0, 0.55)" : "0 1px 1px rgba(0, 0, 0, 0.12)" },
|
|
179
|
-
subtle: { boxShadow: theme.dark ? "0 1px 2px rgba(0, 0, 0, 0.32)" : "0 1px 2px rgba(0, 0, 0, 0.05)" },
|
|
180
|
-
elevated: { boxShadow: theme.dark ? "0 20px 40px rgba(0, 0, 0, 0.38)" : "0 20px 40px rgba(0, 0, 0, 0.15)" },
|
|
181
|
-
glow: { boxShadow: `0 0 20px ${theme.colors.primary}` },
|
|
182
|
-
glass: { boxShadow: theme.dark ? "0 4px 30px rgba(0, 0, 0, 0.32)" : "0 4px 30px rgba(0, 0, 0, 0.05)" },
|
|
183
|
-
card: { boxShadow: theme.dark ? "0 1px 2px rgba(0, 0, 0, 0.32)" : "0 1px 3px rgba(0, 0, 0, 0.08)" },
|
|
184
|
-
cardHover: { boxShadow: theme.dark ? "0 8px 24px rgba(0, 0, 0, 0.36)" : "0 8px 24px rgba(0, 0, 0, 0.12)" },
|
|
185
|
-
cardSubtle: { boxShadow: theme.dark ? "0 1px 2px rgba(0, 0, 0, 0.32)" : "0 1px 3px rgba(0, 0, 0, 0.05)" },
|
|
186
|
-
};
|
|
187
|
-
return webShadows[type];
|
|
188
|
-
}
|
|
189
|
-
return Platform.select({
|
|
190
|
-
default: config,
|
|
191
|
-
});
|
|
192
127
|
}, [theme]);
|
|
193
128
|
const getFocusRingStyle = useCallback((offset = 2) => {
|
|
194
129
|
if (Platform.OS !== "web") {
|