@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.
- package/CHANGELOG.md +202 -0
- package/LLM_USAGE.md +4 -3
- package/README.md +2 -2
- package/dist/components/Avatar.d.ts +19 -5
- package/dist/components/Avatar.js +84 -22
- package/dist/components/BottomSheet.js +1 -1
- package/dist/components/Carousel.d.ts +32 -6
- package/dist/components/Carousel.js +79 -17
- package/dist/components/DismissKeyboard.js +1 -1
- package/dist/components/KeyboardAvoidingView.d.ts +4 -2
- package/dist/components/KeyboardAvoidingView.js +5 -3
- package/dist/components/Label.d.ts +30 -10
- package/dist/components/Label.js +19 -10
- package/dist/components/Notification.js +7 -7
- package/dist/components/keyboardController.d.ts +46 -0
- package/dist/components/keyboardController.js +89 -0
- package/dist/components/keyboardController.native.d.ts +9 -0
- package/dist/components/keyboardController.native.js +9 -0
- package/dist/constants/colors.d.ts +34 -0
- package/dist/constants/colors.js +93 -3
- package/dist/constants/fonts.js +5 -4
- package/dist/hooks/useDimensions.d.ts +21 -9
- package/dist/hooks/useDimensions.js +29 -18
- package/dist/hooks/useResources.d.ts +10 -7
- package/dist/hooks/useResources.js +21 -24
- package/dist/hooks/useTheme.d.ts +18 -0
- package/dist/hooks/useTheme.js +45 -29
- package/dist/lib/themedStyles.d.ts +7 -7
- package/dist/lib/themedStyles.js +7 -7
- package/dist/state/SsrViewportContext.d.ts +9 -38
- package/dist/state/SsrViewportContext.js +9 -38
- package/dist/state/index.d.ts +1 -2
- package/dist/state/index.js +1 -2
- package/dist/state/themeStore.d.ts +9 -13
- package/dist/state/themeStore.js +19 -40
- package/llms-full.md +1 -1
- package/package.json +2 -2
- package/dist/state/ssrTheme.d.ts +0 -41
- 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
|
|
60
|
-
* empty scroller that only fills in after measurement. Snapping
|
|
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
|
|
82
|
-
//
|
|
83
|
-
//
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
131
|
-
|
|
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(
|
|
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
|
|
170
|
-
|
|
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 "
|
|
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`
|
|
17
|
-
*
|
|
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 {
|
|
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`
|
|
13
|
-
*
|
|
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
|
-
*
|
|
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
|
-
*
|
|
41
|
-
*
|
|
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
|
-
* //
|
|
46
|
-
*
|
|
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
|
-
*
|
|
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;
|
package/dist/components/Label.js
CHANGED
|
@@ -14,28 +14,37 @@ const SIZE_CONFIGS = {
|
|
|
14
14
|
/**
|
|
15
15
|
* Label component for form fields using @rn-primitives/label
|
|
16
16
|
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
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
|
-
* //
|
|
23
|
-
*
|
|
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
|
-
* //
|
|
33
|
-
* <Label
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
227
|
-
backgroundColor: theme.colors.primary
|
|
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 };
|