@mrmeg/expo-ui 0.27.0 → 0.28.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 +712 -0
- package/LLM_USAGE.md +260 -32
- package/README.md +558 -101
- package/dist/components/Accordion.js +12 -6
- package/dist/components/AnimatedView.d.ts +1 -1
- package/dist/components/AnimatedView.js +5 -2
- package/dist/components/Avatar.d.ts +1 -1
- package/dist/components/Avatar.js +2 -2
- package/dist/components/Badge.js +5 -1
- package/dist/components/BottomSheet.d.ts +66 -9
- package/dist/components/BottomSheet.js +143 -39
- package/dist/components/Button.d.ts +20 -4
- package/dist/components/Button.js +64 -43
- package/dist/components/Card.d.ts +1 -1
- package/dist/components/Card.js +24 -4
- package/dist/components/Carousel.js +2 -2
- package/dist/components/Checkbox.js +51 -44
- package/dist/components/Collapsible.js +7 -3
- package/dist/components/Dialog.js +178 -29
- package/dist/components/Drawer.d.ts +1 -1
- package/dist/components/Drawer.js +65 -55
- package/dist/components/DropdownMenu.js +11 -7
- package/dist/components/EmptyState.d.ts +2 -2
- package/dist/components/EmptyState.js +5 -2
- package/dist/components/Hydrated.d.ts +31 -0
- package/dist/components/Hydrated.js +30 -0
- package/dist/components/Icon.d.ts +17 -5
- package/dist/components/Icon.js +31 -10
- package/dist/components/InputOTP.js +7 -3
- package/dist/components/Item.d.ts +103 -4
- package/dist/components/Item.js +168 -15
- package/dist/components/KeyboardAvoidingView.d.ts +1 -1
- package/dist/components/KeyboardAvoidingView.js +4 -1
- package/dist/components/Label.js +3 -2
- package/dist/components/MaxWidthContainer.js +1 -1
- package/dist/components/Notification.js +45 -27
- package/dist/components/Popover.d.ts +18 -5
- package/dist/components/Popover.js +72 -8
- package/dist/components/Progress.js +8 -6
- package/dist/components/RadioGroup.js +52 -49
- package/dist/components/Screen.d.ts +61 -0
- package/dist/components/Screen.js +62 -0
- package/dist/components/SectionHeader.d.ts +1 -1
- package/dist/components/SegmentedControl.d.ts +12 -4
- package/dist/components/SegmentedControl.js +94 -4
- package/dist/components/Select.js +21 -26
- package/dist/components/Skeleton.js +24 -10
- package/dist/components/Slider.d.ts +11 -3
- package/dist/components/Slider.js +74 -6
- package/dist/components/StatCard.d.ts +4 -2
- package/dist/components/StatCard.js +3 -1
- package/dist/components/StyledText.d.ts +7 -1
- package/dist/components/StyledText.js +5 -3
- package/dist/components/Switch.d.ts +1 -1
- package/dist/components/Switch.js +23 -32
- package/dist/components/Tabs.d.ts +2 -2
- package/dist/components/Tabs.js +13 -24
- package/dist/components/TextInput.d.ts +1 -1
- package/dist/components/TextInput.js +73 -27
- package/dist/components/Toggle.d.ts +2 -42
- package/dist/components/Toggle.js +38 -27
- package/dist/components/ToggleGroup.d.ts +1 -1
- package/dist/components/ToggleGroup.js +37 -32
- package/dist/components/Tooltip.d.ts +1 -1
- package/dist/components/Tooltip.js +9 -3
- package/dist/components/UIProvider.d.ts +22 -2
- package/dist/components/UIProvider.js +15 -3
- package/dist/components/bottomSheetDismiss.d.ts +19 -0
- package/dist/components/bottomSheetDismiss.js +44 -0
- package/dist/components/bottomSheetHostSupport.d.ts +46 -0
- package/dist/components/bottomSheetHostSupport.js +67 -0
- package/dist/components/index.d.ts +46 -44
- package/dist/components/index.js +2 -0
- package/dist/components/keyboardDismiss.d.ts +25 -0
- package/dist/components/keyboardDismiss.js +60 -3
- package/dist/components/nativeTextField.android.js +13 -9
- package/dist/constants/colors.d.ts +40 -5
- package/dist/constants/colors.js +26 -8
- package/dist/constants/fonts.d.ts +41 -2
- package/dist/constants/fonts.js +46 -7
- package/dist/constants/index.d.ts +5 -4
- package/dist/constants/index.js +1 -0
- package/dist/constants/interaction.d.ts +18 -0
- package/dist/constants/interaction.js +18 -0
- package/dist/constants/spacing.d.ts +2 -1
- package/dist/constants/spacing.js +2 -1
- package/dist/design-system.json +154 -2
- package/dist/hooks/index.d.ts +12 -7
- package/dist/hooks/index.js +5 -0
- package/dist/hooks/useDimensions.d.ts +13 -8
- package/dist/hooks/useDimensions.js +123 -48
- package/dist/hooks/useFocusVisible.d.ts +30 -0
- package/dist/hooks/useFocusVisible.js +45 -0
- package/dist/hooks/useFontStyle.d.ts +2 -2
- package/dist/hooks/useFontStyle.js +3 -2
- package/dist/hooks/useHydrated.d.ts +13 -0
- package/dist/hooks/useHydrated.js +19 -0
- package/dist/hooks/useNotificationOffset.d.ts +20 -0
- package/dist/hooks/useNotificationOffset.js +34 -0
- package/dist/hooks/useResources.d.ts +21 -20
- package/dist/hooks/useResources.js +72 -68
- package/dist/hooks/useScalePress.d.ts +8 -5
- package/dist/hooks/useScalePress.js +16 -9
- package/dist/hooks/useShape.d.ts +24 -0
- package/dist/hooks/useShape.js +24 -0
- package/dist/hooks/useStaggeredEntrance.js +9 -7
- package/dist/hooks/useTheme.d.ts +3 -3
- package/dist/hooks/useTheme.js +177 -66
- package/dist/hooks/useWindowInsets.d.ts +13 -0
- package/dist/hooks/useWindowInsets.js +23 -0
- package/dist/index.d.ts +5 -5
- package/dist/lib/composeRefs.d.ts +8 -0
- package/dist/lib/composeRefs.js +20 -0
- package/dist/lib/haptics.d.ts +12 -0
- package/dist/lib/haptics.js +21 -0
- package/dist/lib/index.d.ts +6 -4
- package/dist/lib/index.js +2 -0
- package/dist/lib/interFonts.d.ts +15 -0
- package/dist/lib/interFonts.js +1 -0
- package/dist/lib/interFonts.native.d.ts +3 -0
- package/dist/lib/interFonts.native.js +20 -0
- package/dist/lib/popoverPlacement.d.ts +36 -0
- package/dist/lib/popoverPlacement.js +29 -0
- package/dist/lib/stateSurface.d.ts +28 -0
- package/dist/lib/stateSurface.js +30 -0
- package/dist/lib/themedStyles.d.ts +1 -1
- package/dist/lib/useAnimatedValue.d.ts +15 -0
- package/dist/lib/useAnimatedValue.js +19 -0
- package/dist/lib/webHitTarget.d.ts +40 -0
- package/dist/lib/webHitTarget.js +37 -0
- package/dist/state/feedbackStore.d.ts +24 -0
- package/dist/state/feedbackStore.js +16 -0
- package/dist/state/globalUIStore.d.ts +27 -0
- package/dist/state/globalUIStore.js +23 -0
- package/dist/state/index.d.ts +7 -5
- package/dist/state/index.js +2 -0
- package/dist/state/initialScheme.d.ts +27 -0
- package/dist/state/initialScheme.js +29 -0
- package/dist/state/notify.d.ts +1 -1
- package/dist/state/themeColorScope.d.ts +1 -1
- package/dist/state/themeStore.d.ts +69 -16
- package/dist/state/themeStore.js +65 -38
- package/llms-full.md +56 -8
- package/llms.txt +10 -1
- package/package.json +69 -2
package/dist/components/index.js
CHANGED
|
@@ -16,6 +16,7 @@ export * from "./Drawer.js";
|
|
|
16
16
|
export * from "./DropdownMenu.js";
|
|
17
17
|
export * from "./EmptyState.js";
|
|
18
18
|
export * from "./ErrorBoundary.js";
|
|
19
|
+
export * from "./Hydrated.js";
|
|
19
20
|
export * from "./Icon.js";
|
|
20
21
|
export * from "./InputOTP.js";
|
|
21
22
|
export * from "./Item.js";
|
|
@@ -23,6 +24,7 @@ export * from "./KeyboardAvoidingView.js";
|
|
|
23
24
|
export * from "./Label.js";
|
|
24
25
|
export * from "./MaxWidthContainer.js";
|
|
25
26
|
export * from "./Notification.js";
|
|
27
|
+
export * from "./Screen.js";
|
|
26
28
|
export * from "./Popover.js";
|
|
27
29
|
export * from "./Progress.js";
|
|
28
30
|
export * from "./RadioGroup.js";
|
|
@@ -27,6 +27,31 @@ export type TextInputSurfaceResponderProps = Pick<ViewProps, "onStartShouldSetRe
|
|
|
27
27
|
* for that.
|
|
28
28
|
*/
|
|
29
29
|
export declare function useTextInputSurfaceResponder(focus: () => void): TextInputSurfaceResponderProps;
|
|
30
|
+
export type AncestorClaimWarningProps = Pick<ViewProps, "onStartShouldSetResponderCapture" | "onTouchStart">;
|
|
31
|
+
/** Test hook: let the once-per-session ancestor-claim warning fire again. */
|
|
32
|
+
export declare function resetAncestorClaimWarningForTests(): void;
|
|
33
|
+
/**
|
|
34
|
+
* Dev-only diagnostic for a boundary whose subtree is drawn in another native
|
|
35
|
+
* window but still lives in the screen's React tree (`BottomSheet.Content`).
|
|
36
|
+
*
|
|
37
|
+
* React Native's responder negotiation walks the React tree, so every ancestor
|
|
38
|
+
* of the sheet takes part in the capture phase for a tap inside it, even though
|
|
39
|
+
* none of them is under the finger. An ancestor `ScrollView` left on the default
|
|
40
|
+
* `keyboardShouldPersistTaps="never"` claims the tap there as soon as a text
|
|
41
|
+
* input holds focus and RN's `Keyboard` has reported the IME, and blurs the
|
|
42
|
+
* field on release: the keyboard closes and the tapped control never fires.
|
|
43
|
+
* Nothing inside the tree can preempt a capture-phase claim, so the boundary
|
|
44
|
+
* names it instead.
|
|
45
|
+
*
|
|
46
|
+
* The signal needs no heuristics: `onStartShouldSetResponderCapture` runs on
|
|
47
|
+
* this view for every touch that reaches the negotiation without an ancestor
|
|
48
|
+
* claiming it, while `onTouchStart` bubbles regardless. A touch start with no
|
|
49
|
+
* capture call for the same native event, while a package field is focused,
|
|
50
|
+
* is exactly a tap an ancestor took. Warns once per session, Android only,
|
|
51
|
+
* `__DEV__` only; elsewhere the inner props are returned untouched. The wrapped
|
|
52
|
+
* `onTouchStart` still runs, so the boundary's own behavior is unchanged.
|
|
53
|
+
*/
|
|
54
|
+
export declare function useAncestorClaimWarning<P extends Pick<ViewProps, "onTouchStart">>(inner: P, message: string): P & AncestorClaimWarningProps;
|
|
30
55
|
export type KeyboardDismissResponderProps = Pick<ViewProps, "onStartShouldSetResponder" | "onTouchStart" | "onTouchMove" | "onTouchEnd" | "onTouchCancel">;
|
|
31
56
|
/**
|
|
32
57
|
* View props for a tap-away keyboard-dismiss boundary. Spread onto the wrapper
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
* slop; release coordinates also enforce it when move delivery was missed.
|
|
29
29
|
* Cancellation and multitouch drop the tap without rearming another finger.
|
|
30
30
|
*/
|
|
31
|
-
import { useMemo, useRef } from "react";
|
|
31
|
+
import { useLayoutEffect, useMemo, useRef } from "react";
|
|
32
32
|
import { Platform } from "react-native";
|
|
33
33
|
import { KeyboardController, useKeyboardState } from "./keyboardController";
|
|
34
34
|
import { dismissKeyboardFocusedInput, hasKeyboardFocusedInput } from "./keyboardFocusRegistry.js";
|
|
@@ -164,8 +164,12 @@ function createTapTracker() {
|
|
|
164
164
|
* for that.
|
|
165
165
|
*/
|
|
166
166
|
export function useTextInputSurfaceResponder(focus) {
|
|
167
|
+
// Latest `focus` for the memoized handlers, synced after each commit (before
|
|
168
|
+
// any touch can be dispatched) instead of written during render.
|
|
167
169
|
const focusRef = useRef(focus);
|
|
168
|
-
|
|
170
|
+
useLayoutEffect(() => {
|
|
171
|
+
focusRef.current = focus;
|
|
172
|
+
});
|
|
169
173
|
return useMemo(() => {
|
|
170
174
|
const tap = createTapTracker();
|
|
171
175
|
return {
|
|
@@ -191,6 +195,57 @@ export function useTextInputSurfaceResponder(focus) {
|
|
|
191
195
|
};
|
|
192
196
|
}, []);
|
|
193
197
|
}
|
|
198
|
+
let warnedAncestorClaim = false;
|
|
199
|
+
/** Test hook: let the once-per-session ancestor-claim warning fire again. */
|
|
200
|
+
export function resetAncestorClaimWarningForTests() {
|
|
201
|
+
warnedAncestorClaim = false;
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Dev-only diagnostic for a boundary whose subtree is drawn in another native
|
|
205
|
+
* window but still lives in the screen's React tree (`BottomSheet.Content`).
|
|
206
|
+
*
|
|
207
|
+
* React Native's responder negotiation walks the React tree, so every ancestor
|
|
208
|
+
* of the sheet takes part in the capture phase for a tap inside it, even though
|
|
209
|
+
* none of them is under the finger. An ancestor `ScrollView` left on the default
|
|
210
|
+
* `keyboardShouldPersistTaps="never"` claims the tap there as soon as a text
|
|
211
|
+
* input holds focus and RN's `Keyboard` has reported the IME, and blurs the
|
|
212
|
+
* field on release: the keyboard closes and the tapped control never fires.
|
|
213
|
+
* Nothing inside the tree can preempt a capture-phase claim, so the boundary
|
|
214
|
+
* names it instead.
|
|
215
|
+
*
|
|
216
|
+
* The signal needs no heuristics: `onStartShouldSetResponderCapture` runs on
|
|
217
|
+
* this view for every touch that reaches the negotiation without an ancestor
|
|
218
|
+
* claiming it, while `onTouchStart` bubbles regardless. A touch start with no
|
|
219
|
+
* capture call for the same native event, while a package field is focused,
|
|
220
|
+
* is exactly a tap an ancestor took. Warns once per session, Android only,
|
|
221
|
+
* `__DEV__` only; elsewhere the inner props are returned untouched. The wrapped
|
|
222
|
+
* `onTouchStart` still runs, so the boundary's own behavior is unchanged.
|
|
223
|
+
*/
|
|
224
|
+
export function useAncestorClaimWarning(inner, message) {
|
|
225
|
+
const messageRef = useRef(message);
|
|
226
|
+
useLayoutEffect(() => {
|
|
227
|
+
messageRef.current = message;
|
|
228
|
+
});
|
|
229
|
+
return useMemo(() => {
|
|
230
|
+
if (!__DEV__ || Platform.OS !== "android")
|
|
231
|
+
return inner;
|
|
232
|
+
let captured = null;
|
|
233
|
+
return {
|
|
234
|
+
...inner,
|
|
235
|
+
onStartShouldSetResponderCapture: (event) => {
|
|
236
|
+
captured = keyOf(event);
|
|
237
|
+
return false;
|
|
238
|
+
},
|
|
239
|
+
onTouchStart: (event) => {
|
|
240
|
+
if (!warnedAncestorClaim && !isSameEvent(captured, event) && hasKeyboardFocusedInput()) {
|
|
241
|
+
warnedAncestorClaim = true;
|
|
242
|
+
console.warn(messageRef.current);
|
|
243
|
+
}
|
|
244
|
+
inner.onTouchStart?.(event);
|
|
245
|
+
},
|
|
246
|
+
};
|
|
247
|
+
}, [inner]);
|
|
248
|
+
}
|
|
194
249
|
/**
|
|
195
250
|
* View props for a tap-away keyboard-dismiss boundary. Spread onto the wrapper
|
|
196
251
|
* `View` that encloses the screen (or a form). Nesting is safe: the innermost
|
|
@@ -200,7 +255,9 @@ export function useTextInputSurfaceResponder(focus) {
|
|
|
200
255
|
export function useKeyboardDismissResponder() {
|
|
201
256
|
const isVisible = useKeyboardState((state) => state.isVisible);
|
|
202
257
|
const isVisibleRef = useRef(isVisible);
|
|
203
|
-
|
|
258
|
+
useLayoutEffect(() => {
|
|
259
|
+
isVisibleRef.current = isVisible;
|
|
260
|
+
});
|
|
204
261
|
return useMemo(() => {
|
|
205
262
|
if (Platform.OS === "web")
|
|
206
263
|
return {};
|
|
@@ -34,7 +34,7 @@ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
|
34
34
|
* Only `@expo/ui/jetpack-compose` and `@expo/ui/jetpack-compose/modifiers` are
|
|
35
35
|
* imported, both in the peer's export map since SDK 56.
|
|
36
36
|
*/
|
|
37
|
-
import { useImperativeHandle, useRef } from "react";
|
|
37
|
+
import { useImperativeHandle, useRef, useState } from "react";
|
|
38
38
|
import { BasicTextField, Box, Text, useNativeState, } from "@expo/ui/jetpack-compose";
|
|
39
39
|
import { fillMaxWidth, onSizeChanged, padding, paddingAll, semantics, testID as testIDModifier, } from "@expo/ui/jetpack-compose/modifiers";
|
|
40
40
|
// region Prop mapping (ported verbatim from @expo/ui's TextInput/utils.ts)
|
|
@@ -145,8 +145,10 @@ export function NativeTextField({ ref, value, onChangeText, placeholder, autoFoc
|
|
|
145
145
|
const numberOfLines = numberOfLinesProp ?? rows;
|
|
146
146
|
const keyboardType = keyboardTypeProp ?? inputModeToKeyboardType(inputMode);
|
|
147
147
|
const returnKeyType = returnKeyTypeProp ?? enterKeyHintToReturnKeyType(enterKeyHint);
|
|
148
|
-
|
|
149
|
-
|
|
148
|
+
// Pinned to the first render's `defaultValue`, as an uncontrolled default
|
|
149
|
+
// is, whatever `useNativeState` does with a later argument.
|
|
150
|
+
const [initialFallback] = useState(defaultValue ?? "");
|
|
151
|
+
const fallback = useNativeState(initialFallback);
|
|
150
152
|
const state = (value ?? fallback);
|
|
151
153
|
const innerRef = useRef(null);
|
|
152
154
|
const isFocusedRef = useRef(false);
|
|
@@ -176,12 +178,14 @@ export function NativeTextField({ ref, value, onChangeText, placeholder, autoFoc
|
|
|
176
178
|
// Sticky for the life of the mounted field: revealing a password
|
|
177
179
|
// (`secureTextEntry` -> false on the same element) must keep the password
|
|
178
180
|
// keyboard options unchanged so the IME neither restarts nor starts
|
|
179
|
-
// suggesting/learning the now-visible text.
|
|
180
|
-
//
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
181
|
+
// suggesting/learning the now-visible text. The latch is state adjusted
|
|
182
|
+
// during render (React re-runs this render before committing it), and the
|
|
183
|
+
// `||` makes the very render that turns secure on already read as a password
|
|
184
|
+
// field; a fresh mount starts from the prop.
|
|
185
|
+
const [everSecure, setEverSecure] = useState(!!secureTextEntry);
|
|
186
|
+
if (secureTextEntry && !everSecure)
|
|
187
|
+
setEverSecure(true);
|
|
188
|
+
const passwordField = everSecure || !!secureTextEntry;
|
|
185
189
|
const mappedKeyboardType = keyboardType ? mapKeyboardType(keyboardType) : undefined;
|
|
186
190
|
const resolvedKeyboardType = passwordField
|
|
187
191
|
? passwordKeyboardType(mappedKeyboardType)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { navigationFonts } from "./fonts";
|
|
1
|
+
import { navigationFonts } from "./fonts.js";
|
|
2
2
|
/**
|
|
3
3
|
* Raw color palette - use semantic colors in components instead
|
|
4
4
|
*
|
|
@@ -38,10 +38,36 @@ declare const palette: {
|
|
|
38
38
|
readonly red500: "#EF4444";
|
|
39
39
|
readonly red400: "#F87171";
|
|
40
40
|
};
|
|
41
|
+
/**
|
|
42
|
+
* Extra color tokens an app declares for itself, so it stops keeping a side
|
|
43
|
+
* palette. Empty here; an app augments it once and the keys become part of
|
|
44
|
+
* `ThemeColors` everywhere — `setColors`, `ThemeColorScope`,
|
|
45
|
+
* `useTheme().theme.colors`, `getThemeCssVariables`:
|
|
46
|
+
*
|
|
47
|
+
* ```ts
|
|
48
|
+
* declare module "@mrmeg/expo-ui/constants" {
|
|
49
|
+
* interface ThemeColorExtensions {
|
|
50
|
+
* brandGold: string;
|
|
51
|
+
* }
|
|
52
|
+
* }
|
|
53
|
+
* useThemeStore.getState().setColors({
|
|
54
|
+
* light: { brandGold: "#c9a227" },
|
|
55
|
+
* dark: { brandGold: "#ffe066" },
|
|
56
|
+
* });
|
|
57
|
+
* ```
|
|
58
|
+
*
|
|
59
|
+
* The package ships no value for an extension key: provide it in both schemes
|
|
60
|
+
* or the token is `undefined` at runtime in the scheme that lacks it. On web
|
|
61
|
+
* the built-in tokens are `var(--c-*)` references and extension values are
|
|
62
|
+
* literals per scheme, so with any override `theme.colors` is a new object
|
|
63
|
+
* when the scheme changes (the identity caveat every override already has).
|
|
64
|
+
*/
|
|
65
|
+
export interface ThemeColorExtensions {
|
|
66
|
+
}
|
|
41
67
|
/**
|
|
42
68
|
* Semantic color interface - what colors mean in the UI
|
|
43
69
|
*/
|
|
44
|
-
export interface ThemeColors {
|
|
70
|
+
export interface ThemeColors extends ThemeColorExtensions {
|
|
45
71
|
surfaceSunken: string;
|
|
46
72
|
background: string;
|
|
47
73
|
foreground: string;
|
|
@@ -86,14 +112,21 @@ export interface Colors {
|
|
|
86
112
|
light: Theme;
|
|
87
113
|
dark: Theme;
|
|
88
114
|
}
|
|
115
|
+
/**
|
|
116
|
+
* The package's own tokens: `ThemeColors` minus whatever an app added through
|
|
117
|
+
* `ThemeColorExtensions`. The default palettes, the CSS-variable table and
|
|
118
|
+
* `resolveRawColor` are typed on this, because the package has no value for an
|
|
119
|
+
* extension key.
|
|
120
|
+
*/
|
|
121
|
+
export type ThemeColorTokens = Omit<ThemeColors, keyof ThemeColorExtensions>;
|
|
89
122
|
/**
|
|
90
123
|
* Raw hex/rgba theme colors, always literal strings on every platform. Use
|
|
91
124
|
* these for sinks that cannot take CSS `var()` — e.g. `<meta name="theme-color">`,
|
|
92
125
|
* color parsing/math — and for generating the CSS variable definitions below.
|
|
93
126
|
*/
|
|
94
127
|
export declare const rawThemeColors: {
|
|
95
|
-
light:
|
|
96
|
-
dark:
|
|
128
|
+
light: ThemeColorTokens;
|
|
129
|
+
dark: ThemeColorTokens;
|
|
97
130
|
};
|
|
98
131
|
/**
|
|
99
132
|
* Resolves a theme color value to a literal color string. On web, theme
|
|
@@ -114,7 +147,9 @@ export declare function resolveRawColor(color: string, scheme: "light" | "dark")
|
|
|
114
147
|
* paint before any theme script stamps `data-theme`.
|
|
115
148
|
*
|
|
116
149
|
* Apps that override brand colors can pass those overrides so the emitted
|
|
117
|
-
* CSS matches their theme.
|
|
150
|
+
* CSS matches their theme. Extension tokens (`ThemeColorExtensions`) present
|
|
151
|
+
* in the overrides get a variable too, per scheme that names them; the
|
|
152
|
+
* package has no default to fall back to for those.
|
|
118
153
|
*/
|
|
119
154
|
export declare function getThemeCssVariables(overrides?: {
|
|
120
155
|
light?: Partial<ThemeColors>;
|
package/dist/constants/colors.js
CHANGED
|
@@ -139,6 +139,7 @@ const themeColorTokens = Object.keys(lightTheme.colors);
|
|
|
139
139
|
function cssVarName(token) {
|
|
140
140
|
return `--c-${token.replace(/[A-Z]/g, (c) => `-${c.toLowerCase()}`)}`;
|
|
141
141
|
}
|
|
142
|
+
const themeColorTokenSet = new Set(themeColorTokens);
|
|
142
143
|
// Reverse lookup: "var(--c-surface-sunken)" → "surfaceSunken"
|
|
143
144
|
const varToToken = new Map(themeColorTokens.map((token) => [`var(${cssVarName(token)})`, token]));
|
|
144
145
|
/**
|
|
@@ -176,17 +177,27 @@ function toRgbTriplet(value) {
|
|
|
176
177
|
* paint before any theme script stamps `data-theme`.
|
|
177
178
|
*
|
|
178
179
|
* Apps that override brand colors can pass those overrides so the emitted
|
|
179
|
-
* CSS matches their theme.
|
|
180
|
+
* CSS matches their theme. Extension tokens (`ThemeColorExtensions`) present
|
|
181
|
+
* in the overrides get a variable too, per scheme that names them; the
|
|
182
|
+
* package has no default to fall back to for those.
|
|
180
183
|
*/
|
|
181
184
|
export function getThemeCssVariables(overrides) {
|
|
182
|
-
const
|
|
183
|
-
.
|
|
184
|
-
|
|
185
|
+
const extensionTokens = [
|
|
186
|
+
...Object.keys(overrides?.light ?? {}),
|
|
187
|
+
...Object.keys(overrides?.dark ?? {}),
|
|
188
|
+
].filter((token, index, all) => !themeColorTokenSet.has(token) && all.indexOf(token) === index);
|
|
189
|
+
const declaration = (token, value) => {
|
|
185
190
|
const name = cssVarName(token);
|
|
186
191
|
const rgb = toRgbTriplet(value);
|
|
187
192
|
return `${name}: ${value};${rgb ? ` ${name}-rgb: ${rgb};` : ""}`;
|
|
188
|
-
}
|
|
189
|
-
|
|
193
|
+
};
|
|
194
|
+
const declarations = (scheme) => [
|
|
195
|
+
...themeColorTokens.map((token) => declaration(token, overrides?.[scheme]?.[token] ?? rawThemeColors[scheme][token])),
|
|
196
|
+
...extensionTokens.flatMap((token) => {
|
|
197
|
+
const value = overrides?.[scheme]?.[token];
|
|
198
|
+
return typeof value === "string" ? [declaration(token, value)] : [];
|
|
199
|
+
}),
|
|
200
|
+
].join("\n ");
|
|
190
201
|
return `
|
|
191
202
|
:root {
|
|
192
203
|
${declarations("light")}
|
|
@@ -209,10 +220,17 @@ export function getThemeCssVariables(overrides) {
|
|
|
209
220
|
// values (no CSS engine). The `navigation` maps stay literal on both
|
|
210
221
|
// platforms: @react-navigation internals color-parse their theme values.
|
|
211
222
|
const webVarColors = Object.fromEntries(themeColorTokens.map((token) => [token, `var(${cssVarName(token)})`]));
|
|
223
|
+
// Web: both schemes hold the same `var(--c-*)` references, so they share ONE
|
|
224
|
+
// `colors` object. A scheme switch then keeps `theme.colors` identical, so
|
|
225
|
+
// anything memoized on it keeps its result; the CSS variables do the
|
|
226
|
+
// re-theming. `colors.light` and `colors.dark` stay distinct objects:
|
|
227
|
+
// `dark`, `navigation`, and `fonts` differ per scheme.
|
|
228
|
+
// The base themes carry the package tokens only; extension keys (typed on
|
|
229
|
+
// `ThemeColors` once an app augments it) arrive through `setColors`.
|
|
212
230
|
export const colors = Platform.OS === "web"
|
|
213
231
|
? {
|
|
214
|
-
light: { ...lightTheme, colors:
|
|
215
|
-
dark: { ...darkTheme, colors:
|
|
232
|
+
light: { ...lightTheme, colors: webVarColors },
|
|
233
|
+
dark: { ...darkTheme, colors: webVarColors },
|
|
216
234
|
}
|
|
217
235
|
: {
|
|
218
236
|
light: lightTheme,
|
|
@@ -34,8 +34,39 @@ export type FontFamilyMap = Record<FontVariant, Record<FontFamilyWeight, string>
|
|
|
34
34
|
* A per-variant override group. Partial per weight: missing weights fall back
|
|
35
35
|
* to the group's own `regular` (not the package default), so an app that
|
|
36
36
|
* registers only Regular + Medium never leaks a package face into its brand.
|
|
37
|
+
*
|
|
38
|
+
* `italic` maps weights to real italic faces. An italic weight that is missing
|
|
39
|
+
* falls back to the group's italic `regular`; a group with no `italic` map
|
|
40
|
+
* renders italic as `fontStyle: "italic"` on the upright face (the OS or
|
|
41
|
+
* browser synthesizes the slant).
|
|
42
|
+
*/
|
|
43
|
+
export type FontFamilyOverride = Partial<Record<FontFamilyWeight, string>> & {
|
|
44
|
+
italic?: Partial<Record<FontFamilyWeight, string>>;
|
|
45
|
+
};
|
|
46
|
+
/**
|
|
47
|
+
* The package's serif. `"georgia"` (default) is the single system face on
|
|
48
|
+
* every platform; `"newsreader"` is a real serif family — four weights and a
|
|
49
|
+
* 400 italic — that `useResources({ serif: "newsreader" })` loads and turns on.
|
|
50
|
+
* Nothing about Newsreader ships to an app that keeps the default.
|
|
37
51
|
*/
|
|
38
|
-
export type
|
|
52
|
+
export type SerifPreset = "georgia" | "newsreader";
|
|
53
|
+
/**
|
|
54
|
+
* The Newsreader faces the `"newsreader"` preset resolves to. Native names
|
|
55
|
+
* match `@expo-google-fonts/newsreader`'s per-weight exports, which the app
|
|
56
|
+
* registers through `useResources({ serif: "newsreader", serifFonts })`; web
|
|
57
|
+
* is one stack the injected Google Fonts stylesheet serves, weight numeric.
|
|
58
|
+
*/
|
|
59
|
+
export declare const newsreaderFamilies: {
|
|
60
|
+
readonly web: "\"Newsreader\", Georgia, 'Times New Roman', serif";
|
|
61
|
+
readonly native: {
|
|
62
|
+
light: string;
|
|
63
|
+
regular: string;
|
|
64
|
+
medium: string;
|
|
65
|
+
semibold: string;
|
|
66
|
+
bold: string;
|
|
67
|
+
};
|
|
68
|
+
readonly nativeItalic: "Newsreader_400Regular_Italic";
|
|
69
|
+
};
|
|
39
70
|
/**
|
|
40
71
|
* Font overrides a host app can inject to brand the package.
|
|
41
72
|
*
|
|
@@ -65,6 +96,14 @@ export type ResolvedFontStyle = {
|
|
|
65
96
|
fontFamily: string;
|
|
66
97
|
/** Present only under the `"numeric"` weight strategy (web). */
|
|
67
98
|
fontWeight?: "400" | "500" | "600" | "700";
|
|
99
|
+
/** Present only when italic was asked for and no italic face exists. */
|
|
100
|
+
fontStyle?: "italic";
|
|
101
|
+
};
|
|
102
|
+
export type FontStyleOptions = {
|
|
103
|
+
/** Render italic: a real italic face when one exists, else a synthesized slant. */
|
|
104
|
+
italic?: boolean;
|
|
105
|
+
/** The active serif preset (`useThemeStore().serifPreset`); defaults to Georgia. */
|
|
106
|
+
serifPreset?: SerifPreset;
|
|
68
107
|
};
|
|
69
108
|
/**
|
|
70
109
|
* Resolve the `fontFamily` (+ optional numeric `fontWeight`) for a variant and
|
|
@@ -82,7 +121,7 @@ export type ResolvedFontStyle = {
|
|
|
82
121
|
* own family per weight on web too, and must declare
|
|
83
122
|
* `webWeightStrategy: "family"` to avoid a second synthesised layer of bold.
|
|
84
123
|
*/
|
|
85
|
-
export declare function resolveFontStyle(overrides: FontOverrides, variant: FontVariant, weight: FontFamilyWeight): ResolvedFontStyle;
|
|
124
|
+
export declare function resolveFontStyle(overrides: FontOverrides, variant: FontVariant, weight: FontFamilyWeight, options?: FontStyleOptions): ResolvedFontStyle;
|
|
86
125
|
/**
|
|
87
126
|
* Typography scale — Tailwind-matching sizes for consistent text hierarchy.
|
|
88
127
|
* Components can progressively adopt these tokens.
|
package/dist/constants/fonts.js
CHANGED
|
@@ -79,6 +79,23 @@ const sansSerifFamilies = isWebRuntime
|
|
|
79
79
|
semibold: "sans-serif",
|
|
80
80
|
bold: "sans-serif",
|
|
81
81
|
};
|
|
82
|
+
/**
|
|
83
|
+
* The Newsreader faces the `"newsreader"` preset resolves to. Native names
|
|
84
|
+
* match `@expo-google-fonts/newsreader`'s per-weight exports, which the app
|
|
85
|
+
* registers through `useResources({ serif: "newsreader", serifFonts })`; web
|
|
86
|
+
* is one stack the injected Google Fonts stylesheet serves, weight numeric.
|
|
87
|
+
*/
|
|
88
|
+
export const newsreaderFamilies = {
|
|
89
|
+
web: "\"Newsreader\", Georgia, 'Times New Roman', serif",
|
|
90
|
+
native: {
|
|
91
|
+
light: "Newsreader_400Regular",
|
|
92
|
+
regular: "Newsreader_400Regular",
|
|
93
|
+
medium: "Newsreader_500Medium",
|
|
94
|
+
semibold: "Newsreader_600SemiBold",
|
|
95
|
+
bold: "Newsreader_700Bold",
|
|
96
|
+
},
|
|
97
|
+
nativeItalic: "Newsreader_400Regular_Italic",
|
|
98
|
+
};
|
|
82
99
|
/** The package's built-in families. Override per-app via `setFonts`. */
|
|
83
100
|
export const fontFamilies = {
|
|
84
101
|
serif: serifFamilies,
|
|
@@ -120,18 +137,40 @@ const WEB_FONT_WEIGHTS = {
|
|
|
120
137
|
* own family per weight on web too, and must declare
|
|
121
138
|
* `webWeightStrategy: "family"` to avoid a second synthesised layer of bold.
|
|
122
139
|
*/
|
|
123
|
-
export function resolveFontStyle(overrides, variant, weight) {
|
|
124
|
-
const
|
|
140
|
+
export function resolveFontStyle(overrides, variant, weight, options = {}) {
|
|
141
|
+
const { italic = false, serifPreset = "georgia" } = options;
|
|
142
|
+
const isWeb = Platform.OS === "web";
|
|
125
143
|
const overrideGroup = overrides.families?.[variant];
|
|
126
|
-
|
|
144
|
+
// Precedence for the upright face: app override → serif preset → package default.
|
|
145
|
+
const preset = variant === "serif" && serifPreset === "newsreader" && !overrideGroup;
|
|
146
|
+
const defaults = preset
|
|
147
|
+
? isWeb
|
|
148
|
+
? { light: newsreaderFamilies.web, regular: newsreaderFamilies.web, medium: newsreaderFamilies.web, semibold: newsreaderFamilies.web, bold: newsreaderFamilies.web }
|
|
149
|
+
: newsreaderFamilies.native
|
|
150
|
+
: fontFamilies[variant];
|
|
151
|
+
const upright = overrideGroup
|
|
127
152
|
? overrideGroup[weight] ?? overrideGroup.regular ?? defaults[weight]
|
|
128
153
|
: defaults[weight];
|
|
129
|
-
|
|
154
|
+
// A real italic face: the app's for this weight, else its italic regular,
|
|
155
|
+
// else the preset's native italic file. Web preset italics ride on
|
|
156
|
+
// `fontStyle` (the stylesheet serves the 400 italic under one family).
|
|
157
|
+
const italicFace = !italic
|
|
158
|
+
? undefined
|
|
159
|
+
: overrideGroup?.italic
|
|
160
|
+
? overrideGroup.italic[weight] ?? overrideGroup.italic.regular
|
|
161
|
+
: preset && !isWeb
|
|
162
|
+
? newsreaderFamilies.nativeItalic
|
|
163
|
+
: undefined;
|
|
164
|
+
const fontFamily = italicFace ?? upright;
|
|
165
|
+
const synthesized = italic && italicFace === undefined;
|
|
166
|
+
const strategy = isWeb
|
|
130
167
|
? overrides.webWeightStrategy ?? defaultWebWeightStrategy
|
|
131
168
|
: "family";
|
|
132
|
-
return
|
|
133
|
-
|
|
134
|
-
|
|
169
|
+
return {
|
|
170
|
+
fontFamily,
|
|
171
|
+
...(strategy === "numeric" && { fontWeight: WEB_FONT_WEIGHTS[weight] }),
|
|
172
|
+
...(synthesized && { fontStyle: "italic" }),
|
|
173
|
+
};
|
|
135
174
|
}
|
|
136
175
|
export const typography = {
|
|
137
176
|
xs: { fontSize: 12, lineHeight: 16 },
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
export * from "./colors";
|
|
2
|
-
export * from "./fonts";
|
|
3
|
-
export * from "./motion";
|
|
4
|
-
export * from "./spacing";
|
|
1
|
+
export * from "./colors.js";
|
|
2
|
+
export * from "./fonts.js";
|
|
3
|
+
export * from "./motion.js";
|
|
4
|
+
export * from "./spacing.js";
|
|
5
|
+
export * from "./interaction.js";
|
package/dist/constants/index.js
CHANGED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interaction state tokens.
|
|
3
|
+
*
|
|
4
|
+
* One opacity for "pressed", one for "disabled", and the two press scales the
|
|
5
|
+
* kit's pressables share, so a Button, a pressable Card and a row read as the
|
|
6
|
+
* same system under the finger. Components layer these after their base
|
|
7
|
+
* styles; a caller `style` still wins.
|
|
8
|
+
*/
|
|
9
|
+
export declare const interaction: {
|
|
10
|
+
/** Opacity of a filled surface while pressed. */
|
|
11
|
+
readonly pressedOpacity: 0.85;
|
|
12
|
+
/** Opacity of a disabled control and its label. */
|
|
13
|
+
readonly disabledOpacity: 0.5;
|
|
14
|
+
/** Press scale for large surfaces (Button, Card, Item). */
|
|
15
|
+
readonly pressedScale: 0.97;
|
|
16
|
+
/** Press scale for small controls (Switch, Checkbox, Toggle). */
|
|
17
|
+
readonly controlPressedScale: 0.92;
|
|
18
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Interaction state tokens.
|
|
3
|
+
*
|
|
4
|
+
* One opacity for "pressed", one for "disabled", and the two press scales the
|
|
5
|
+
* kit's pressables share, so a Button, a pressable Card and a row read as the
|
|
6
|
+
* same system under the finger. Components layer these after their base
|
|
7
|
+
* styles; a caller `style` still wins.
|
|
8
|
+
*/
|
|
9
|
+
export const interaction = {
|
|
10
|
+
/** Opacity of a filled surface while pressed. */
|
|
11
|
+
pressedOpacity: 0.85,
|
|
12
|
+
/** Opacity of a disabled control and its label. */
|
|
13
|
+
disabledOpacity: 0.5,
|
|
14
|
+
/** Press scale for large surfaces (Button, Card, Item). */
|
|
15
|
+
pressedScale: 0.97,
|
|
16
|
+
/** Press scale for small controls (Switch, Checkbox, Toggle). */
|
|
17
|
+
controlPressedScale: 0.92,
|
|
18
|
+
};
|
|
@@ -30,6 +30,7 @@ export declare const spacing: {
|
|
|
30
30
|
readonly cardPadding: 16;
|
|
31
31
|
readonly dialogPadding: 20;
|
|
32
32
|
readonly touchTarget: 44;
|
|
33
|
+
readonly minTarget: 24;
|
|
33
34
|
readonly buttonPadding: 10;
|
|
34
35
|
readonly inputPadding: 10;
|
|
35
36
|
readonly listItemSpacing: 8;
|
|
@@ -47,5 +48,5 @@ export declare const spacing: {
|
|
|
47
48
|
readonly iconLg: 32;
|
|
48
49
|
readonly iconXl: 48;
|
|
49
50
|
};
|
|
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
|
+
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, minTarget: 24, 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
52
|
export declare const space: (multiplier: number) => number;
|
|
@@ -34,6 +34,7 @@ export const spacing = {
|
|
|
34
34
|
cardPadding: 16, // Default card / stat card padding
|
|
35
35
|
dialogPadding: 20, // Default dialog / alert dialog padding
|
|
36
36
|
touchTarget: 44, // Minimum comfortable native touch target
|
|
37
|
+
minTarget: 24, // Smallest pointer target the kit draws on web (WCAG 2.5.8 minimum)
|
|
37
38
|
// Component-specific
|
|
38
39
|
buttonPadding: 10, // Default button padding
|
|
39
40
|
inputPadding: 10, // Default input padding
|
|
@@ -55,6 +56,6 @@ export const spacing = {
|
|
|
55
56
|
iconXl: 48,
|
|
56
57
|
};
|
|
57
58
|
// Export individual constants for convenience
|
|
58
|
-
export const { base, xxs, xs, sm, smd, md, mdl, lg, xl, xxl, xxxl, gutter, gutterVertical, screenPadding, sectionSpacing, rowPaddingY, rowPaddingX, rowGap, rowMinHeight, formRowMinHeight, cardPadding, dialogPadding, touchTarget, buttonPadding, inputPadding, listItemSpacing, radiusNone, radiusXs, radiusSm, radiusMd, radiusLg, radiusXl, radius2xl, radiusFull, iconXs, iconSm, iconMd, iconLg, iconXl, } = spacing;
|
|
59
|
+
export const { base, xxs, xs, sm, smd, md, mdl, lg, xl, xxl, xxxl, gutter, gutterVertical, screenPadding, sectionSpacing, rowPaddingY, rowPaddingX, rowGap, rowMinHeight, formRowMinHeight, cardPadding, dialogPadding, touchTarget, minTarget, buttonPadding, inputPadding, listItemSpacing, radiusNone, radiusXs, radiusSm, radiusMd, radiusLg, radiusXl, radius2xl, radiusFull, iconXs, iconSm, iconMd, iconLg, iconXl, } = spacing;
|
|
59
60
|
// Helper function to multiply base spacing
|
|
60
61
|
export const space = (multiplier) => spacing.base * multiplier;
|