@mrmeg/expo-ui 0.9.0 → 0.10.1
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 +24 -0
- 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/TextInput.js +15 -5
- package/package.json +2 -1
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `@mrmeg/expo-ui` are documented here. This project
|
|
4
|
+
adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
5
|
+
|
|
6
|
+
## [0.10.1]
|
|
7
|
+
|
|
8
|
+
### Fixed
|
|
9
|
+
|
|
10
|
+
- Fix TextInput rounded-corner fill leak on New Architecture. On Fabric, the
|
|
11
|
+
`outline`/`filled` variants stroked a rounded border but painted the
|
|
12
|
+
background fill as an un-clipped rect, so the fill's square corners poked past
|
|
13
|
+
the rounded stroke (most visible on dark themes and the `filled` variant). The
|
|
14
|
+
fill, border, and radius now live on the RN wrapper `View` with
|
|
15
|
+
`overflow: "hidden"`, and the native `@expo/ui` host renders transparent inside
|
|
16
|
+
that clipped rounded surface. The `underlined` variant (bottom border only),
|
|
17
|
+
error state, `forceLight` mode, and the secure-entry eye toggle are unchanged.
|
|
18
|
+
|
|
19
|
+
## [0.10.0]
|
|
20
|
+
|
|
21
|
+
### Added
|
|
22
|
+
|
|
23
|
+
- Drawer collapsible rail mode (`variant="rail"`, `Drawer.ToggleCollapse`).
|
|
24
|
+
- Theme-aware Icon color resolution.
|
|
@@ -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;
|
|
@@ -243,16 +243,26 @@ leftElement, rightElement, clearable, focusedStyle, style, ...rest }) {
|
|
|
243
243
|
const textColor = forceLight
|
|
244
244
|
? "#1f2937"
|
|
245
245
|
: getContrastingColor(backgroundColor === "transparent" ? theme.colors.background : backgroundColor, theme.colors.text, palette.white);
|
|
246
|
-
//
|
|
247
|
-
//
|
|
248
|
-
|
|
246
|
+
// The rounded surface (fill + border + radius) lives on the RN wrapper View,
|
|
247
|
+
// NOT on the native field. On the New Architecture (Fabric), @expo/ui's host
|
|
248
|
+
// paints `backgroundColor` as an un-clipped rect and strokes the rounded border
|
|
249
|
+
// on top, so a fill handed to the host leaks square corners past the stroke.
|
|
250
|
+
// Letting the RN View own the surface (with `overflow: "hidden"`) keeps the
|
|
251
|
+
// fill clipped to `borderRadius`; the native host sits transparent inside it.
|
|
252
|
+
const surfaceStyle = {
|
|
249
253
|
backgroundColor,
|
|
250
254
|
borderColor,
|
|
251
255
|
borderRadius: variant === "underlined" ? 0 : spacing.radiusMd,
|
|
252
256
|
borderWidth: variant === "outline" ? 1 : 0,
|
|
257
|
+
opacity: editable === false ? 0.6 : 1,
|
|
258
|
+
overflow: "hidden",
|
|
259
|
+
};
|
|
260
|
+
// Native field: transparent, padding + height only. The visible surface is
|
|
261
|
+
// drawn by `surfaceStyle` on the wrapper above.
|
|
262
|
+
const boxStyle = {
|
|
263
|
+
backgroundColor: "transparent",
|
|
253
264
|
paddingHorizontal: sizeConfig.paddingHorizontal,
|
|
254
265
|
paddingVertical: sizeConfig.paddingVertical,
|
|
255
|
-
opacity: editable === false ? 0.6 : 1,
|
|
256
266
|
...(multiline ? null : { height: sizeConfig.height }),
|
|
257
267
|
};
|
|
258
268
|
// "System" is an RN-only sentinel (RCTFont resolves it to the system font).
|
|
@@ -268,7 +278,7 @@ leftElement, rightElement, clearable, focusedStyle, style, ...rest }) {
|
|
|
268
278
|
fontSize: sizeConfig.fontSize,
|
|
269
279
|
...(nativeFontFamily ? { fontFamily: nativeFontFamily } : null),
|
|
270
280
|
};
|
|
271
|
-
return (_jsxs(View, { style: wrapperStyle, children: [!!label && (_jsx(View, { style: styles.labelContainer, children: _jsxs(StyledText, { selectable: false, style: styles.label, children: [label, required && _jsx(StyledText, { selectable: false, style: styles.required, children: " *" })] }) })), _jsxs(View, { style: hasSecureToggle
|
|
281
|
+
return (_jsxs(View, { style: wrapperStyle, children: [!!label && (_jsx(View, { style: styles.labelContainer, children: _jsxs(StyledText, { selectable: false, style: styles.label, children: [label, required && _jsx(StyledText, { selectable: false, style: styles.required, children: " *" })] }) })), _jsxs(View, { style: [surfaceStyle, hasSecureToggle && styles.nativeRow], children: [_jsx(Host, { matchContents: { vertical: true }, style: hasSecureToggle ? styles.nativeHostFlex : styles.nativeHost, children: _jsx(ExpoTextInput, { ...rest, ref: innerRef, value: state, defaultValue: defaultValue, onChangeText: onChangeText, editable: editable, multiline: multiline, rows: rows, inputMode: inputMode, secureTextEntry: effectiveSecureTextEntry, placeholderTextColor: theme.colors.textDim, style: boxStyle, textStyle: textStyle }) }), hasSecureToggle && (_jsx(Pressable, { style: styles.nativePasswordToggle, onPress: () => setPasswordVisible((v) => !v), accessibilityLabel: passwordVisible ? "Hide password" : "Show password", accessibilityRole: "button", children: _jsx(Icon, { name: passwordVisible ? "eye-off" : "eye", size: spacing.iconSm + 4, color: "textDim" }) }))] }), !!(helperText || errorText) && (_jsx(StyledText, { selectable: false, style: [styles.helperText, hasError && styles.errorText], children: errorText || helperText }))] }));
|
|
272
282
|
}
|
|
273
283
|
const createStyles = (theme, variant, size) => StyleSheet.create({
|
|
274
284
|
nativeHost: {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mrmeg/expo-ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.10.1",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Reusable Expo and React Native UI primitives for MrMeg projects.",
|
|
6
6
|
"keywords": [
|
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
"dist",
|
|
34
34
|
"package.json",
|
|
35
35
|
"README.md",
|
|
36
|
+
"CHANGELOG.md",
|
|
36
37
|
"LLM_USAGE.md",
|
|
37
38
|
"llms.txt",
|
|
38
39
|
"llms-full.md"
|