@mrmeg/expo-ui 0.25.0 → 0.26.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 +118 -0
- package/LLM_USAGE.md +13 -1
- package/README.md +89 -18
- package/dist/components/Avatar.d.ts +1 -1
- package/dist/components/BottomSheet.d.ts +1 -1
- package/dist/components/BottomSheet.js +28 -51
- package/dist/components/DismissKeyboard.d.ts +8 -6
- package/dist/components/DismissKeyboard.js +9 -7
- package/dist/components/DropdownMenu.js +1 -1
- package/dist/components/Icon.d.ts +24 -22
- package/dist/components/Icon.js +26 -3
- package/dist/components/Notification.js +3 -3
- package/dist/components/Popover.js +1 -1
- package/dist/components/Select.js +1 -1
- package/dist/components/StatCard.d.ts +1 -1
- package/dist/components/TextInput.d.ts +1 -1
- package/dist/components/TextInput.js +55 -35
- package/dist/components/Tooltip.js +1 -1
- package/dist/components/iconRegistry.generated.d.ts +148 -0
- package/dist/components/iconRegistry.generated.js +296 -0
- package/dist/components/keyboardDismiss.js +126 -55
- package/dist/components/keyboardFocusRegistry.d.ts +14 -4
- package/dist/components/keyboardFocusRegistry.js +13 -5
- package/dist/components/nativeTextField.android.d.ts +3 -0
- package/dist/components/nativeTextField.android.js +226 -0
- package/dist/components/nativeTextField.d.ts +14 -0
- package/dist/components/nativeTextField.js +14 -0
- package/dist/constants/spacing.d.ts +2 -2
- package/dist/constants/spacing.js +2 -2
- package/dist/design-system.json +152 -3
- package/dist/hooks/useResources.d.ts +5 -18
- package/dist/hooks/useResources.js +5 -43
- package/dist/state/themeStore.d.ts +1 -1
- package/llms-full.md +3 -3
- package/package.json +4 -3
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
/**
|
|
3
|
+
* Native text field indirection — Android variant.
|
|
4
|
+
*
|
|
5
|
+
* A package-owned port of `@expo/ui`'s universal Android `TextInput`
|
|
6
|
+
* (`@expo/ui` 58.0.2, `src/universal/TextInput/index.android.tsx`, MIT
|
|
7
|
+
* licensed, Copyright (c) 650 Industries, Inc.), built on the public
|
|
8
|
+
* `@expo/ui/jetpack-compose` `BasicTextField`. It keeps the universal field's
|
|
9
|
+
* prop surface (`NativeTextFieldProps` *is* `@expo/ui`'s `TextInputProps`), its
|
|
10
|
+
* `useNativeState` fallback buffer, its ref handle, its slot layout and its
|
|
11
|
+
* modifier order, and differs in exactly two places:
|
|
12
|
+
*
|
|
13
|
+
* 1. **Secure fields declare a password keyboard.** The universal field maps
|
|
14
|
+
* `secureTextEntry` to `visualTransformation="password"` only, so the IME
|
|
15
|
+
* still sees a plain text field: Gboard shows its suggestion strip, learns
|
|
16
|
+
* the password, and `dumpsys input_method` reports `TYPE_CLASS_TEXT` with no
|
|
17
|
+
* password variation. Here a secure field sets `keyboardType` to
|
|
18
|
+
* `'password'` (`textPassword`), or `'numberPassword'` when the requested
|
|
19
|
+
* keyboard is numeric, and defaults `autoCorrectEnabled` to `false` and
|
|
20
|
+
* `capitalization` to `'none'`. Once a mounted field has been secure the
|
|
21
|
+
* password keyboard options stay constant for its lifetime, so the eye
|
|
22
|
+
* toggle (`secureTextEntry` true -> false on the same element) only drops the
|
|
23
|
+
* masking transformation: Compose treats `KeyboardType.Password` with
|
|
24
|
+
* `VisualTransformation.None` as the canonical revealed-password state, and
|
|
25
|
+
* an unchanged `KeyboardOptions` avoids an IME restart, preserving focus,
|
|
26
|
+
* selection and the input connection.
|
|
27
|
+
* 2. **Box style maps only the padding keys the package sends.** The universal
|
|
28
|
+
* field's `transformToModifiers` is not a public export. `TextInput.tsx`
|
|
29
|
+
* hands the field `{ backgroundColor: "transparent", paddingHorizontal,
|
|
30
|
+
* paddingVertical }` and paints the surface (fill, border, radius) on its RN
|
|
31
|
+
* wrapper, so `styleToModifiers` below handles the padding cascade and
|
|
32
|
+
* ignores `backgroundColor`.
|
|
33
|
+
*
|
|
34
|
+
* Only `@expo/ui/jetpack-compose` and `@expo/ui/jetpack-compose/modifiers` are
|
|
35
|
+
* imported, both in the peer's export map since SDK 56.
|
|
36
|
+
*/
|
|
37
|
+
import { useImperativeHandle, useRef } from "react";
|
|
38
|
+
import { BasicTextField, Box, Text, useNativeState, } from "@expo/ui/jetpack-compose";
|
|
39
|
+
import { fillMaxWidth, onSizeChanged, padding, paddingAll, semantics, testID as testIDModifier, } from "@expo/ui/jetpack-compose/modifiers";
|
|
40
|
+
// region Prop mapping (ported verbatim from @expo/ui's TextInput/utils.ts)
|
|
41
|
+
function inputModeToKeyboardType(inputMode) {
|
|
42
|
+
if (!inputMode || inputMode === "none")
|
|
43
|
+
return undefined;
|
|
44
|
+
switch (inputMode) {
|
|
45
|
+
case "text":
|
|
46
|
+
return "default";
|
|
47
|
+
case "decimal":
|
|
48
|
+
return "decimal-pad";
|
|
49
|
+
case "tel":
|
|
50
|
+
return "phone-pad";
|
|
51
|
+
case "search":
|
|
52
|
+
return "web-search";
|
|
53
|
+
case "email":
|
|
54
|
+
return "email-address";
|
|
55
|
+
default:
|
|
56
|
+
return inputMode;
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function enterKeyHintToReturnKeyType(hint) {
|
|
60
|
+
if (!hint)
|
|
61
|
+
return undefined;
|
|
62
|
+
if (hint === "enter")
|
|
63
|
+
return "default";
|
|
64
|
+
return hint;
|
|
65
|
+
}
|
|
66
|
+
function resolveEditable(editable, readOnly) {
|
|
67
|
+
if (editable !== undefined)
|
|
68
|
+
return editable;
|
|
69
|
+
if (readOnly === true)
|
|
70
|
+
return false;
|
|
71
|
+
return undefined;
|
|
72
|
+
}
|
|
73
|
+
function mapReturnKeyType(value) {
|
|
74
|
+
if (value === "google" || value === "yahoo")
|
|
75
|
+
return "search";
|
|
76
|
+
if (value === "join" || value === "route" || value === "emergency-call")
|
|
77
|
+
return "default";
|
|
78
|
+
return value;
|
|
79
|
+
}
|
|
80
|
+
function mapKeyboardType(value) {
|
|
81
|
+
switch (value) {
|
|
82
|
+
case "default":
|
|
83
|
+
return "text";
|
|
84
|
+
case "email-address":
|
|
85
|
+
return "email";
|
|
86
|
+
case "numeric":
|
|
87
|
+
return "decimal";
|
|
88
|
+
case "number-pad":
|
|
89
|
+
return "number";
|
|
90
|
+
case "decimal-pad":
|
|
91
|
+
return "decimal";
|
|
92
|
+
case "phone-pad":
|
|
93
|
+
return "phone";
|
|
94
|
+
case "url":
|
|
95
|
+
return "uri";
|
|
96
|
+
case "ascii-capable":
|
|
97
|
+
case "numbers-and-punctuation":
|
|
98
|
+
case "name-phone-pad":
|
|
99
|
+
case "twitter":
|
|
100
|
+
case "web-search":
|
|
101
|
+
case "visible-password":
|
|
102
|
+
return "text";
|
|
103
|
+
default:
|
|
104
|
+
return "text";
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
// endregion Prop mapping
|
|
108
|
+
// region Style mapping
|
|
109
|
+
/** Compose modifiers take dp numbers; anything else (percent strings, animated values) is ignored. */
|
|
110
|
+
function dp(value) {
|
|
111
|
+
return typeof value === "number" ? value : undefined;
|
|
112
|
+
}
|
|
113
|
+
/**
|
|
114
|
+
* Maps the box-style padding keys to Compose padding modifiers, with the same
|
|
115
|
+
* cascade as `@expo/ui`'s `transformToModifiers`: a specific side beats the
|
|
116
|
+
* directional shorthand, which beats `padding`. `backgroundColor` is
|
|
117
|
+
* deliberately not mapped — the RN wrapper in `TextInput.tsx` paints the
|
|
118
|
+
* surface.
|
|
119
|
+
*/
|
|
120
|
+
function styleToModifiers(style) {
|
|
121
|
+
if (!style)
|
|
122
|
+
return [];
|
|
123
|
+
const all = dp(style.padding) ?? 0;
|
|
124
|
+
const top = dp(style.paddingTop) ?? dp(style.paddingVertical) ?? all;
|
|
125
|
+
const bottom = dp(style.paddingBottom) ?? dp(style.paddingVertical) ?? all;
|
|
126
|
+
const start = dp(style.paddingLeft) ?? dp(style.paddingHorizontal) ?? all;
|
|
127
|
+
const end = dp(style.paddingRight) ?? dp(style.paddingHorizontal) ?? all;
|
|
128
|
+
if (!top && !bottom && !start && !end)
|
|
129
|
+
return [];
|
|
130
|
+
if (top === bottom && bottom === start && start === end)
|
|
131
|
+
return [paddingAll(top)];
|
|
132
|
+
return [padding(start, top, end, bottom)];
|
|
133
|
+
}
|
|
134
|
+
// endregion Style mapping
|
|
135
|
+
/**
|
|
136
|
+
* Picks the password keyboard for a secure field: numeric keyboards become
|
|
137
|
+
* `numberPassword` (a `TYPE_CLASS_NUMBER | TYPE_NUMBER_VARIATION_PASSWORD`
|
|
138
|
+
* field), everything else `password` (`TYPE_TEXT_VARIATION_PASSWORD`).
|
|
139
|
+
*/
|
|
140
|
+
function passwordKeyboardType(mapped) {
|
|
141
|
+
return mapped === "number" || mapped === "decimal" || mapped === "phone" ? "numberPassword" : "password";
|
|
142
|
+
}
|
|
143
|
+
export function NativeTextField({ ref, value, onChangeText, placeholder, autoFocus, editable: editableProp, multiline, keyboardType: keyboardTypeProp, autoCapitalize, autoCorrect, returnKeyType: returnKeyTypeProp, onSubmitEditing, onFocus, onBlur, cursorColor, textAlign, readOnly, inputMode, enterKeyHint, defaultValue, numberOfLines: numberOfLinesProp, rows, testID, placeholderTextColor, textStyle, style, secureTextEntry, autoComplete, onContentSizeChange, maxLength, caretHidden, selectionColor, selectionHandleColor, selection, onSelectionChange, selectTextOnFocus, modifiers: userModifiers, }) {
|
|
144
|
+
const editable = resolveEditable(editableProp, readOnly);
|
|
145
|
+
const numberOfLines = numberOfLinesProp ?? rows;
|
|
146
|
+
const keyboardType = keyboardTypeProp ?? inputModeToKeyboardType(inputMode);
|
|
147
|
+
const returnKeyType = returnKeyTypeProp ?? enterKeyHintToReturnKeyType(enterKeyHint);
|
|
148
|
+
const initialFallbackRef = useRef(defaultValue ?? "");
|
|
149
|
+
const fallback = useNativeState(initialFallbackRef.current);
|
|
150
|
+
const state = (value ?? fallback);
|
|
151
|
+
const innerRef = useRef(null);
|
|
152
|
+
const isFocusedRef = useRef(false);
|
|
153
|
+
useImperativeHandle(ref, () => ({
|
|
154
|
+
focus: () => {
|
|
155
|
+
innerRef.current?.focus();
|
|
156
|
+
},
|
|
157
|
+
blur: () => {
|
|
158
|
+
innerRef.current?.blur();
|
|
159
|
+
},
|
|
160
|
+
clear: () => {
|
|
161
|
+
innerRef.current?.clear();
|
|
162
|
+
},
|
|
163
|
+
isFocused: () => isFocusedRef.current,
|
|
164
|
+
setSelection: (start, end) => innerRef.current?.setSelection(start, end) ?? Promise.resolve(),
|
|
165
|
+
}), []);
|
|
166
|
+
const handleFocusChanged = (focused) => {
|
|
167
|
+
isFocusedRef.current = focused;
|
|
168
|
+
if (focused && selectTextOnFocus) {
|
|
169
|
+
innerRef.current?.setSelection(0, state.value.length);
|
|
170
|
+
}
|
|
171
|
+
if (focused)
|
|
172
|
+
onFocus?.();
|
|
173
|
+
else
|
|
174
|
+
onBlur?.();
|
|
175
|
+
};
|
|
176
|
+
// Sticky for the life of the mounted field: revealing a password
|
|
177
|
+
// (`secureTextEntry` -> false on the same element) must keep the password
|
|
178
|
+
// keyboard options unchanged so the IME neither restarts nor starts
|
|
179
|
+
// suggesting/learning the now-visible text. Written during render like the
|
|
180
|
+
// wrapper's own `activeSecureRef`; a fresh mount starts from the prop.
|
|
181
|
+
const everSecureRef = useRef(!!secureTextEntry);
|
|
182
|
+
if (secureTextEntry)
|
|
183
|
+
everSecureRef.current = true;
|
|
184
|
+
const passwordField = everSecureRef.current;
|
|
185
|
+
const mappedKeyboardType = keyboardType ? mapKeyboardType(keyboardType) : undefined;
|
|
186
|
+
const resolvedKeyboardType = passwordField
|
|
187
|
+
? passwordKeyboardType(mappedKeyboardType)
|
|
188
|
+
: mappedKeyboardType;
|
|
189
|
+
const capitalization = autoCapitalize ?? (passwordField ? "none" : undefined);
|
|
190
|
+
const autoCorrectEnabled = autoCorrect ?? (passwordField ? false : undefined);
|
|
191
|
+
const keyboardOptions = resolvedKeyboardType || capitalization || autoCorrectEnabled !== undefined || returnKeyType
|
|
192
|
+
? {
|
|
193
|
+
...(resolvedKeyboardType ? { keyboardType: resolvedKeyboardType } : null),
|
|
194
|
+
...(capitalization ? { capitalization } : null),
|
|
195
|
+
...(autoCorrectEnabled !== undefined ? { autoCorrectEnabled } : null),
|
|
196
|
+
...(returnKeyType ? { imeAction: mapReturnKeyType(returnKeyType) } : null),
|
|
197
|
+
}
|
|
198
|
+
: undefined;
|
|
199
|
+
const keyboardActions = onSubmitEditing
|
|
200
|
+
? {
|
|
201
|
+
onDone: onSubmitEditing,
|
|
202
|
+
onGo: onSubmitEditing,
|
|
203
|
+
onNext: onSubmitEditing,
|
|
204
|
+
onSearch: onSubmitEditing,
|
|
205
|
+
onSend: onSubmitEditing,
|
|
206
|
+
onPrevious: onSubmitEditing,
|
|
207
|
+
}
|
|
208
|
+
: undefined;
|
|
209
|
+
return (_jsx(BasicTextField, { ref: innerRef, modifiers: [
|
|
210
|
+
...(userModifiers ?? []),
|
|
211
|
+
...styleToModifiers(style),
|
|
212
|
+
...(testID ? [testIDModifier(testID)] : []),
|
|
213
|
+
...(autoComplete ? [semantics({ contentType: autoComplete })] : []),
|
|
214
|
+
...(onContentSizeChange ? [onSizeChanged(onContentSizeChange)] : []),
|
|
215
|
+
], value: state, autoFocus: autoFocus, readOnly: editable === false, singleLine: !multiline, maxLines: multiline && numberOfLines && numberOfLines > 0 ? numberOfLines : undefined, minLines: multiline && numberOfLines && numberOfLines > 0 ? numberOfLines : undefined, cursorColor: caretHidden ? "transparent" : (cursorColor ?? selectionColor), textStyle: textStyle || (textAlign && textAlign !== "auto")
|
|
216
|
+
? {
|
|
217
|
+
...textStyle,
|
|
218
|
+
...(textAlign && textAlign !== "auto" ? { textAlign } : null),
|
|
219
|
+
}
|
|
220
|
+
: undefined, visualTransformation: secureTextEntry ? "password" : undefined, textSelectionColors: selectionColor || selectionHandleColor
|
|
221
|
+
? {
|
|
222
|
+
handleColor: selectionHandleColor ?? selectionColor,
|
|
223
|
+
backgroundColor: selectionColor,
|
|
224
|
+
}
|
|
225
|
+
: undefined, keyboardOptions: keyboardOptions, keyboardActions: keyboardActions, onValueChange: onChangeText, maxLength: maxLength, onFocusChanged: handleFocusChanged, selection: selection, onSelectionChange: onSelectionChange, children: _jsx(BasicTextField.DecorationBox, { children: _jsxs(Box, { modifiers: [fillMaxWidth()], contentAlignment: textAlign === "center" ? "topCenter" : textAlign === "right" ? "topEnd" : undefined, children: [placeholder != null ? (_jsx(BasicTextField.Placeholder, { children: _jsx(Text, { color: placeholderTextColor, modifiers: [fillMaxWidth()], style: textAlign && textAlign !== "auto" ? { textAlign } : undefined, children: placeholder }) })) : null, _jsx(BasicTextField.InnerTextField, {})] }) }) }));
|
|
226
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Native text field indirection — default variant (iOS; also what Jest and
|
|
3
|
+
* TypeScript resolve).
|
|
4
|
+
*
|
|
5
|
+
* Re-exports `@expo/ui`'s universal `TextInput`, which on iOS bridges to
|
|
6
|
+
* SwiftUI's `TextField`/`SecureField` and already reports a password field to
|
|
7
|
+
* the keyboard when `secureTextEntry` is set.
|
|
8
|
+
*
|
|
9
|
+
* Metro resolves `nativeTextField.android.tsx` on Android instead: the package
|
|
10
|
+
* owns the Compose field there so a secure entry can declare a password
|
|
11
|
+
* keyboard type (see that file). Both variants must export the same names with
|
|
12
|
+
* the same types — consumers and the type checker only ever see this one.
|
|
13
|
+
*/
|
|
14
|
+
export { TextInput as NativeTextField, type TextInputProps as NativeTextFieldProps, } from "@expo/ui";
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Native text field indirection — default variant (iOS; also what Jest and
|
|
3
|
+
* TypeScript resolve).
|
|
4
|
+
*
|
|
5
|
+
* Re-exports `@expo/ui`'s universal `TextInput`, which on iOS bridges to
|
|
6
|
+
* SwiftUI's `TextField`/`SecureField` and already reports a password field to
|
|
7
|
+
* the keyboard when `secureTextEntry` is set.
|
|
8
|
+
*
|
|
9
|
+
* Metro resolves `nativeTextField.android.tsx` on Android instead: the package
|
|
10
|
+
* owns the Compose field there so a secure entry can declare a password
|
|
11
|
+
* keyboard type (see that file). Both variants must export the same names with
|
|
12
|
+
* the same types — consumers and the type checker only ever see this one.
|
|
13
|
+
*/
|
|
14
|
+
export { TextInput as NativeTextField, } from "@expo/ui";
|
|
@@ -36,7 +36,7 @@ export declare const spacing: {
|
|
|
36
36
|
readonly radiusNone: 0;
|
|
37
37
|
readonly radiusXs: 4;
|
|
38
38
|
readonly radiusSm: 8;
|
|
39
|
-
readonly radiusMd:
|
|
39
|
+
readonly radiusMd: 10;
|
|
40
40
|
readonly radiusLg: 14;
|
|
41
41
|
readonly radiusXl: 18;
|
|
42
42
|
readonly radius2xl: 24;
|
|
@@ -47,5 +47,5 @@ export declare const spacing: {
|
|
|
47
47
|
readonly iconLg: 32;
|
|
48
48
|
readonly iconXl: 48;
|
|
49
49
|
};
|
|
50
|
-
export declare const base: 8, xxs: 2, xs: 4, sm: 8, smd: 12, md: 16, mdl: 20, lg: 24, xl: 32, xxl: 48, xxxl: 64, gutter: 16, gutterVertical: 24, screenPadding: 16, sectionSpacing: 24, rowPaddingY: 10, rowPaddingX: 16, rowGap: 12, rowMinHeight: 40, formRowMinHeight: 32, cardPadding: 16, dialogPadding: 20, touchTarget: 44, buttonPadding: 10, inputPadding: 10, listItemSpacing: 8, radiusNone: 0, radiusXs: 4, radiusSm: 8, radiusMd:
|
|
50
|
+
export declare const base: 8, xxs: 2, xs: 4, sm: 8, smd: 12, md: 16, mdl: 20, lg: 24, xl: 32, xxl: 48, xxxl: 64, gutter: 16, gutterVertical: 24, screenPadding: 16, sectionSpacing: 24, rowPaddingY: 10, rowPaddingX: 16, rowGap: 12, rowMinHeight: 40, formRowMinHeight: 32, cardPadding: 16, dialogPadding: 20, touchTarget: 44, buttonPadding: 10, inputPadding: 10, listItemSpacing: 8, radiusNone: 0, radiusXs: 4, radiusSm: 8, radiusMd: 10, radiusLg: 14, radiusXl: 18, radius2xl: 24, radiusFull: 9999, iconXs: 12, iconSm: 16, iconMd: 24, iconLg: 32, iconXl: 48;
|
|
51
51
|
export declare const space: (multiplier: number) => number;
|
|
@@ -38,11 +38,11 @@ export const spacing = {
|
|
|
38
38
|
buttonPadding: 10, // Default button padding
|
|
39
39
|
inputPadding: 10, // Default input padding
|
|
40
40
|
listItemSpacing: 8, // Space between list items
|
|
41
|
-
// Border radius — shadcn-inspired scale (radiusMd =
|
|
41
|
+
// Border radius — shadcn-inspired scale (radiusMd = 10px default)
|
|
42
42
|
radiusNone: 0,
|
|
43
43
|
radiusXs: 4,
|
|
44
44
|
radiusSm: 8,
|
|
45
|
-
radiusMd:
|
|
45
|
+
radiusMd: 10,
|
|
46
46
|
radiusLg: 14,
|
|
47
47
|
radiusXl: 18,
|
|
48
48
|
radius2xl: 24,
|
package/dist/design-system.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
3
|
"package": "@mrmeg/expo-ui",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.26.0",
|
|
5
5
|
"tokens": {
|
|
6
6
|
"spacing": {
|
|
7
7
|
"entries": [
|
|
@@ -127,7 +127,7 @@
|
|
|
127
127
|
},
|
|
128
128
|
{
|
|
129
129
|
"name": "radiusMd",
|
|
130
|
-
"value":
|
|
130
|
+
"value": 10
|
|
131
131
|
},
|
|
132
132
|
{
|
|
133
133
|
"name": "radiusLg",
|
|
@@ -2232,5 +2232,154 @@
|
|
|
2232
2232
|
"variantValues": null,
|
|
2233
2233
|
"sizeValues": null
|
|
2234
2234
|
}
|
|
2235
|
-
]
|
|
2235
|
+
],
|
|
2236
|
+
"icons": {
|
|
2237
|
+
"names": [
|
|
2238
|
+
"archive",
|
|
2239
|
+
"arrow-down",
|
|
2240
|
+
"arrow-left",
|
|
2241
|
+
"arrow-right",
|
|
2242
|
+
"arrow-up",
|
|
2243
|
+
"award",
|
|
2244
|
+
"beer",
|
|
2245
|
+
"bell",
|
|
2246
|
+
"bell-off",
|
|
2247
|
+
"bold",
|
|
2248
|
+
"bookmark",
|
|
2249
|
+
"box",
|
|
2250
|
+
"briefcase",
|
|
2251
|
+
"calendar",
|
|
2252
|
+
"calendar-check",
|
|
2253
|
+
"camera",
|
|
2254
|
+
"camera-off",
|
|
2255
|
+
"chart-no-axes-column",
|
|
2256
|
+
"check",
|
|
2257
|
+
"chevron-down",
|
|
2258
|
+
"chevron-left",
|
|
2259
|
+
"chevron-right",
|
|
2260
|
+
"chevron-up",
|
|
2261
|
+
"circle-alert",
|
|
2262
|
+
"circle-check-big",
|
|
2263
|
+
"circle-plus",
|
|
2264
|
+
"circle-question-mark",
|
|
2265
|
+
"circle-stop",
|
|
2266
|
+
"circle-x",
|
|
2267
|
+
"clock",
|
|
2268
|
+
"cloud",
|
|
2269
|
+
"cloud-off",
|
|
2270
|
+
"code",
|
|
2271
|
+
"coffee",
|
|
2272
|
+
"compass",
|
|
2273
|
+
"copy",
|
|
2274
|
+
"credit-card",
|
|
2275
|
+
"crosshair",
|
|
2276
|
+
"database",
|
|
2277
|
+
"dollar-sign",
|
|
2278
|
+
"download",
|
|
2279
|
+
"droplet",
|
|
2280
|
+
"ellipsis-vertical",
|
|
2281
|
+
"external-link",
|
|
2282
|
+
"eye",
|
|
2283
|
+
"eye-off",
|
|
2284
|
+
"face-slightly-smiling",
|
|
2285
|
+
"file",
|
|
2286
|
+
"file-text",
|
|
2287
|
+
"folder",
|
|
2288
|
+
"gift",
|
|
2289
|
+
"git-branch",
|
|
2290
|
+
"git-pull-request",
|
|
2291
|
+
"globe",
|
|
2292
|
+
"grid-3x3",
|
|
2293
|
+
"hash",
|
|
2294
|
+
"heart",
|
|
2295
|
+
"hexagon",
|
|
2296
|
+
"house",
|
|
2297
|
+
"image",
|
|
2298
|
+
"inbox",
|
|
2299
|
+
"info",
|
|
2300
|
+
"italic",
|
|
2301
|
+
"key",
|
|
2302
|
+
"layers",
|
|
2303
|
+
"layout-grid",
|
|
2304
|
+
"link",
|
|
2305
|
+
"link-2",
|
|
2306
|
+
"list",
|
|
2307
|
+
"loader",
|
|
2308
|
+
"lock",
|
|
2309
|
+
"lock-open",
|
|
2310
|
+
"log-in",
|
|
2311
|
+
"log-out",
|
|
2312
|
+
"mail",
|
|
2313
|
+
"map",
|
|
2314
|
+
"map-pin",
|
|
2315
|
+
"maximize",
|
|
2316
|
+
"maximize-2",
|
|
2317
|
+
"menu",
|
|
2318
|
+
"message-circle",
|
|
2319
|
+
"message-square",
|
|
2320
|
+
"mic",
|
|
2321
|
+
"mic-off",
|
|
2322
|
+
"minus",
|
|
2323
|
+
"monitor",
|
|
2324
|
+
"moon",
|
|
2325
|
+
"music",
|
|
2326
|
+
"navigation",
|
|
2327
|
+
"package",
|
|
2328
|
+
"panel-left",
|
|
2329
|
+
"panels-top-left",
|
|
2330
|
+
"party-popper",
|
|
2331
|
+
"pen",
|
|
2332
|
+
"pen-line",
|
|
2333
|
+
"pencil",
|
|
2334
|
+
"percent",
|
|
2335
|
+
"phone",
|
|
2336
|
+
"play",
|
|
2337
|
+
"plus",
|
|
2338
|
+
"printer",
|
|
2339
|
+
"radio",
|
|
2340
|
+
"refresh-cw",
|
|
2341
|
+
"repeat",
|
|
2342
|
+
"rotate-cw",
|
|
2343
|
+
"search",
|
|
2344
|
+
"send",
|
|
2345
|
+
"server",
|
|
2346
|
+
"settings",
|
|
2347
|
+
"share",
|
|
2348
|
+
"share-2",
|
|
2349
|
+
"shield",
|
|
2350
|
+
"shield-off",
|
|
2351
|
+
"shopping-bag",
|
|
2352
|
+
"shopping-cart",
|
|
2353
|
+
"shuffle",
|
|
2354
|
+
"slash",
|
|
2355
|
+
"sliders-horizontal",
|
|
2356
|
+
"sliders-vertical",
|
|
2357
|
+
"smartphone",
|
|
2358
|
+
"square",
|
|
2359
|
+
"square-check-big",
|
|
2360
|
+
"square-pen",
|
|
2361
|
+
"star",
|
|
2362
|
+
"sun",
|
|
2363
|
+
"tag",
|
|
2364
|
+
"terminal",
|
|
2365
|
+
"thumbs-up",
|
|
2366
|
+
"trash",
|
|
2367
|
+
"trending-down",
|
|
2368
|
+
"trending-up",
|
|
2369
|
+
"triangle-alert",
|
|
2370
|
+
"truck",
|
|
2371
|
+
"underline",
|
|
2372
|
+
"upload",
|
|
2373
|
+
"user",
|
|
2374
|
+
"user-check",
|
|
2375
|
+
"user-minus",
|
|
2376
|
+
"user-plus",
|
|
2377
|
+
"users",
|
|
2378
|
+
"video",
|
|
2379
|
+
"wifi-off",
|
|
2380
|
+
"wrench",
|
|
2381
|
+
"x",
|
|
2382
|
+
"zap"
|
|
2383
|
+
]
|
|
2384
|
+
}
|
|
2236
2385
|
}
|
|
@@ -1,20 +1,3 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Registers the Feather icon font in expo-font's server store during
|
|
3
|
-
* export-time rendering (`expo export` renders every route's HTML shell in
|
|
4
|
-
* Node, where the module-scope client load above is guarded off).
|
|
5
|
-
*
|
|
6
|
-
* Must be called from a **render body** (see `useResources` below): expo-font
|
|
7
|
-
* scopes its server store per render context, so a module-scope registration
|
|
8
|
-
* lands in at most one route's shell. Registering during render means every
|
|
9
|
-
* exported shell emits the `@font-face` rule and renders `<Icon>` glyphs —
|
|
10
|
-
* without it the shell contains an EMPTY icon text node while the client's
|
|
11
|
-
* first render (font pre-loaded above) contains the glyph, and React throws
|
|
12
|
-
* hydration error #418 on every cold load and regenerates the tree.
|
|
13
|
-
*
|
|
14
|
-
* No-op on native and on client web (both already covered: native by the
|
|
15
|
-
* effect below, client web by the module-scope call above).
|
|
16
|
-
*/
|
|
17
|
-
export declare function ensureIconFontRegistered(): void;
|
|
18
1
|
interface LoadResourcesResult {
|
|
19
2
|
loaded: boolean;
|
|
20
3
|
error: Error | null;
|
|
@@ -27,12 +10,16 @@ interface LoadResourcesResult {
|
|
|
27
10
|
* font files. Web loads Inter from Google Fonts as a single CSS family;
|
|
28
11
|
* weight differentiation there comes from a numeric fontWeight instead.
|
|
29
12
|
*
|
|
13
|
+
* Icons need nothing here: `Icon` renders `lucide-react-native` SVGs, which
|
|
14
|
+
* have no font face to register on any platform, so server-rendered HTML
|
|
15
|
+
* carries the glyph markup and hydrates without a font round trip.
|
|
16
|
+
*
|
|
30
17
|
* A host app that overrides the sans-serif families via `setFonts` owns
|
|
31
18
|
* loading its own faces (typically through `expo-font`), so the Inter fetch
|
|
32
19
|
* is skipped entirely — nothing would reference those files. For the skip to
|
|
33
20
|
* apply, call `setFonts` before this hook mounts (module scope or ahead of
|
|
34
21
|
* rendering the root); a later call still re-skins text, it just doesn't
|
|
35
|
-
* un-download Inter.
|
|
22
|
+
* un-download Inter.
|
|
36
23
|
*/
|
|
37
24
|
export declare const useResources: () => LoadResourcesResult;
|
|
38
25
|
export {};
|
|
@@ -1,46 +1,8 @@
|
|
|
1
1
|
import { useEffect, useState } from "react";
|
|
2
2
|
import * as Font from "expo-font";
|
|
3
|
-
import Feather from "@expo/vector-icons/Feather";
|
|
4
3
|
import { Inter_400Regular, Inter_500Medium, Inter_600SemiBold, Inter_700Bold, } from "@expo-google-fonts/inter";
|
|
5
4
|
import { Platform } from "react-native";
|
|
6
5
|
import { useThemeStore } from "../state/themeStore.js";
|
|
7
|
-
// Eager, module-scope load that primes the Feather font before first render:
|
|
8
|
-
// on web it synchronously injects the @font-face rule, so icons have their
|
|
9
|
-
// glyphs on the first paint; on native it just starts the fetch ahead of the
|
|
10
|
-
// effect below (which then no-ops for the already-loaded font).
|
|
11
|
-
//
|
|
12
|
-
// Guarded on `window` because web bundles are also evaluated in Node when
|
|
13
|
-
// `expo export` renders the HTML shell, where there is no document to inject
|
|
14
|
-
// into.
|
|
15
|
-
if (typeof window !== "undefined") {
|
|
16
|
-
void Font.loadAsync(Feather.font);
|
|
17
|
-
}
|
|
18
|
-
/**
|
|
19
|
-
* Registers the Feather icon font in expo-font's server store during
|
|
20
|
-
* export-time rendering (`expo export` renders every route's HTML shell in
|
|
21
|
-
* Node, where the module-scope client load above is guarded off).
|
|
22
|
-
*
|
|
23
|
-
* Must be called from a **render body** (see `useResources` below): expo-font
|
|
24
|
-
* scopes its server store per render context, so a module-scope registration
|
|
25
|
-
* lands in at most one route's shell. Registering during render means every
|
|
26
|
-
* exported shell emits the `@font-face` rule and renders `<Icon>` glyphs —
|
|
27
|
-
* without it the shell contains an EMPTY icon text node while the client's
|
|
28
|
-
* first render (font pre-loaded above) contains the glyph, and React throws
|
|
29
|
-
* hydration error #418 on every cold load and regenerates the tree.
|
|
30
|
-
*
|
|
31
|
-
* No-op on native and on client web (both already covered: native by the
|
|
32
|
-
* effect below, client web by the module-scope call above).
|
|
33
|
-
*/
|
|
34
|
-
export function ensureIconFontRegistered() {
|
|
35
|
-
if (Platform.OS !== "web" || typeof window !== "undefined")
|
|
36
|
-
return;
|
|
37
|
-
if (Font.isLoaded("feather"))
|
|
38
|
-
return;
|
|
39
|
-
// Synchronous on server-web (expo-font routes to registerStaticFont). No
|
|
40
|
-
// `void`/try-catch: a throw here means we're outside a render scope, which
|
|
41
|
-
// must be loud.
|
|
42
|
-
Font.loadAsync(Feather.font);
|
|
43
|
-
}
|
|
44
6
|
// The four static Inter weights StyledText's native family keys point at
|
|
45
7
|
// (see constants/fonts.ts). Native-only: web never renders these family names
|
|
46
8
|
// ("Inter_400Regular" etc.) — fontFamilies.sansSerif resolves every weight to
|
|
@@ -86,17 +48,18 @@ function ensureWebFontStylesheet() {
|
|
|
86
48
|
* font files. Web loads Inter from Google Fonts as a single CSS family;
|
|
87
49
|
* weight differentiation there comes from a numeric fontWeight instead.
|
|
88
50
|
*
|
|
51
|
+
* Icons need nothing here: `Icon` renders `lucide-react-native` SVGs, which
|
|
52
|
+
* have no font face to register on any platform, so server-rendered HTML
|
|
53
|
+
* carries the glyph markup and hydrates without a font round trip.
|
|
54
|
+
*
|
|
89
55
|
* A host app that overrides the sans-serif families via `setFonts` owns
|
|
90
56
|
* loading its own faces (typically through `expo-font`), so the Inter fetch
|
|
91
57
|
* is skipped entirely — nothing would reference those files. For the skip to
|
|
92
58
|
* apply, call `setFonts` before this hook mounts (module scope or ahead of
|
|
93
59
|
* rendering the root); a later call still re-skins text, it just doesn't
|
|
94
|
-
* un-download Inter.
|
|
60
|
+
* un-download Inter.
|
|
95
61
|
*/
|
|
96
62
|
export const useResources = () => {
|
|
97
|
-
// Export-time renders need the icon font registered per render scope; the
|
|
98
|
-
// module-scope call only reaches the client. See ensureIconFontRegistered.
|
|
99
|
-
ensureIconFontRegistered();
|
|
100
63
|
const [loaded, setLoaded] = useState(false);
|
|
101
64
|
const [error, setError] = useState(null);
|
|
102
65
|
useEffect(() => {
|
|
@@ -107,7 +70,6 @@ export const useResources = () => {
|
|
|
107
70
|
async function loadResourcesAndDataAsync() {
|
|
108
71
|
try {
|
|
109
72
|
const fontPromise = Promise.all([
|
|
110
|
-
Font.loadAsync(Feather.font),
|
|
111
73
|
sansSerifOverridden ? Promise.resolve() : loadNativeInterFonts(),
|
|
112
74
|
sansSerifOverridden ? Promise.resolve() : ensureWebFontStylesheet(),
|
|
113
75
|
]);
|
|
@@ -36,7 +36,7 @@ export type ShapeOverrides = {
|
|
|
36
36
|
button?: {
|
|
37
37
|
/**
|
|
38
38
|
* Border radius applied to every Button preset. Package default:
|
|
39
|
-
* `spacing.radiusMd` (
|
|
39
|
+
* `spacing.radiusMd` (10). Use 9999 for pill buttons. A caller `style`
|
|
40
40
|
* still wins over this, as it always has.
|
|
41
41
|
*/
|
|
42
42
|
borderRadius?: number;
|
package/llms-full.md
CHANGED
|
@@ -36,8 +36,8 @@ import { Button, colors, UIProvider, useTheme } from "@mrmeg/expo-ui";
|
|
|
36
36
|
## App Setup
|
|
37
37
|
|
|
38
38
|
Call `useResources()` once near the Expo app root; it resolves `{ loaded, error }`
|
|
39
|
-
and loads the
|
|
40
|
-
|
|
39
|
+
and loads the four static Inter weights (web gets a single Google Fonts Inter
|
|
40
|
+
stylesheet). Icons are `lucide-react-native` SVGs, so there is no icon font to load.
|
|
41
41
|
|
|
42
42
|
Mount `UIProvider` once near the root. It owns the package `Notification`,
|
|
43
43
|
`StatusBar`, default `@rn-primitives` portal host, and native keyboard-avoiding
|
|
@@ -115,7 +115,7 @@ before creating a new app-local primitive.
|
|
|
115
115
|
| `DropdownMenu` | Menus and command lists | Requires root `UIProvider` portal host. |
|
|
116
116
|
| `EmptyState` | No-data or recoverable error regions | Props: `icon`, `title`, `description`, `actionLabel`, `onAction`, `actionPreset`. |
|
|
117
117
|
| `ErrorBoundary` | React render error fallback | Use for route or feature boundaries. |
|
|
118
|
-
| `Icon` |
|
|
118
|
+
| `Icon` | Lucide or custom icons with theme tokens | `color` takes a theme color name; pass `decorative` to hide from a11y. |
|
|
119
119
|
| `InputOTP` | Verification code entry | Prefer over manually managed text input groups. |
|
|
120
120
|
| `Item` | List / settings rows | Applies the row density tokens and a 44px native hit area (40px on web). |
|
|
121
121
|
| `KeyboardAvoidingView` | Native keyboard-aware roots, composer footers, form-heavy subtrees | `UIProvider` already mounts one root; use directly only for custom subtrees. |
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mrmeg/expo-ui",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.26.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"description": "Reusable Expo and React Native UI primitives for MrMeg projects.",
|
|
6
6
|
"keywords": [
|
|
@@ -86,13 +86,12 @@
|
|
|
86
86
|
"scripts": {
|
|
87
87
|
"typecheck": "tsc --noEmit -p tsconfig.json",
|
|
88
88
|
"test": "jest --config ../../jest.config.js packages/ui/src --runInBand --watchman=false && bun run check:forbidden-imports",
|
|
89
|
-
"build": "rm -rf dist && tsc -p tsconfig.build.json && node ../../scripts/fix-package-esm.mjs ui && node ../../scripts/build-design-system-manifest.mjs ui && bun run check:forbidden-imports",
|
|
89
|
+
"build": "node ../../scripts/build-icon-registry.mjs && rm -rf dist && tsc -p tsconfig.build.json && node ../../scripts/fix-package-esm.mjs ui && node ../../scripts/build-design-system-manifest.mjs ui && bun run check:forbidden-imports",
|
|
90
90
|
"check:forbidden-imports": "node ../../scripts/check-ui-forbidden-imports.mjs",
|
|
91
91
|
"publish:dry-run": "bun pm pack --dry-run"
|
|
92
92
|
},
|
|
93
93
|
"dependencies": {
|
|
94
94
|
"@expo-google-fonts/inter": "^0.4.2",
|
|
95
|
-
"@expo/vector-icons": ">=15.0.0 <16.0.0",
|
|
96
95
|
"@rn-primitives/accordion": "~1.5.2",
|
|
97
96
|
"@rn-primitives/alert-dialog": "~1.5.2",
|
|
98
97
|
"@rn-primitives/checkbox": "~1.5.2",
|
|
@@ -118,12 +117,14 @@
|
|
|
118
117
|
"expo": ">=56.0.0 <59.0.0 || >=58.0.0-0 <59.0.0",
|
|
119
118
|
"expo-font": ">=56.0.0 <59.0.0 || >=58.0.0-0 <59.0.0",
|
|
120
119
|
"expo-haptics": ">=56.0.0 <59.0.0 || >=58.0.0-0 <59.0.0",
|
|
120
|
+
"lucide-react-native": ">=1.46.0 <2.0.0",
|
|
121
121
|
"react": ">=19.2.0 <20.0.0",
|
|
122
122
|
"react-native": ">=0.85.0 <0.89.0 || >=0.88.0-0 <0.89.0",
|
|
123
123
|
"react-native-gesture-handler": ">=2.30.0 <4.0.0",
|
|
124
124
|
"react-native-keyboard-controller": ">=1.21.0 <2.0.0",
|
|
125
125
|
"react-native-safe-area-context": ">=5.6.0 <6.0.0",
|
|
126
126
|
"react-native-screens": ">=4.23.0 <5.0.0",
|
|
127
|
+
"react-native-svg": ">=15.0.0 <16.0.0",
|
|
127
128
|
"react-native-web": ">=0.21.0 <0.22.0",
|
|
128
129
|
"zustand": ">=5.0.0 <6.0.0"
|
|
129
130
|
},
|