@mrmeg/expo-ui 0.20.0 → 0.22.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.
Files changed (39) hide show
  1. package/CHANGELOG.md +202 -0
  2. package/LLM_USAGE.md +4 -3
  3. package/README.md +2 -2
  4. package/dist/components/Avatar.d.ts +19 -5
  5. package/dist/components/Avatar.js +84 -22
  6. package/dist/components/BottomSheet.js +1 -1
  7. package/dist/components/Carousel.d.ts +32 -6
  8. package/dist/components/Carousel.js +79 -17
  9. package/dist/components/DismissKeyboard.js +1 -1
  10. package/dist/components/KeyboardAvoidingView.d.ts +4 -2
  11. package/dist/components/KeyboardAvoidingView.js +5 -3
  12. package/dist/components/Label.d.ts +30 -10
  13. package/dist/components/Label.js +19 -10
  14. package/dist/components/Notification.js +7 -7
  15. package/dist/components/keyboardController.d.ts +46 -0
  16. package/dist/components/keyboardController.js +89 -0
  17. package/dist/components/keyboardController.native.d.ts +9 -0
  18. package/dist/components/keyboardController.native.js +9 -0
  19. package/dist/constants/colors.d.ts +34 -0
  20. package/dist/constants/colors.js +93 -3
  21. package/dist/constants/fonts.js +5 -4
  22. package/dist/hooks/useDimensions.d.ts +21 -9
  23. package/dist/hooks/useDimensions.js +29 -18
  24. package/dist/hooks/useResources.d.ts +10 -7
  25. package/dist/hooks/useResources.js +21 -24
  26. package/dist/hooks/useTheme.d.ts +18 -0
  27. package/dist/hooks/useTheme.js +45 -29
  28. package/dist/lib/themedStyles.d.ts +7 -7
  29. package/dist/lib/themedStyles.js +7 -7
  30. package/dist/state/SsrViewportContext.d.ts +9 -38
  31. package/dist/state/SsrViewportContext.js +9 -38
  32. package/dist/state/index.d.ts +1 -2
  33. package/dist/state/index.js +1 -2
  34. package/dist/state/themeStore.d.ts +9 -13
  35. package/dist/state/themeStore.js +19 -40
  36. package/llms-full.md +1 -1
  37. package/package.json +2 -2
  38. package/dist/state/ssrTheme.d.ts +0 -41
  39. package/dist/state/ssrTheme.js +0 -33
@@ -1,6 +1,6 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
2
  import React, { useCallback, useEffect, useRef, useState } from "react";
3
- import { Platform, ScrollView, StyleSheet, Text, View, } from "react-native";
3
+ import { Platform, Pressable, ScrollView, StyleSheet, Text, View, } from "react-native";
4
4
  import { useTheme } from "../hooks/useTheme.js";
5
5
  import { useDimensions } from "../hooks/useDimensions.js";
6
6
  import { spacing } from "../constants/spacing.js";
