@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 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
- declare function DrawerContent({ swipeEnabled, swipeThreshold, velocityThreshold, style: styleOverride, children, ...props }: DrawerContentProps): React.JSX.Element | null;
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
- function DrawerContent({ swipeEnabled = true, swipeThreshold = 0.3, velocityThreshold = 500, style: styleOverride, children, ...props }) {
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
- * Only includes colors that actually exist in the theme
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 = "primary" | "primaryForeground" | "secondary" | "muted" | "destructive" | "success" | "warning" | "text" | "textDim";
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 */
@@ -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
- const THEME_COLOR_KEYS = [
5
- "primary", "primaryForeground", "secondary", "muted",
6
- "destructive", "success", "warning", "text", "textDim",
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 (THEME_COLOR_KEYS.includes(color)) {
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
- // Map variant/size to @expo/ui's UniversalStyle (translated to SwiftUI /
247
- // Compose modifiers natively).
248
- const boxStyle = {
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 ? styles.nativeRow : undefined, 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 }))] }));
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.9.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"