@@ -56,11 +56,14 @@ function clampIndex(index, count) {
56
56
  * resolved item width.
57
57
  *
58
58
  * Built on `ScrollView` — not a virtualized list — on purpose: every slide is
59
- * in the server-rendered tree, so web SSR ships the real content instead of an
60
- * empty scroller that only fills in after measurement. Snapping uses the
61
- * platform scroller (`snapToInterval` on native, CSS scroll-snap on web); no
59
+ * in the prerendered tree, so a web build's HTML shell ships the real content
60
+ * instead of an empty scroller that only fills in after measurement. Snapping
61
+ * uses the platform scroller (`snapToInterval` on native, CSS scroll-snap on web); no
62
62
  * animation library is involved.
63
63
  *
64
+ * Slides sized by a fractional `itemWidth` measure against the viewport until
65
+ * the first `onLayout` lands — see the `itemWidth` caveat on `CarouselProps`.
66
+ *
64
67
  * @example
65
68
  * ```tsx
66
69
  * <Carousel itemWidth={0.8} onIndexChange={setPage}>
@@ -78,9 +81,9 @@ export function Carousel({ children, itemWidth = 0.85, gap = spacing.md, content
78
81
  const items = React.Children.toArray(children);
79
82
  const count = items.length;
80
83
  // Width source of truth: the measured container after mount, seeded from the
81
- // viewport so the server and the client's first render agree. `useDimensions`
82
- // reads the SSR viewport context rather than `window`, which is what keeps
83
- // that seed hydration-safe (docs/ssr-hydration.md §4).
84
+ // viewport so a prerendered HTML shell and the client's first render agree.
85
+ // `useDimensions` seeds from a constant default rather than `window` on that
86
+ // first render, which is what keeps the seed hydration-safe.
84
87
  const { width: viewportWidth } = useDimensions();
85
88
  const [measuredWidth, setMeasuredWidth] = useState(null);
86
89
  const containerWidth = measuredWidth ?? viewportWidth;
@@ -88,7 +91,8 @@ export function Carousel({ children, itemWidth = 0.85, gap = spacing.md, content
88
91
  const interval = slideWidth + gap;
89
92
  const [activeIndex, setActiveIndex] = useState(() => clampIndex(initialIndex, count));
90
93
  // Mirrors activeIndex so the scroll handler can dedupe without re-reading
91
- // state: one settle emits many scroll events, onIndexChange must fire once.
94
+ // state: web's throttled ticks report the same page many times over, and
95
+ // onIndexChange must fire once per page.
92
96
  const activeIndexRef = useRef(activeIndex);
93
97
  const scrollRef = useRef(null);
94
98
  const hasScrolledToInitial = useRef(false);
@@ -99,9 +103,28 @@ export function Carousel({ children, itemWidth = 0.85, gap = spacing.md, content
99
103
  setActiveIndex(next);
100
104
  onIndexChange?.(next);
101
105
  }, [onIndexChange]);
106
+ const isWeb = Platform.OS === "web";
102
107
  const handleScroll = useCallback((event) => {
103
108
  commitIndex(getCarouselIndex(event.nativeEvent.contentOffset.x, interval, count));
104
109
  }, [commitIndex, count, interval]);
110
+ // Native fallback for a drag that releases with the content at rest: no
111
+ // momentum follows, so `onMomentumScrollEnd` never arrives and the settle
112
+ // would go unreported.
113
+ //
114
+ // Gated on velocity because an ungated drag-end handler is exactly the
115
+ // double-fire this replaced: a flick past a slide midpoint that snaps back
116
+ // reports the midpoint page on release and the original page on settle. A
117
+ // release that *will* glide reports a non-zero velocity (points/ms on iOS,
118
+ // density-scaled px/s on Android — either way, orders of magnitude above
119
+ // this epsilon), and momentum end is left to speak for it. A release at rest
120
+ // reports 0, and there `getCarouselIndex`'s round-to-nearest already agrees
121
+ // with where `snapToInterval` would land, so the two paths can't disagree.
122
+ const handleScrollEndDrag = useCallback((event) => {
123
+ const velocityX = event.nativeEvent.velocity?.x ?? 0;
124
+ if (Math.abs(velocityX) > 0.01)
125
+ return;
126
+ handleScroll(event);
127
+ }, [handleScroll]);
105
128
  const handleLayout = useCallback((event) => {
106
129
  const next = event.nativeEvent.layout.width;
107
130
  if (!(next > 0))
@@ -120,24 +143,54 @@ export function Carousel({ children, itemWidth = 0.85, gap = spacing.md, content
120
143
  return;
121
144
  scrollRef.current?.scrollTo({ x: targetIndex * interval, animated: false });
122
145
  }, [interval, measuredWidth, targetIndex]);
123
- const isWeb = Platform.OS === "web";
146
+ // Re-clamp when children shrink. Without this the status line reads "5 of 3"
147
+ // with no active dot until the next scroll, because activeIndex is only
148
+ // seeded once and the scroller — already past the new content width — emits
149
+ // nothing on its own. Scrolling back is what makes the offset agree with the
150
+ // reported page again.
151
+ useEffect(() => {
152
+ if (count <= 0 || activeIndexRef.current < count)
153
+ return;
154
+ const clamped = count - 1;
155
+ commitIndex(clamped);
156
+ if (interval > 0) {
157
+ scrollRef.current?.scrollTo({ x: clamped * interval, animated: false });
158
+ }
159
+ }, [commitIndex, count, interval]);
160
+ const handleDotPress = useCallback((index) => {
161
+ commitIndex(index);
162
+ if (interval > 0) {
163
+ scrollRef.current?.scrollTo({ x: index * interval, animated: true });
164
+ }
165
+ }, [commitIndex, interval]);
124
166
  const useCssSnap = isWeb && snap;
125
167
  const dotsVisible = showDots && count > 1;
126
168
  return (_jsxs(View, { style: [styles.container, styleOverride], onLayout: handleLayout, testID: testID, children: [_jsx(ScrollView, { ref: scrollRef, testID: `${testID}-scroll`, horizontal: true, showsHorizontalScrollIndicator: false,
127
169
  // Native snapping. react-native-web drops these (they aren't in its
128
170
  // forwarded-prop list), so web uses the CSS scroll-snap styles instead.
129
- snapToInterval: snap && interval > 0 ? interval : undefined, snapToAlignment: "start", decelerationRate: snap ? "fast" : "normal",
130
- // react-native-web only emits scroll ticks when the throttle is > 0 and
131
- // never emits momentum events — this is what keeps the dots live for
132
- // wheel/trackpad scrolling.
133
- scrollEventThrottle: 16, onScroll: handleScroll, onMomentumScrollEnd: handleScroll, style: useCssSnap
171
+ snapToInterval: snap && interval > 0 ? interval : undefined, snapToAlignment: "start", decelerationRate: snap ? "fast" : "normal", ...(isWeb
172
+ ? { scrollEventThrottle: 16, onScroll: handleScroll }
173
+ : { onMomentumScrollEnd: handleScroll, onScrollEndDrag: handleScrollEndDrag }), style: useCssSnap
134
174
  ? [webStyles.scroller, { scrollPaddingLeft: contentPadding }]
135
- : undefined, contentContainerStyle: [{ paddingHorizontal: contentPadding, gap }, contentContainerStyle], children: items.map((child, index) => (_jsx(View, { testID: `${testID}-item-${index}`, style: [slideWidth > 0 && { width: slideWidth }, useCssSnap && webStyles.item], children: child }, React.isValidElement(child) && child.key != null ? child.key : index))) }), count > 1 && (_jsx(View, { testID: `${testID}-status`, style: styles.status, "aria-live": "polite", children: _jsx(Text, { style: styles.statusText, children: `${activeIndex + 1} of ${count}` }) })), dotsVisible && (_jsx(View, { testID: `${testID}-dots`, style: styles.dots, accessibilityRole: "tablist", children: items.map((_, index) => (_jsx(View, { testID: `${testID}-dot-${index}`, accessibilityRole: "tab", accessibilityState: { selected: index === activeIndex }, accessibilityLabel: `Slide ${index + 1} of ${count}`, style: [styles.dot, index === activeIndex ? styles.dotActive : styles.dotInactive] }, index))) }))] }));
175
+ : undefined, contentContainerStyle: [{ paddingHorizontal: contentPadding, gap }, contentContainerStyle], children: items.map((child, index) => (_jsx(View, { testID: `${testID}-item-${index}`, style: [slideWidth > 0 && { width: slideWidth }, useCssSnap && webStyles.item], children: child }, React.isValidElement(child) && child.key != null ? child.key : index))) }), count > 1 && (_jsx(View, { testID: `${testID}-status`, style: styles.status, "aria-live": "polite", children: _jsx(Text, { style: styles.statusText, children: `${activeIndex + 1} of ${count}` }) })), dotsVisible && (_jsx(View, { testID: `${testID}-dots`, style: styles.dots, accessibilityRole: "tablist", children: items.map((_, index) => (_jsx(Pressable, { testID: `${testID}-dot-${index}`, accessibilityRole: "tab", accessibilityState: { selected: index === activeIndex }, "aria-selected": index === activeIndex, accessibilityLabel: `Slide ${index + 1} of ${count}`, hitSlop: DOT_HIT_SLOP, onPress: () => handleDotPress(index), style: styles.dotButton, children: _jsx(View, { testID: `${testID}-dot-${index}-indicator`, style: [styles.dot, index === activeIndex ? styles.dotActive : styles.dotInactive] }) }, index))) }))] }));
136
176
  }
137
177
  // ============================================================================
138
178
  // Styles
139
179
  // ============================================================================
140
180
  const DOT_SIZE = 8;
181
+ // Press target around each 8px dot. The square is what separates the dots
182
+ // visually (the row itself has no gap, so neighboring targets are flush — no
183
+ // dead strip between them), and `DOT_HIT_SLOP` extends it vertically to
184
+ // `spacing.touchTarget` (24 + 2 x 10 = 44). The slop is vertical-only on
185
+ // purpose: horizontal slop would overlap the neighboring dot's target and make
186
+ // which dot a tap lands on ambiguous.
187
+ const DOT_HIT_SIZE = 24;
188
+ const DOT_HIT_SLOP = {
189
+ top: (spacing.touchTarget - DOT_HIT_SIZE) / 2,
190
+ bottom: (spacing.touchTarget - DOT_HIT_SIZE) / 2,
191
+ left: 0,
192
+ right: 0,
193
+ };
141
194
  // Web-only CSS scroll-snap. `scrollPaddingLeft` is applied inline at the call
142
195
  // site (it tracks the contentPadding prop) so the leading inset survives the
143
196
  // browser snapping slide 0 to the scrollport start — which also makes page `i`
@@ -166,8 +219,17 @@ const createStyles = (theme) => StyleSheet.create({
166
219
  flexDirection: "row",
167
220
  alignItems: "center",
168
221
  justifyContent: "center",
169
- gap: spacing.xs,
170
- marginTop: spacing.md,
222
+ // No `gap`: the DOT_HIT_SIZE squares carry the visual spacing, and a gap
223
+ // would open an unpressable strip between adjacent targets. marginTop
224
+ // absorbs the wrapper's own vertical padding so the row sits where the
225
+ // pre-Pressable dots did.
226
+ marginTop: spacing.md - (DOT_HIT_SIZE - DOT_SIZE) / 2,
227
+ },
228
+ dotButton: {
229
+ width: DOT_HIT_SIZE,
230
+ height: DOT_HIT_SIZE,
231
+ alignItems: "center",
232
+ justifyContent: "center",
171
233
  },
172
234
  dot: {
173
235
  width: DOT_SIZE,
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import React from "react";
3
3
  import { Platform, ScrollView, View, } from "react-native";
4
- import { KeyboardController, useKeyboardContext, useKeyboardState, } from "react-native-keyboard-controller";
4
+ import { KeyboardController, useKeyboardContext, useKeyboardState, } from "./keyboardController";
5
5
  import { KeyboardAvoidingView, useKeyboardAvoidance } from "./KeyboardAvoidingView.js";
6
6
  import { isTouchInsideKeyboardFocusedInput } from "./keyboardFocusRegistry.js";
7
7
  /**
@@ -13,8 +13,10 @@ export declare function useKeyboardAvoidance(): boolean;
13
13
  /**
14
14
  * Package-level keyboard avoiding wrapper.
15
15
  *
16
- * Native uses `react-native-keyboard-controller` so screens can avoid the soft
17
- * keyboard with `automaticOffset`; web renders a plain `View`.
16
+ * Native uses `react-native-keyboard-controller` (through the platform-split
17
+ * `./keyboardController` module, which keeps the package out of the web bundle)
18
+ * so screens can avoid the soft keyboard with `automaticOffset`; web renders a
19
+ * plain `View`.
18
20
  */
19
21
  export declare function KeyboardAvoidingView({ children, style, behavior, automaticOffset, contentContainerStyle, keyboardVerticalOffset, ...props }: KeyboardAvoidingViewProps): React.JSX.Element;
20
22
  export {};
@@ -1,7 +1,7 @@
1
1
  import { jsx as _jsx } from "react/jsx-runtime";
2
2
  import { createContext, useContext } from "react";
3
3
  import { Platform, View, } from "react-native";
4
- import { KeyboardAvoidingView as NativeKeyboardAvoidingView, } from "react-native-keyboard-controller";
4
+ import { NativeKeyboardAvoidingView } from "./keyboardController";
5
5
  const KeyboardAvoidanceContext = createContext(false);
6
6
  export function useKeyboardAvoidance() {
7
7
  return useContext(KeyboardAvoidanceContext);
@@ -9,8 +9,10 @@ export function useKeyboardAvoidance() {
9
9
  /**
10
10
  * Package-level keyboard avoiding wrapper.
11
11
  *
12
- * Native uses `react-native-keyboard-controller` so screens can avoid the soft
13
- * keyboard with `automaticOffset`; web renders a plain `View`.
12
+ * Native uses `react-native-keyboard-controller` (through the platform-split
13
+ * `./keyboardController` module, which keeps the package out of the web bundle)
14
+ * so screens can avoid the soft keyboard with `automaticOffset`; web renders a
15
+ * plain `View`.
14
16
  */
15
17
  export function KeyboardAvoidingView({ children, style, behavior = Platform.OS === "ios" ? "padding" : "height", automaticOffset = true, contentContainerStyle, keyboardVerticalOffset, ...props }) {
16
18
  if (Platform.OS === "web") {
@@ -5,9 +5,20 @@ export interface LabelProps {
5
5
  */
6
6
  children: string;
7
7
  /**
8
- * Native ID to associate with a form control
8
+ * The label's OWN id. On web react-native-web maps it to `id`; on native it
9
+ * is the `nativeID` a control's `accessibilityLabelledBy` can point at.
10
+ *
11
+ * Must differ from the paired input's id — see `htmlFor`.
9
12
  */
10
13
  nativeID?: string;
14
+ /**
15
+ * WEB ONLY — the id of the input this label describes (the input's
16
+ * `nativeID`). Passing it makes `@rn-primitives/label` render a real
17
+ * `<label for="…">`, which is what associates the two and makes clicking the
18
+ * label focus the input. No-op on native, where association goes the other
19
+ * way (the control points at the label's `nativeID`).
20
+ */
21
+ htmlFor?: string;
11
22
  /**
12
23
  * Whether the field is required (shows asterisk)
13
24
  */
@@ -37,25 +48,34 @@ export interface LabelProps {
37
48
  /**
38
49
  * Label component for form fields using @rn-primitives/label
39
50
  *
40
- * Provides accessible labeling for form controls with automatic
41
- * association via nativeID.
51
+ * Accessible labeling for form controls. Association needs **two distinct
52
+ * ids**: `nativeID` is the label's own id, `htmlFor` is the input's id. Giving
53
+ * both the same value renders two elements with one id on web and associates
54
+ * nothing.
42
55
  *
43
56
  * Usage:
44
57
  * ```tsx
45
- * // Basic label
46
- * <Label nativeID="email-input">Email</Label>
58
+ * // Paired with an input. `htmlFor` (web) makes this a real <label for>, so
59
+ * // clicking it focuses the input; `nativeID` names the label itself, which is
60
+ * // what a control's `accessibilityLabelledBy` points at on native.
61
+ * <Label nativeID="email-label" htmlFor="email-input">Email</Label>
47
62
  * <TextInput nativeID="email-input" />
48
63
  *
49
64
  * // Required field
50
- * <Label nativeID="password" required>Password</Label>
65
+ * <Label nativeID="password-label" htmlFor="password-input" required>Password</Label>
66
+ * <TextInput nativeID="password-input" secureTextEntry />
51
67
  *
52
68
  * // With error state
53
- * <Label nativeID="username" error>Username</Label>
69
+ * <Label nativeID="username-label" htmlFor="username-input" error>Username</Label>
70
+ *
71
+ * // Standalone caption with no control to associate — omit htmlFor
72
+ * <Label>Filters</Label>
54
73
  *
55
- * // With press handler to focus input
56
- * <Label nativeID="search" onPress={() => inputRef.current?.focus()}>
74
+ * // With press handler to focus input (needed on native, where htmlFor is a
75
+ * // no-op and pressing the label does nothing on its own)
76
+ * <Label nativeID="search-label" htmlFor="search-input" onPress={() => inputRef.current?.focus()}>
57
77
  * Search
58
78
  * </Label>
59
79
  * ```
60
80
  */
61
- export declare function Label({ children, nativeID, required, size, error, disabled, style, onPress, }: LabelProps): import("react").JSX.Element;
81
+ export declare function Label({ children, nativeID, htmlFor, required, size, error, disabled, style, onPress, }: LabelProps): import("react").JSX.Element;
@@ -14,28 +14,37 @@ const SIZE_CONFIGS = {
14
14
  /**
15
15
  * Label component for form fields using @rn-primitives/label
16
16
  *
17
- * Provides accessible labeling for form controls with automatic
18
- * association via nativeID.
17
+ * Accessible labeling for form controls. Association needs **two distinct
18
+ * ids**: `nativeID` is the label's own id, `htmlFor` is the input's id. Giving
19
+ * both the same value renders two elements with one id on web and associates
20
+ * nothing.
19
21
  *
20
22
  * Usage:
21
23
  * ```tsx
22
- * // Basic label
23
- * <Label nativeID="email-input">Email</Label>
24
+ * // Paired with an input. `htmlFor` (web) makes this a real <label for>, so
25
+ * // clicking it focuses the input; `nativeID` names the label itself, which is
26
+ * // what a control's `accessibilityLabelledBy` points at on native.
27
+ * <Label nativeID="email-label" htmlFor="email-input">Email</Label>
24
28
  * <TextInput nativeID="email-input" />
25
29
  *
26
30
  * // Required field
27
- * <Label nativeID="password" required>Password</Label>
31
+ * <Label nativeID="password-label" htmlFor="password-input" required>Password</Label>
32
+ * <TextInput nativeID="password-input" secureTextEntry />
28
33
  *
29
34
  * // With error state
30
- * <Label nativeID="username" error>Username</Label>
35
+ * <Label nativeID="username-label" htmlFor="username-input" error>Username</Label>
31
36
  *
32
- * // With press handler to focus input
33
- * <Label nativeID="search" onPress={() => inputRef.current?.focus()}>
37
+ * // Standalone caption with no control to associate — omit htmlFor
38
+ * <Label>Filters</Label>
39
+ *
40
+ * // With press handler to focus input (needed on native, where htmlFor is a
41
+ * // no-op and pressing the label does nothing on its own)
42
+ * <Label nativeID="search-label" htmlFor="search-input" onPress={() => inputRef.current?.focus()}>
34
43
  * Search
35
44
  * </Label>
36
45
  * ```
37
46
  */
38
- export function Label({ children, nativeID, required, size = "md", error, disabled, style, onPress, }) {
47
+ export function Label({ children, nativeID, htmlFor, required, size = "md", error, disabled, style, onPress, }) {
39
48
  const { theme } = useTheme();
40
49
  const styles = themedStyles(theme);
41
50
  const sizeConfig = SIZE_CONFIGS[size];
@@ -53,7 +62,7 @@ export function Label({ children, nativeID, required, size = "md", error, disabl
53
62
  return (_jsx(LabelPrimitive.Root, { nativeID: nativeID, onPress: onPress, style: {
54
63
  ...styles.root,
55
64
  ...(Platform.OS === "web" && onPress && { cursor: "pointer" }),
56
- }, children: _jsxs(LabelPrimitive.Text, { style: textStyle, children: [children, required && _jsx(StyledText, { selectable: false, fontWeight: "bold", style: styles.required, children: " *" })] }) }));
65
+ }, children: _jsxs(LabelPrimitive.Text, { htmlFor: htmlFor, style: textStyle, children: [children, required && _jsx(StyledText, { selectable: false, fontWeight: "bold", style: styles.required, children: " *" })] }) }));
57
66
  }
58
67
  const createStyles = (theme) => StyleSheet.create({
59
68
  root: {
@@ -3,7 +3,7 @@ import { useCallback, use, useEffect, useEffectEvent, useRef } from "react";
3
3
  import { Animated, Easing, StyleSheet, View, ActivityIndicator, Pressable, Platform } from "react-native";
4
4
  import { SafeAreaInsetsContext } from "react-native-safe-area-context";
5
5
  import { Icon } from "./Icon.js";
6
- import { useTheme } from "../hooks/useTheme.js";
6
+ import { useTheme, withAlpha } from "../hooks/useTheme.js";
7
7
  import { useReducedMotion } from "../hooks/useReduceMotion.js";
8
8
  import { spacing } from "../constants/spacing.js";
9
9
  import { durations } from "../constants/motion.js";
@@ -158,26 +158,26 @@ export const Notification = () => {
158
158
  return {
159
159
  icon: "alert-circle",
160
160
  color: theme.colors.destructive,
161
- bgColor: theme.colors.destructive + "15",
161
+ bgColor: withAlpha(theme.colors.destructive, 0.08),
162
162
  };
163
163
  case "success":
164
164
  return {
165
165
  icon: "check-circle",
166
166
  color: theme.colors.success,
167
- bgColor: theme.colors.success + "15",
167
+ bgColor: withAlpha(theme.colors.success, 0.08),
168
168
  };
169
169
  case "warning":
170
170
  return {
171
171
  icon: "alert-triangle",
172
172
  color: theme.colors.warning,
173
- bgColor: theme.colors.warning + "15",
173
+ bgColor: withAlpha(theme.colors.warning, 0.08),
174
174
  };
175
175
  case "info":
176
176
  default:
177
177
  return {
178
178
  icon: "info",
179
179
  color: theme.colors.accent,
180
- bgColor: theme.colors.accent + "15",
180
+ bgColor: withAlpha(theme.colors.accent, 0.08),
181
181
  };
182
182
  }
183
183
  };
@@ -223,8 +223,8 @@ export const Notification = () => {
223
223
  ], children: [_jsx(View, { style: [styles.iconBadge, { backgroundColor: iconBgColor }], children: alert?.loading ? (_jsx(ActivityIndicator, { size: "small", color: iconColor })) : (_jsx(Icon, { name: icon, size: 18, color: iconColor })) }), _jsxs(View, { style: styles.alertContent, children: [!!title && (_jsx(StyledText, { selectable: false, size: "base", fontWeight: "semibold", style: { color: theme.colors.foreground }, numberOfLines: 1, children: title })), hasMessage && (_jsx(StyledText, { selectable: false, size: "sm", style: { color: theme.colors.mutedForeground }, numberOfLines: 2, children: message }))] }), action && (_jsx(Pressable, { style: ({ pressed }) => [
224
224
  styles.actionButton,
225
225
  {
226
- borderColor: theme.colors.primary + "30",
227
- backgroundColor: theme.colors.primary + "10",
226
+ borderColor: withAlpha(theme.colors.primary, 0.19),
227
+ backgroundColor: withAlpha(theme.colors.primary, 0.06),
228
228
  },
229
229
  pressed && styles.actionButtonPressed,
230
230
  ], hitSlop: spacing.xs, onPress: handleActionPress, accessibilityLabel: action.label, accessibilityRole: "button", children: _jsx(StyledText, { selectable: false, size: "sm", fontWeight: "semibold", style: { color: theme.colors.primary }, numberOfLines: 1, children: action.label }) })), _jsx(Pressable, { style: styles.closeButton, hitSlop: spacing.sm, onPress: animateOut, accessibilityLabel: "Dismiss notification", accessibilityRole: "button", children: _jsx(Icon, { name: "x", size: 16, color: theme.colors.mutedForeground }) })] }) }));
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Keyboard-controller indirection — web variant (inert stubs).
3
+ *
4
+ * `react-native-keyboard-controller` drags a large native animation runtime
5
+ * into every bundle that imports it, and none of that code can run on web.
6
+ * Components therefore import the keyboard API from this module instead of the
7
+ * package: Metro resolves `keyboardController.native.ts` (the real re-exports)
8
+ * on iOS/Android and this file on web, so the package never reaches the web
9
+ * bundle.
10
+ *
11
+ * Both variants are typed against the package's own types — type-only imports
12
+ * are erased at compile time and bundle nothing — so the two files stay
13
+ * interchangeable to the type checker. TypeScript always resolves this `.ts`
14
+ * variant for consumers, which is why the stubs must keep the full native
15
+ * signatures.
16
+ *
17
+ * Every stub here is a plain function that calls no hooks, so the `use*`
18
+ * exports are safe from any call site (rules of hooks can't be violated by a
19
+ * function that has no hook calls of its own).
20
+ */
21
+ import React from "react";
22
+ import { View } from "react-native";
23
+ import type { IKeyboardState, KeyboardAnimationContext, KeyboardAvoidingViewProps, KeyboardControllerModule } from "react-native-keyboard-controller";
24
+ /** No-op imperative module: there is no IME to dismiss or preload on web. */
25
+ export declare const KeyboardController: KeyboardControllerModule;
26
+ /**
27
+ * Applies `selector` to a permanently "keyboard hidden" state.
28
+ *
29
+ * @param selector - Picks the fields a caller needs from the keyboard state.
30
+ * @returns The selected value, or the whole state when no selector is given.
31
+ */
32
+ export declare function useKeyboardState<T = IKeyboardState>(selector?: (state: IKeyboardState) => T): T;
33
+ /**
34
+ * @returns A stable, empty keyboard context (no focused input layout on web).
35
+ */
36
+ export declare function useKeyboardContext(): KeyboardAnimationContext;
37
+ type NativeKeyboardAvoidingViewProps = KeyboardAvoidingViewProps & {
38
+ children?: React.ReactNode;
39
+ };
40
+ /**
41
+ * Plain `View` passthrough. Keyboard-avoidance props are accepted (so callers
42
+ * stay identical across platforms) and dropped, since the browser handles
43
+ * viewport resizing itself.
44
+ */
45
+ export declare function NativeKeyboardAvoidingView({ behavior, contentContainerStyle, keyboardVerticalOffset, automaticOffset, enabled, children, ...viewProps }: NativeKeyboardAvoidingViewProps): React.CElement<import("react-native").ViewProps, View>;
46
+ export {};
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Keyboard-controller indirection — web variant (inert stubs).
3
+ *
4
+ * `react-native-keyboard-controller` drags a large native animation runtime
5
+ * into every bundle that imports it, and none of that code can run on web.
6
+ * Components therefore import the keyboard API from this module instead of the
7
+ * package: Metro resolves `keyboardController.native.ts` (the real re-exports)
8
+ * on iOS/Android and this file on web, so the package never reaches the web
9
+ * bundle.
10
+ *
11
+ * Both variants are typed against the package's own types — type-only imports
12
+ * are erased at compile time and bundle nothing — so the two files stay
13
+ * interchangeable to the type checker. TypeScript always resolves this `.ts`
14
+ * variant for consumers, which is why the stubs must keep the full native
15
+ * signatures.
16
+ *
17
+ * Every stub here is a plain function that calls no hooks, so the `use*`
18
+ * exports are safe from any call site (rules of hooks can't be violated by a
19
+ * function that has no hook calls of its own).
20
+ */
21
+ import React from "react";
22
+ import { View } from "react-native";
23
+ /** There is no software keyboard to observe on web, so state is constant. */
24
+ const HIDDEN_KEYBOARD_STATE = {
25
+ isVisible: false,
26
+ height: 0,
27
+ duration: 0,
28
+ timestamp: 0,
29
+ target: -1,
30
+ type: "default",
31
+ appearance: "light",
32
+ };
33
+ /**
34
+ * Inert keyboard context.
35
+ *
36
+ * The real context exposes animation values that only exist alongside the
37
+ * native module, so this is a minimal stand-in cast to the package's type: the
38
+ * only field consumers read on web-capable paths is `layout.value`. It is a
39
+ * module-level singleton because `DismissKeyboard` puts the context object in a
40
+ * `useCallback` dependency list — a fresh object per call would invalidate it
41
+ * on every render.
42
+ */
43
+ const INERT_KEYBOARD_CONTEXT = {
44
+ enabled: false,
45
+ layout: { value: null },
46
+ update: () => Promise.resolve(),
47
+ setKeyboardHandlers: () => () => undefined,
48
+ setInputHandlers: () => () => undefined,
49
+ setEnabled: () => undefined,
50
+ };
51
+ /** No-op imperative module: there is no IME to dismiss or preload on web. */
52
+ export const KeyboardController = {
53
+ setDefaultMode: () => undefined,
54
+ setInputMode: () => undefined,
55
+ preload: () => undefined,
56
+ dismiss: () => Promise.resolve(),
57
+ setFocusTo: () => undefined,
58
+ isVisible: () => false,
59
+ state: () => HIDDEN_KEYBOARD_STATE,
60
+ };
61
+ /**
62
+ * Applies `selector` to a permanently "keyboard hidden" state.
63
+ *
64
+ * @param selector - Picks the fields a caller needs from the keyboard state.
65
+ * @returns The selected value, or the whole state when no selector is given.
66
+ */
67
+ export function useKeyboardState(selector) {
68
+ return selector ? selector(HIDDEN_KEYBOARD_STATE) : HIDDEN_KEYBOARD_STATE;
69
+ }
70
+ /**
71
+ * @returns A stable, empty keyboard context (no focused input layout on web).
72
+ */
73
+ export function useKeyboardContext() {
74
+ return INERT_KEYBOARD_CONTEXT;
75
+ }
76
+ /**
77
+ * Plain `View` passthrough. Keyboard-avoidance props are accepted (so callers
78
+ * stay identical across platforms) and dropped, since the browser handles
79
+ * viewport resizing itself.
80
+ */
81
+ export function NativeKeyboardAvoidingView({ behavior, contentContainerStyle, keyboardVerticalOffset, automaticOffset, enabled, children, ...viewProps }) {
82
+ // Dropped rather than forwarded so they never reach the DOM node.
83
+ void behavior;
84
+ void contentContainerStyle;
85
+ void keyboardVerticalOffset;
86
+ void automaticOffset;
87
+ void enabled;
88
+ return React.createElement(View, viewProps, children);
89
+ }
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Keyboard-controller indirection — native variant.
3
+ *
4
+ * Re-exports the real `react-native-keyboard-controller` API under the names the
5
+ * package's components import. See `keyboardController.ts` (the web variant)
6
+ * for why the components go through this module instead of importing the
7
+ * package directly.
8
+ */
9
+ export { KeyboardController, KeyboardAvoidingView as NativeKeyboardAvoidingView, useKeyboardContext, useKeyboardState, } from "react-native-keyboard-controller";
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Keyboard-controller indirection — native variant.
3
+ *
4
+ * Re-exports the real `react-native-keyboard-controller` API under the names the
5
+ * package's components import. See `keyboardController.ts` (the web variant)
6
+ * for why the components go through this module instead of importing the
7
+ * package directly.
8
+ */
9
+ export { KeyboardController, KeyboardAvoidingView as NativeKeyboardAvoidingView, useKeyboardContext, useKeyboardState, } from "react-native-keyboard-controller";
@@ -86,5 +86,39 @@ export interface Colors {
86
86
  light: Theme;
87
87
  dark: Theme;
88
88
  }
89
+ /**
90
+ * Raw hex/rgba theme colors, always literal strings on every platform. Use
91
+ * these for sinks that cannot take CSS `var()` — e.g. `<meta name="theme-color">`,
92
+ * color parsing/math — and for generating the CSS variable definitions below.
93
+ */
94
+ export declare const rawThemeColors: {
95
+ light: ThemeColors;
96
+ dark: ThemeColors;
97
+ };
98
+ /**
99
+ * Resolves a theme color value to a literal color string. On web, theme
100
+ * colors are `var(--c-*)` references; this maps one back to the given
101
+ * scheme's raw hex/rgba. Literal inputs (including app override colors)
102
+ * pass through unchanged.
103
+ */
104
+ export declare function resolveRawColor(color: string, scheme: "light" | "dark"): string;
105
+ /**
106
+ * CSS custom-property definitions for every semantic theme color, for
107
+ * embedding in a web app's `+html.tsx` global `<style>`. Each token gets
108
+ * `--c-<kebab-token>` (the color value) and `--c-<kebab-token>-rgb` (a
109
+ * comma-separated RGB triplet, alpha stripped — `withAlpha` builds
110
+ * `rgba(var(--c-x-rgb), a)` from it).
111
+ *
112
+ * Light values are defined on `:root`; dark values apply under
113
+ * `html[data-theme="dark"]`, with a `prefers-color-scheme` fallback for the
114
+ * paint before any theme script stamps `data-theme`.
115
+ *
116
+ * Apps that override brand colors can pass those overrides so the emitted
117
+ * CSS matches their theme.
118
+ */
119
+ export declare function getThemeCssVariables(overrides?: {
120
+ light?: Partial<ThemeColors>;
121
+ dark?: Partial<ThemeColors>;
122
+ }): string;
89
123
  export declare const colors: Colors;
90
124
  export { palette };