@thewhileloop/whileui 4.2.3 → 4.4.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/README.md +1271 -663
- package/dist/agent/catalog.json +1737 -0
- package/dist/agent/llms-components.txt +2808 -0
- package/dist/agent/llms-full.txt +2998 -0
- package/dist/agent/llms.txt +152 -0
- package/dist/blocks/chat/chat.d.ts +10 -2
- package/dist/blocks/chat/chat.js +1 -1
- package/dist/blocks/chat/index.d.ts +1 -1
- package/dist/blocks/commerce/feature-gate.js +2 -2
- package/dist/blocks/commerce/metric-card.js +1 -1
- package/dist/blocks/commerce/subscription-card.d.ts +7 -0
- package/dist/blocks/commerce/usage-bar.d.ts +4 -0
- package/dist/blocks/layout/app-shell.d.ts +4 -0
- package/dist/blocks/layout/empty-state.js +1 -1
- package/dist/blocks/layout/error-state.js +1 -1
- package/dist/blocks/layout/page-skeleton.d.ts +1 -0
- package/dist/blocks/layout/pull-to-refresh-scroll-view.d.ts +1 -0
- package/dist/blocks/layout/screen-skeleton.d.ts +5 -0
- package/dist/blocks/layout/smart-input.js +2 -2
- package/dist/blocks/lists/list-item.js +1 -1
- package/dist/blocks/lists/notification-item.js +1 -1
- package/dist/blocks/media/smart-image.js +4 -2
- package/dist/blocks/navigation/bottom-nav.js +1 -1
- package/dist/blocks/navigation/drawer-menu.js +1 -1
- package/dist/blocks/navigation/floating-bottom-nav.js +3 -1
- package/dist/blocks/navigation/tab-bar.js +1 -1
- package/dist/components/alert/alert.d.ts +7 -0
- package/dist/components/alert/alert.js +23 -4
- package/dist/components/button/button.d.ts +3 -0
- package/dist/components/button/button.js +2 -1
- package/dist/components/fab/fab.d.ts +12 -0
- package/dist/components/fab/fab.js +14 -2
- package/dist/components/fab/index.d.ts +1 -1
- package/dist/components/fab/index.js +1 -1
- package/dist/components/form-field/form-field.d.ts +18 -0
- package/dist/components/form-field/form-field.js +25 -4
- package/dist/components/input/input.d.ts +5 -0
- package/dist/components/input/input.js +4 -4
- package/dist/components/numeric-input/numeric-input.d.ts +3 -0
- package/dist/components/numeric-input/numeric-input.js +4 -4
- package/dist/components/otp-input/otp-input.d.ts +8 -0
- package/dist/components/popover/popover.js +9 -1
- package/dist/components/pressable/pressable.d.ts +1 -0
- package/dist/components/segmented-control/segmented-control.d.ts +39 -1
- package/dist/components/segmented-control/segmented-control.js +29 -5
- package/dist/components/select/select.js +7 -1
- package/dist/components/surface/surface.d.ts +5 -0
- package/dist/components/textarea/textarea.js +3 -3
- package/dist/components/toast/toast.d.ts +10 -10
- package/dist/components/toast/toast.js +53 -35
- package/dist/index.d.ts +2 -1
- package/dist/index.js +2 -1
- package/dist/lib/dismiss-keyboard-on-open.d.ts +16 -0
- package/dist/lib/dismiss-keyboard-on-open.js +22 -0
- package/dist/lib/frosted-surface.d.ts +3 -0
- package/dist/lib/recipes.d.ts +20 -0
- package/dist/lib/recipes.js +19 -2
- package/dist/lib/theme-bridge.d.ts +9 -0
- package/dist/lib/theme-bridge.js +3 -1
- package/dist/lib/theme-contract.js +12 -12
- package/dist/lib/theme-presets.js +9 -9
- package/dist/lib/theme-seeds.d.ts +40 -0
- package/dist/lib/theme-seeds.js +143 -0
- package/dist/metro.cjs +5 -4
- package/dist/tokens.d.ts +1 -0
- package/dist/tokens.js +1 -0
- package/package.json +3 -2
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { createContext, useCallback, useContext, useEffect, useMemo, useState } from 'react';
|
|
3
|
-
import { Pressable, View } from 'react-native';
|
|
4
|
-
import Animated, {
|
|
3
|
+
import { AccessibilityInfo, Platform, Pressable, View } from 'react-native';
|
|
4
|
+
import Animated, { FadeOut, SlideInUp } from 'react-native-reanimated';
|
|
5
5
|
import { Text } from '../text';
|
|
6
6
|
import { cn } from '../../lib/cn';
|
|
7
|
+
import { focusRing } from '../../lib/focus-ring';
|
|
8
|
+
import { useMotionPolicy } from '../../lib/motion-policy';
|
|
7
9
|
import { tv } from '../../lib/tv';
|
|
8
10
|
import { useVisualTokens } from '../../lib/visual-tokens';
|
|
9
11
|
import { useThemeColors } from '../../lib/theme-colors';
|
|
@@ -17,15 +19,11 @@ export function useToast() {
|
|
|
17
19
|
return context;
|
|
18
20
|
}
|
|
19
21
|
let toastId = 0;
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
const DESCRIPTION_COLOR_BY_VARIANT = {
|
|
26
|
-
default: 'text-muted-foreground',
|
|
27
|
-
destructive: 'text-destructive-foreground-muted',
|
|
28
|
-
success: 'text-success-foreground-muted',
|
|
22
|
+
/** Status variants carry a small dot, not a fill, so every toast sits on the theme's surface. */
|
|
23
|
+
const ACCENT_BY_VARIANT = {
|
|
24
|
+
default: undefined,
|
|
25
|
+
destructive: 'bg-destructive',
|
|
26
|
+
success: 'bg-success',
|
|
29
27
|
};
|
|
30
28
|
export function ToastProvider({ children }) {
|
|
31
29
|
const [toasts, setToasts] = useState([]);
|
|
@@ -44,12 +42,12 @@ export function ToastProvider({ children }) {
|
|
|
44
42
|
return _jsx(ToastContext.Provider, { value: contextValue, children: children });
|
|
45
43
|
}
|
|
46
44
|
const toastVariants = tv({
|
|
47
|
-
base: 'w-full flex-row items-center
|
|
45
|
+
base: 'w-full flex-row items-center gap-3 border bg-popover',
|
|
48
46
|
variants: {
|
|
49
47
|
variant: {
|
|
50
|
-
default: 'border-border
|
|
51
|
-
destructive: 'border-destructive
|
|
52
|
-
success: 'border-success
|
|
48
|
+
default: 'border-border',
|
|
49
|
+
destructive: 'border-destructive-soft-border',
|
|
50
|
+
success: 'border-success-soft-border',
|
|
53
51
|
},
|
|
54
52
|
},
|
|
55
53
|
defaultVariants: {
|
|
@@ -60,31 +58,51 @@ function Toast({ toast: toastData, onDismiss, className, style, ...props }) {
|
|
|
60
58
|
const { title, description, variant = 'default', duration = 4000, action } = toastData;
|
|
61
59
|
const visual = useVisualTokens();
|
|
62
60
|
const colors = useThemeColors();
|
|
61
|
+
const motion = useMotionPolicy();
|
|
62
|
+
const accent = ACCENT_BY_VARIANT[variant];
|
|
63
63
|
useEffect(() => {
|
|
64
64
|
if (duration > 0) {
|
|
65
65
|
const timer = setTimeout(onDismiss, duration);
|
|
66
66
|
return () => clearTimeout(timer);
|
|
67
67
|
}
|
|
68
68
|
}, [duration, onDismiss]);
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
69
|
+
// Android and the web announce through the live region below; iOS has no live
|
|
70
|
+
// regions, so it is told once, when the toast appears.
|
|
71
|
+
useEffect(() => {
|
|
72
|
+
if (Platform.OS !== 'ios')
|
|
73
|
+
return;
|
|
74
|
+
const message = [title, description].filter(Boolean).join('. ');
|
|
75
|
+
if (message)
|
|
76
|
+
AccessibilityInfo.announceForAccessibility(message);
|
|
77
|
+
// Announce on appearance only; a re-render must not repeat it.
|
|
78
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps -- once per toast, by design
|
|
79
|
+
}, []);
|
|
80
|
+
return (_jsx(Animated.View, { entering: motion.animationsEnabled ? SlideInUp.duration(motion.durations.normal) : undefined, exiting: motion.animationsEnabled ? FadeOut.duration(motion.durations.fast) : undefined, children: _jsxs(View
|
|
81
|
+
// Announced where it appears, without moving focus. Set before the spread so a
|
|
82
|
+
// caller can still override.
|
|
83
|
+
, {
|
|
84
|
+
// Announced where it appears, without moving focus. Set before the spread so a
|
|
85
|
+
// caller can still override.
|
|
86
|
+
accessibilityRole: "alert", accessibilityLiveRegion: "polite", className: cn(toastVariants({ variant }), className), style: [
|
|
87
|
+
{
|
|
88
|
+
borderRadius: surfaceRadius(visual, 'lg'),
|
|
89
|
+
borderWidth: visual.borderWidthHairline,
|
|
90
|
+
padding: surfacePadding(visual, 'sm'),
|
|
91
|
+
},
|
|
92
|
+
shadowStyle(visual, colors, 'lg'),
|
|
93
|
+
style,
|
|
94
|
+
], ...props, children: [_jsxs(View, { className: "flex-1 gap-0.5", children: [title ? (_jsxs(View, { className: "flex-row items-center gap-2", children: [accent ? _jsx(View, { className: cn('h-2 w-2 rounded-full', accent) }) : null, _jsx(Text, { className: "font-semibold text-popover-foreground", style: typographyStyle(visual, 'label'), children: title })] })) : null, description ? (_jsx(Text, { className: "text-muted-foreground", style: typographyStyle(visual, 'label'), children: description })) : null] }), action ? (_jsx(Pressable, { accessibilityRole: "button", onPress: () => {
|
|
95
|
+
action.onPress();
|
|
96
|
+
onDismiss();
|
|
97
|
+
}, className: cn('border border-border active:bg-state-pressed', focusRing), style: {
|
|
98
|
+
borderRadius: visual.radiusMd,
|
|
99
|
+
borderWidth: visual.borderWidthControl,
|
|
100
|
+
paddingHorizontal: visual.controlPaddingXSm,
|
|
101
|
+
paddingVertical: visual.controlPaddingYSm,
|
|
102
|
+
}, children: _jsx(Text, { className: "font-medium text-popover-foreground", style: typographyStyle(visual, 'label'), children: action.label }) })) : null, _jsx(Pressable, { accessibilityRole: "button", accessibilityLabel: "Dismiss", onPress: onDismiss,
|
|
103
|
+
// 44 px everywhere, since hitSlop does nothing on the web; the negative margin
|
|
104
|
+
// keeps the toast as compact as the old 32 px button left it.
|
|
105
|
+
hitSlop: 2, className: cn('-my-1.5 -mr-1.5 h-11 w-11 items-center justify-center rounded-full active:bg-state-pressed', focusRing), children: _jsx(Text, { className: "text-muted-foreground", style: typographyStyle(visual, 'emphasis'), children: "\u00D7" }) })] }) }));
|
|
88
106
|
}
|
|
89
107
|
function ToastContainerItem({ toast, dismiss, }) {
|
|
90
108
|
const handleDismiss = useCallback(() => dismiss(toast.id), [dismiss, toast.id]);
|
|
@@ -94,6 +112,6 @@ export function ToastContainer({ position = 'top', className, style, ...props })
|
|
|
94
112
|
const { toasts, dismiss } = useToast();
|
|
95
113
|
const visual = useVisualTokens();
|
|
96
114
|
const positionClass = position === 'top' ? 'top-0' : 'bottom-0';
|
|
97
|
-
return (_jsx(View, { className: cn('absolute left-0 right-0 z-50 py-2', positionClass, className), style: [{ paddingHorizontal: visual.surfacePaddingSm }, style], pointerEvents: "box-none", ...props, children: toasts.map((toast) => (_jsx(ToastContainerItem, { toast: toast, dismiss: dismiss }, toast.id))) }));
|
|
115
|
+
return (_jsx(View, { className: cn('absolute left-0 right-0 z-50 py-2', positionClass, className), style: [{ paddingHorizontal: visual.surfacePaddingSm }, style], pointerEvents: "box-none", ...props, children: _jsx(View, { className: "w-full max-w-md self-center", pointerEvents: "box-none", children: toasts.map((toast) => (_jsx(ToastContainerItem, { toast: toast, dismiss: dismiss }, toast.id))) }) }));
|
|
98
116
|
}
|
|
99
117
|
export { Toast, toastVariants };
|
package/dist/index.d.ts
CHANGED
|
@@ -15,6 +15,7 @@ export { registerFrostedBlurView, type FrostedSurfaceProps, type FrostedTintToke
|
|
|
15
15
|
export { controlRecipe, fieldRecipe, surfaceRecipe, surfacePadding, surfaceRadius, shadowStyle, typographyStyle, useControlRecipe, useFieldRecipe, useSurfaceRecipe, useShadowStyle, useTypography, type ControlSize, type SurfacePaddingSize, type SurfaceRecipeOptions, type ShadowTier, type TypographyRole, } from './lib/recipes';
|
|
16
16
|
export { WHILEUI_PRESET_SOFT, WHILEUI_PRESET_ATELIER, WHILEUI_THEME_PRESETS, themePresetToCssVariables, themePresetToCss, type ThemePreset, } from './lib/theme-presets';
|
|
17
17
|
export { deriveThemeColors, THEME_BASE_TOKEN_KEYS, type ThemeBaseColors } from './lib/theme-derive';
|
|
18
|
+
export { checkThemeContrast, themeFromSeeds, type ContrastCheck, type ThemeSeeds, } from './lib/theme-seeds';
|
|
18
19
|
export { Text, textVariants, type TextProps } from './components/text';
|
|
19
20
|
export { Surface, type SurfaceLevel, type SurfaceProps } from './components/surface';
|
|
20
21
|
export { View, type ViewProps } from './components/view';
|
|
@@ -29,7 +30,7 @@ export { OTPInput, otpCellVariants, type OTPInputProps } from './components/otp-
|
|
|
29
30
|
export { FormField, FormLabel, FormControl, FormHint, FormMessage, useFormFieldState, formFieldVariants, formControlVariants, type FormFieldProps, type FormLabelProps, type FormControlProps, type FormHintProps, type FormMessageProps, type FormFieldState, } from './components/form-field';
|
|
30
31
|
export { LabeledField, LabeledFieldControl, type LabeledFieldProps, type LabeledFieldControlProps, } from './components/labeled-field';
|
|
31
32
|
export { Badge, BadgeText, badgeVariants, badgeTextVariants, type BadgeProps, type BadgeTextProps, } from './components/badge';
|
|
32
|
-
export { FAB, FABGroup, fabVariants, type FabProps, type FabGroupProps } from './components/fab';
|
|
33
|
+
export { FAB, FABGroup, fabClearance, fabVariants, type FabProps, type FabGroupProps, } from './components/fab';
|
|
33
34
|
export { Alert, AlertTitle, AlertDescription, alertVariants, type AlertProps, type AlertTitleProps, type AlertDescriptionProps, } from './components/alert';
|
|
34
35
|
export { Switch, type SwitchProps } from './components/switch';
|
|
35
36
|
export { Checkbox, type CheckboxProps } from './components/checkbox';
|
package/dist/index.js
CHANGED
|
@@ -15,6 +15,7 @@ export { registerFrostedBlurView, } from './lib/frosted-surface';
|
|
|
15
15
|
export { controlRecipe, fieldRecipe, surfaceRecipe, surfacePadding, surfaceRadius, shadowStyle, typographyStyle, useControlRecipe, useFieldRecipe, useSurfaceRecipe, useShadowStyle, useTypography, } from './lib/recipes';
|
|
16
16
|
export { WHILEUI_PRESET_SOFT, WHILEUI_PRESET_ATELIER, WHILEUI_THEME_PRESETS, themePresetToCssVariables, themePresetToCss, } from './lib/theme-presets';
|
|
17
17
|
export { deriveThemeColors, THEME_BASE_TOKEN_KEYS } from './lib/theme-derive';
|
|
18
|
+
export { checkThemeContrast, themeFromSeeds, } from './lib/theme-seeds';
|
|
18
19
|
export { Text, textVariants } from './components/text';
|
|
19
20
|
export { Surface } from './components/surface';
|
|
20
21
|
export { View } from './components/view';
|
|
@@ -29,7 +30,7 @@ export { OTPInput, otpCellVariants } from './components/otp-input';
|
|
|
29
30
|
export { FormField, FormLabel, FormControl, FormHint, FormMessage, useFormFieldState, formFieldVariants, formControlVariants, } from './components/form-field';
|
|
30
31
|
export { LabeledField, LabeledFieldControl, } from './components/labeled-field';
|
|
31
32
|
export { Badge, BadgeText, badgeVariants, badgeTextVariants, } from './components/badge';
|
|
32
|
-
export { FAB, FABGroup, fabVariants } from './components/fab';
|
|
33
|
+
export { FAB, FABGroup, fabClearance, fabVariants, } from './components/fab';
|
|
33
34
|
export { Alert, AlertTitle, AlertDescription, alertVariants, } from './components/alert';
|
|
34
35
|
export { Switch } from './components/switch';
|
|
35
36
|
export { Checkbox } from './components/checkbox';
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type RefObject } from 'react';
|
|
2
|
+
import type { LayoutPosition } from '@rn-primitives/hooks';
|
|
3
|
+
interface Measurable {
|
|
4
|
+
measure: (callback: (x: number, y: number, width: number, height: number, pageX: number, pageY: number) => void) => void;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Closes the keyboard when a portal overlay opens, then measures its trigger again.
|
|
8
|
+
*
|
|
9
|
+
* `Select` and `Popover` draw in a portal rather than a Modal, so iOS and Android leave the
|
|
10
|
+
* keyboard up and it covers the lower half of the content. Closing it moves any
|
|
11
|
+
* keyboard-avoiding screen, which leaves the trigger away from where it was measured on
|
|
12
|
+
* press, so the position is taken again once the keyboard has gone. Modal-hosted overlays
|
|
13
|
+
* need none of this: the platform closes the keyboard when the Modal's window takes focus.
|
|
14
|
+
*/
|
|
15
|
+
export declare function useDismissKeyboardOnOpen(open: boolean, triggerRef: RefObject<Measurable | null>, setTriggerPosition: (position: LayoutPosition) => void): void;
|
|
16
|
+
export {};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { useEffect } from 'react';
|
|
2
|
+
import { Keyboard, Platform } from 'react-native';
|
|
3
|
+
/**
|
|
4
|
+
* Closes the keyboard when a portal overlay opens, then measures its trigger again.
|
|
5
|
+
*
|
|
6
|
+
* `Select` and `Popover` draw in a portal rather than a Modal, so iOS and Android leave the
|
|
7
|
+
* keyboard up and it covers the lower half of the content. Closing it moves any
|
|
8
|
+
* keyboard-avoiding screen, which leaves the trigger away from where it was measured on
|
|
9
|
+
* press, so the position is taken again once the keyboard has gone. Modal-hosted overlays
|
|
10
|
+
* need none of this: the platform closes the keyboard when the Modal's window takes focus.
|
|
11
|
+
*/
|
|
12
|
+
export function useDismissKeyboardOnOpen(open, triggerRef, setTriggerPosition) {
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
if (!open || Platform.OS === 'web' || !Keyboard.isVisible())
|
|
15
|
+
return;
|
|
16
|
+
const subscription = Keyboard.addListener('keyboardDidHide', () => {
|
|
17
|
+
triggerRef.current?.measure((_x, _y, width, height, pageX, pageY) => setTriggerPosition({ width, height, pageX, pageY }));
|
|
18
|
+
});
|
|
19
|
+
Keyboard.dismiss();
|
|
20
|
+
return () => subscription.remove();
|
|
21
|
+
}, [open, triggerRef, setTriggerPosition]);
|
|
22
|
+
}
|
|
@@ -3,8 +3,11 @@ import { type ViewStyle } from 'react-native';
|
|
|
3
3
|
export type FrostedTintToken = 'surfaceElevated' | 'surfaceTranslucent' | 'card' | 'popover';
|
|
4
4
|
export type FrostedBlurPreset = 'subtle' | 'medium' | 'strong';
|
|
5
5
|
export interface FrostedSurfaceProps {
|
|
6
|
+
/** Enable the optional frosted overlay. */
|
|
6
7
|
frosted?: boolean;
|
|
8
|
+
/** Override blur amount directly. */
|
|
7
9
|
blurIntensity?: number;
|
|
10
|
+
/** Tint token used for frosted mode. */
|
|
8
11
|
blurTintToken?: FrostedTintToken;
|
|
9
12
|
}
|
|
10
13
|
interface UseFrostedSurfaceOptions extends FrostedSurfaceProps {
|
package/dist/lib/recipes.d.ts
CHANGED
|
@@ -22,6 +22,26 @@ export declare function surfacePadding(visual: VisualTokens, padding?: SurfacePa
|
|
|
22
22
|
export declare function surfaceRadius(visual: VisualTokens, radius?: 'sm' | 'md' | 'lg' | 'xl'): number;
|
|
23
23
|
export declare function surfaceRecipe(visual: VisualTokens, options?: SurfaceRecipeOptions): ViewStyle;
|
|
24
24
|
export declare function shadowStyle(visual: VisualTokens, colors: ThemeColors, tier: ShadowTier): ViewStyle;
|
|
25
|
+
/**
|
|
26
|
+
* Caret and selection colours for a text field: the theme's primary, where React Native
|
|
27
|
+
* would otherwise use the platform blue. iOS tints the caret, handles and highlight from
|
|
28
|
+
* `selectionColor` alone. Android paints `selectionColor` as the highlight at full
|
|
29
|
+
* strength, which would hide the selected text, so it gets a translucent highlight and
|
|
30
|
+
* the caret and handles take the solid colour. The web keeps the browser's caret.
|
|
31
|
+
*/
|
|
32
|
+
export declare function textCursorColors(colors: ThemeColors): {
|
|
33
|
+
cursorColor: string;
|
|
34
|
+
selectionHandleColor: string;
|
|
35
|
+
selectionColor: string;
|
|
36
|
+
} | {
|
|
37
|
+
selectionColor: string;
|
|
38
|
+
cursorColor?: undefined;
|
|
39
|
+
selectionHandleColor?: undefined;
|
|
40
|
+
} | {
|
|
41
|
+
cursorColor?: undefined;
|
|
42
|
+
selectionHandleColor?: undefined;
|
|
43
|
+
selectionColor?: undefined;
|
|
44
|
+
};
|
|
25
45
|
export declare function typographyStyle(visual: VisualTokens, role: TypographyRole): TextStyle;
|
|
26
46
|
export declare function useControlRecipe(size?: ControlSize): ViewStyle;
|
|
27
47
|
export declare function useFieldRecipe(size?: ControlSize): ViewStyle;
|
package/dist/lib/recipes.js
CHANGED
|
@@ -108,17 +108,34 @@ export function shadowStyle(visual, colors, tier) {
|
|
|
108
108
|
offsetX: 0,
|
|
109
109
|
offsetY,
|
|
110
110
|
blurRadius: radius * 2,
|
|
111
|
-
color:
|
|
111
|
+
color: colorWithOpacity(colors.shadow, opacity),
|
|
112
112
|
},
|
|
113
113
|
],
|
|
114
114
|
};
|
|
115
115
|
}
|
|
116
|
-
function
|
|
116
|
+
function colorWithOpacity(color, opacity) {
|
|
117
117
|
const parsed = parse(color);
|
|
118
118
|
if (!parsed)
|
|
119
119
|
return color;
|
|
120
120
|
return formatRgb({ ...parsed, alpha: opacity });
|
|
121
121
|
}
|
|
122
|
+
/**
|
|
123
|
+
* Caret and selection colours for a text field: the theme's primary, where React Native
|
|
124
|
+
* would otherwise use the platform blue. iOS tints the caret, handles and highlight from
|
|
125
|
+
* `selectionColor` alone. Android paints `selectionColor` as the highlight at full
|
|
126
|
+
* strength, which would hide the selected text, so it gets a translucent highlight and
|
|
127
|
+
* the caret and handles take the solid colour. The web keeps the browser's caret.
|
|
128
|
+
*/
|
|
129
|
+
export function textCursorColors(colors) {
|
|
130
|
+
if (Platform.OS === 'android') {
|
|
131
|
+
return {
|
|
132
|
+
cursorColor: colors.primary,
|
|
133
|
+
selectionHandleColor: colors.primary,
|
|
134
|
+
selectionColor: colorWithOpacity(colors.primary, 0.3),
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
return Platform.OS === 'ios' ? { selectionColor: colors.primary } : {};
|
|
138
|
+
}
|
|
122
139
|
// Component-owned text sizes. Font family/weight stays on className
|
|
123
140
|
// (font-medium etc.) so app font mappings keep working.
|
|
124
141
|
export function typographyStyle(visual, role) {
|
|
@@ -14,10 +14,19 @@ export interface UseThemeBridgeOptions {
|
|
|
14
14
|
adapter?: ThemeBridgeAdapter;
|
|
15
15
|
}
|
|
16
16
|
export interface UseThemeBridgeResult {
|
|
17
|
+
/** The chosen mode, `system` included. */
|
|
17
18
|
mode: ThemeMode;
|
|
19
|
+
/** The theme on screen: `light`, `dark`, or a registered custom theme. */
|
|
18
20
|
resolvedTheme: string;
|
|
19
21
|
setMode: (mode: ThemeMode) => void;
|
|
22
|
+
/**
|
|
23
|
+
* Steps light → dark → system. For a control that shows all three modes; a two-state
|
|
24
|
+
* sun/moon button wants `toggleTheme`, because a step into `system` that resolves to
|
|
25
|
+
* the theme already on screen looks like a tap that did nothing.
|
|
26
|
+
*/
|
|
20
27
|
cycleMode: () => void;
|
|
28
|
+
/** Switches to the opposite of the theme on screen: dark from light, light from anything else. */
|
|
29
|
+
toggleTheme: () => void;
|
|
21
30
|
}
|
|
22
31
|
export declare function applyThemeMode(mode: ThemeMode): void;
|
|
23
32
|
export declare function resolveThemeMode(adapter?: ThemeBridgeAdapter, fallback?: ThemeMode): Promise<ThemeMode>;
|
package/dist/lib/theme-bridge.js
CHANGED
|
@@ -101,12 +101,14 @@ export function useThemeBridge({ initialMode = 'system', adapter, } = {}) {
|
|
|
101
101
|
dispatch({ type: 'select', mode });
|
|
102
102
|
}, []);
|
|
103
103
|
const cycleMode = useCallback(() => dispatch({ type: 'cycle' }), []);
|
|
104
|
+
const toggleTheme = useCallback(() => dispatch({ type: 'select', mode: theme === 'light' ? 'dark' : 'light' }), [theme]);
|
|
104
105
|
return useMemo(() => ({
|
|
105
106
|
mode: state.mode,
|
|
106
107
|
resolvedTheme: theme,
|
|
107
108
|
setMode,
|
|
108
109
|
cycleMode,
|
|
109
|
-
|
|
110
|
+
toggleTheme,
|
|
111
|
+
}), [cycleMode, setMode, state.mode, theme, toggleTheme]);
|
|
110
112
|
}
|
|
111
113
|
function ControlledThemeBridge({ mode, adapter, }) {
|
|
112
114
|
useEffect(() => {
|
|
@@ -13,16 +13,16 @@ export const WHILEUI_LIGHT_THEME_COLORS = {
|
|
|
13
13
|
mutedForeground: '#645783',
|
|
14
14
|
border: 'rgba(68, 87, 255, 0.16)',
|
|
15
15
|
input: 'rgba(255, 255, 255, 0.84)',
|
|
16
|
-
ring: '
|
|
16
|
+
ring: '#3d8bff',
|
|
17
17
|
accent: '#a7f432',
|
|
18
18
|
accentForeground: '#163218',
|
|
19
|
-
destructive: '#
|
|
19
|
+
destructive: '#d03d31',
|
|
20
20
|
destructiveForeground: '#fff8f6',
|
|
21
|
-
success: '#
|
|
21
|
+
success: '#00864d',
|
|
22
22
|
successForeground: '#f5fff9',
|
|
23
23
|
warning: '#ffb224',
|
|
24
24
|
warningForeground: '#3d2200',
|
|
25
|
-
info: '#
|
|
25
|
+
info: '#2070e1',
|
|
26
26
|
infoForeground: '#f8fbff',
|
|
27
27
|
overlay: 'rgba(39, 31, 72, 0.14)',
|
|
28
28
|
overlayStrong: 'rgba(39, 31, 72, 0.22)',
|
|
@@ -37,19 +37,19 @@ export const WHILEUI_LIGHT_THEME_COLORS = {
|
|
|
37
37
|
shadow: '#271f48',
|
|
38
38
|
primaryActive: '#3346e6',
|
|
39
39
|
secondaryActive: 'rgba(255, 123, 92, 0.18)',
|
|
40
|
-
destructiveActive: '#
|
|
40
|
+
destructiveActive: '#bb261d',
|
|
41
41
|
primarySoft: 'rgba(68, 87, 255, 0.13)',
|
|
42
42
|
primarySoftSubtle: 'rgba(68, 87, 255, 0.07)',
|
|
43
43
|
secondarySoft: 'rgba(255, 123, 92, 0.16)',
|
|
44
44
|
mutedSoft: 'rgba(74, 55, 122, 0.05)',
|
|
45
|
-
destructiveSoft: 'rgba(
|
|
46
|
-
destructiveSoftBorder: 'rgba(
|
|
47
|
-
successSoft: 'rgba(
|
|
48
|
-
successSoftBorder: 'rgba(
|
|
45
|
+
destructiveSoft: 'rgba(208, 61, 49, 0.12)',
|
|
46
|
+
destructiveSoftBorder: 'rgba(208, 61, 49, 0.30)',
|
|
47
|
+
successSoft: 'rgba(0, 134, 77, 0.12)',
|
|
48
|
+
successSoftBorder: 'rgba(0, 134, 77, 0.30)',
|
|
49
49
|
warningSoft: 'rgba(255, 178, 36, 0.14)',
|
|
50
50
|
warningSoftBorder: 'rgba(255, 178, 36, 0.34)',
|
|
51
|
-
infoSoft: 'rgba(
|
|
52
|
-
infoSoftBorder: 'rgba(
|
|
51
|
+
infoSoft: 'rgba(32, 112, 225, 0.12)',
|
|
52
|
+
infoSoftBorder: 'rgba(32, 112, 225, 0.30)',
|
|
53
53
|
placeholder: '#645783',
|
|
54
54
|
};
|
|
55
55
|
export const WHILEUI_DARK_THEME_COLORS = {
|
|
@@ -67,7 +67,7 @@ export const WHILEUI_DARK_THEME_COLORS = {
|
|
|
67
67
|
mutedForeground: '#c5bad9',
|
|
68
68
|
border: 'rgba(255, 255, 255, 0.12)',
|
|
69
69
|
input: 'rgba(27, 18, 49, 0.86)',
|
|
70
|
-
ring: '
|
|
70
|
+
ring: '#7ff0c5',
|
|
71
71
|
accent: '#7ff0c5',
|
|
72
72
|
accentForeground: '#0f2d24',
|
|
73
73
|
destructive: '#ff6b57',
|
|
@@ -38,7 +38,7 @@ export const WHILEUI_PRESET_SOFT = {
|
|
|
38
38
|
};
|
|
39
39
|
// Compact geometry, hairline-plus borders, real drop shadows, warm ink.
|
|
40
40
|
const ATELIER_LIGHT = {
|
|
41
|
-
primary: '#
|
|
41
|
+
primary: '#af5028',
|
|
42
42
|
primaryForeground: '#fdf6ee',
|
|
43
43
|
foreground: '#2b2118',
|
|
44
44
|
background: '#f6efe4',
|
|
@@ -49,10 +49,10 @@ const ATELIER_LIGHT = {
|
|
|
49
49
|
secondary: 'rgba(43, 33, 24, 0.06)',
|
|
50
50
|
secondaryForeground: '#4a3a2a',
|
|
51
51
|
muted: 'rgba(43, 33, 24, 0.05)',
|
|
52
|
-
mutedForeground: '#
|
|
52
|
+
mutedForeground: '#716150',
|
|
53
53
|
border: 'rgba(43, 33, 24, 0.22)',
|
|
54
54
|
input: '#fffdf8',
|
|
55
|
-
ring: '
|
|
55
|
+
ring: '#af5028',
|
|
56
56
|
accent: '#1f6f54',
|
|
57
57
|
accentForeground: '#f0faf5',
|
|
58
58
|
destructive: '#b3261e',
|
|
@@ -70,15 +70,15 @@ const ATELIER_LIGHT = {
|
|
|
70
70
|
surfaceTranslucentBorder: 'rgba(43, 33, 24, 0.16)',
|
|
71
71
|
surfaceBorder: 'rgba(43, 33, 24, 0.22)',
|
|
72
72
|
surfaceHighlight: 'rgba(255, 255, 255, 0.65)',
|
|
73
|
-
stateHover: 'rgba(
|
|
74
|
-
statePressed: 'rgba(
|
|
73
|
+
stateHover: 'rgba(175, 80, 40, 0.07)',
|
|
74
|
+
statePressed: 'rgba(175, 80, 40, 0.14)',
|
|
75
75
|
stateDisabled: 'rgba(122, 106, 88, 0.30)',
|
|
76
76
|
shadow: '#3a2c1c',
|
|
77
77
|
primaryActive: '#96431f',
|
|
78
78
|
secondaryActive: 'rgba(43, 33, 24, 0.12)',
|
|
79
79
|
destructiveActive: '#8f1d17',
|
|
80
|
-
primarySoft: 'rgba(
|
|
81
|
-
primarySoftSubtle: 'rgba(
|
|
80
|
+
primarySoft: 'rgba(175, 80, 40, 0.12)',
|
|
81
|
+
primarySoftSubtle: 'rgba(175, 80, 40, 0.06)',
|
|
82
82
|
secondarySoft: 'rgba(31, 111, 84, 0.10)',
|
|
83
83
|
mutedSoft: 'rgba(43, 33, 24, 0.04)',
|
|
84
84
|
destructiveSoft: 'rgba(179, 38, 30, 0.10)',
|
|
@@ -89,7 +89,7 @@ const ATELIER_LIGHT = {
|
|
|
89
89
|
warningSoftBorder: 'rgba(192, 123, 18, 0.36)',
|
|
90
90
|
infoSoft: 'rgba(47, 93, 158, 0.10)',
|
|
91
91
|
infoSoftBorder: 'rgba(47, 93, 158, 0.32)',
|
|
92
|
-
placeholder: '#
|
|
92
|
+
placeholder: '#716150',
|
|
93
93
|
};
|
|
94
94
|
const ATELIER_DARK = {
|
|
95
95
|
primary: '#e8935f',
|
|
@@ -106,7 +106,7 @@ const ATELIER_DARK = {
|
|
|
106
106
|
mutedForeground: '#a3937d',
|
|
107
107
|
border: 'rgba(239, 230, 216, 0.18)',
|
|
108
108
|
input: '#211a14',
|
|
109
|
-
ring: '
|
|
109
|
+
ring: '#e8935f',
|
|
110
110
|
accent: '#6fd0a8',
|
|
111
111
|
accentForeground: '#0b2419',
|
|
112
112
|
destructive: '#e5695f',
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import type { ThemeColors } from './theme-contract';
|
|
2
|
+
import { type ThemeBaseColors } from './theme-derive';
|
|
3
|
+
import { type ThemePreset } from './theme-presets';
|
|
4
|
+
import type { VisualTokens } from './visual-token-contract';
|
|
5
|
+
export interface ThemeSeeds {
|
|
6
|
+
/** Page background. */
|
|
7
|
+
background: string;
|
|
8
|
+
/** Body text. */
|
|
9
|
+
foreground: string;
|
|
10
|
+
/** Brand colour: primary buttons, links, focus ring. */
|
|
11
|
+
primary: string;
|
|
12
|
+
/** Second voice for highlights and selected states. Derived from `primary` if left out. */
|
|
13
|
+
accent?: string;
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* A complete preset from seeds. `light` is required; `dark` defaults to a dark page with
|
|
17
|
+
* the same brand colours. `overrides` replaces any base colour before the rest is
|
|
18
|
+
* derived, so nothing is out of reach; derived tokens can be adjusted on the result.
|
|
19
|
+
*/
|
|
20
|
+
export declare function themeFromSeeds(input: {
|
|
21
|
+
name: string;
|
|
22
|
+
description: string;
|
|
23
|
+
light: ThemeSeeds;
|
|
24
|
+
dark?: ThemeSeeds;
|
|
25
|
+
overrides?: {
|
|
26
|
+
light?: Partial<ThemeBaseColors>;
|
|
27
|
+
dark?: Partial<ThemeBaseColors>;
|
|
28
|
+
};
|
|
29
|
+
visual?: Partial<VisualTokens>;
|
|
30
|
+
}): ThemePreset;
|
|
31
|
+
export interface ContrastCheck {
|
|
32
|
+
/** "foreground on background" */
|
|
33
|
+
pair: string;
|
|
34
|
+
ratio: number;
|
|
35
|
+
/** 4.5 for text, 3 for focus rings and other non-text UI (WCAG 1.4.3, 1.4.11). */
|
|
36
|
+
min: number;
|
|
37
|
+
pass: boolean;
|
|
38
|
+
}
|
|
39
|
+
/** Every text and focus-ring pairing the components render, checked against WCAG AA. */
|
|
40
|
+
export declare function checkThemeContrast(colors: ThemeColors): ContrastCheck[];
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
// A full theme from a few seed colours, plus a contrast check for any palette.
|
|
2
|
+
// Renderer-free: safe in build scripts and for agents generating themes.
|
|
3
|
+
//
|
|
4
|
+
// Seeds are never altered: a brand colour is a brand decision, so when it fails
|
|
5
|
+
// contrast the check reports it instead. Only derived colours (muted text, the text on
|
|
6
|
+
// each filled colour, status colours) are tuned to meet WCAG.
|
|
7
|
+
import { blend, converter, formatHex, parse, wcagContrast } from 'culori';
|
|
8
|
+
import { createThemePreset } from './theme-presets';
|
|
9
|
+
const toOklch = converter('oklch');
|
|
10
|
+
/** WCAG 2.x contrast ratio between two CSS colours, 1–21. */
|
|
11
|
+
function contrastRatio(a, b) {
|
|
12
|
+
return wcagContrast(a, b);
|
|
13
|
+
}
|
|
14
|
+
function oklch(color) {
|
|
15
|
+
const parsed = parse(color);
|
|
16
|
+
if (!parsed)
|
|
17
|
+
throw new Error(`Not a CSS colour: "${color}"`);
|
|
18
|
+
return toOklch(parsed);
|
|
19
|
+
}
|
|
20
|
+
const hex = (l, c, h) => formatHex({ mode: 'oklch', l: Math.min(1, Math.max(0, l)), c, h });
|
|
21
|
+
/** Mix `a` toward `b` by `t` in OKLCH lightness and chroma, keeping `a`'s hue. */
|
|
22
|
+
function toward(a, b, t) {
|
|
23
|
+
const x = oklch(a);
|
|
24
|
+
const y = oklch(b);
|
|
25
|
+
return hex(x.l + (y.l - x.l) * t, (x.c ?? 0) + ((y.c ?? 0) - (x.c ?? 0)) * t, x.h ?? y.h ?? 0);
|
|
26
|
+
}
|
|
27
|
+
/** Move `color` away from `against` in lightness until it reaches `min` contrast. */
|
|
28
|
+
function ensureContrast(color, against, min) {
|
|
29
|
+
const c = oklch(color);
|
|
30
|
+
const darker = oklch(against).l > 0.5;
|
|
31
|
+
for (let l = c.l; l >= 0 && l <= 1; l += darker ? -0.01 : 0.01) {
|
|
32
|
+
const candidate = hex(l, c.c ?? 0, c.h ?? 0);
|
|
33
|
+
if (contrastRatio(candidate, against) >= min)
|
|
34
|
+
return candidate;
|
|
35
|
+
}
|
|
36
|
+
return darker ? '#000000' : '#ffffff';
|
|
37
|
+
}
|
|
38
|
+
/** Black or white tinted toward the theme, whichever reads better on `fill`. */
|
|
39
|
+
function onFill(fill, seeds) {
|
|
40
|
+
const light = oklch(seeds.background).l > oklch(seeds.foreground).l ? seeds.background : seeds.foreground;
|
|
41
|
+
const dark = light === seeds.background ? seeds.foreground : seeds.background;
|
|
42
|
+
return contrastRatio(light, fill) >= contrastRatio(dark, fill) ? light : dark;
|
|
43
|
+
}
|
|
44
|
+
// Status hues stay recognisable across brands; only their lightness adapts to the page.
|
|
45
|
+
const STATUS_HUES = { destructive: 27, success: 150, warning: 75, info: 240 };
|
|
46
|
+
/** The 25 base colours a theme needs, derived from its seeds. */
|
|
47
|
+
function themeBaseFromSeeds(seeds) {
|
|
48
|
+
const { background, foreground, primary } = seeds;
|
|
49
|
+
const dark = oklch(background).l < 0.5;
|
|
50
|
+
const accent = seeds.accent ?? toward(primary, background, 0.82);
|
|
51
|
+
const surface = toward(background, foreground, dark ? 0.06 : 0.025);
|
|
52
|
+
const muted = toward(background, foreground, dark ? 0.1 : 0.06);
|
|
53
|
+
const status = (hue) => ensureContrast(hex(dark ? 0.72 : 0.55, 0.15, hue), background, 4.5);
|
|
54
|
+
const destructive = status(STATUS_HUES.destructive);
|
|
55
|
+
const success = status(STATUS_HUES.success);
|
|
56
|
+
const warning = status(STATUS_HUES.warning);
|
|
57
|
+
const info = status(STATUS_HUES.info);
|
|
58
|
+
const fills = { background, foreground };
|
|
59
|
+
return {
|
|
60
|
+
background,
|
|
61
|
+
foreground,
|
|
62
|
+
card: surface,
|
|
63
|
+
cardForeground: foreground,
|
|
64
|
+
popover: surface,
|
|
65
|
+
popoverForeground: foreground,
|
|
66
|
+
primary,
|
|
67
|
+
primaryForeground: onFill(primary, fills),
|
|
68
|
+
secondary: muted,
|
|
69
|
+
secondaryForeground: foreground,
|
|
70
|
+
accent,
|
|
71
|
+
accentForeground: onFill(accent, fills),
|
|
72
|
+
muted,
|
|
73
|
+
mutedForeground: ensureContrast(toward(foreground, background, 0.42), muted, 4.5),
|
|
74
|
+
border: toward(background, foreground, dark ? 0.16 : 0.13),
|
|
75
|
+
input: muted,
|
|
76
|
+
ring: primary,
|
|
77
|
+
destructive,
|
|
78
|
+
destructiveForeground: onFill(destructive, fills),
|
|
79
|
+
success,
|
|
80
|
+
successForeground: onFill(success, fills),
|
|
81
|
+
warning,
|
|
82
|
+
warningForeground: onFill(warning, fills),
|
|
83
|
+
info,
|
|
84
|
+
infoForeground: onFill(info, fills),
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* A complete preset from seeds. `light` is required; `dark` defaults to a dark page with
|
|
89
|
+
* the same brand colours. `overrides` replaces any base colour before the rest is
|
|
90
|
+
* derived, so nothing is out of reach; derived tokens can be adjusted on the result.
|
|
91
|
+
*/
|
|
92
|
+
export function themeFromSeeds(input) {
|
|
93
|
+
const { light } = input;
|
|
94
|
+
const page = oklch(light.background);
|
|
95
|
+
const darkPage = hex(0.17, Math.min(page.c ?? 0, 0.02), page.h ?? 0);
|
|
96
|
+
// Generated, not supplied, so brand colours may be lifted until they read on a dark page.
|
|
97
|
+
const dark = input.dark ?? {
|
|
98
|
+
background: darkPage,
|
|
99
|
+
foreground: hex(0.94, Math.min(page.c ?? 0, 0.015), page.h ?? 0),
|
|
100
|
+
primary: ensureContrast(light.primary, darkPage, 4.5),
|
|
101
|
+
accent: light.accent && ensureContrast(light.accent, darkPage, 3),
|
|
102
|
+
};
|
|
103
|
+
return createThemePreset({
|
|
104
|
+
name: input.name,
|
|
105
|
+
description: input.description,
|
|
106
|
+
light: { ...themeBaseFromSeeds(light), ...input.overrides?.light },
|
|
107
|
+
dark: { ...themeBaseFromSeeds(dark), ...input.overrides?.dark },
|
|
108
|
+
visual: input.visual,
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
const TEXT_PAIRS = [
|
|
112
|
+
['foreground', 'background'],
|
|
113
|
+
['cardForeground', 'card'],
|
|
114
|
+
['popoverForeground', 'popover'],
|
|
115
|
+
['mutedForeground', 'background'],
|
|
116
|
+
['mutedForeground', 'muted'],
|
|
117
|
+
['primaryForeground', 'primary'],
|
|
118
|
+
['secondaryForeground', 'secondary'],
|
|
119
|
+
['accentForeground', 'accent'],
|
|
120
|
+
['destructiveForeground', 'destructive'],
|
|
121
|
+
['successForeground', 'success'],
|
|
122
|
+
['warningForeground', 'warning'],
|
|
123
|
+
['infoForeground', 'info'],
|
|
124
|
+
['primary', 'background'],
|
|
125
|
+
['destructive', 'background'],
|
|
126
|
+
];
|
|
127
|
+
const UI_PAIRS = [['ring', 'background']];
|
|
128
|
+
/** A translucent colour as it actually renders: blended onto the page. */
|
|
129
|
+
function onPage(color, page) {
|
|
130
|
+
const parsed = parse(color);
|
|
131
|
+
if (!parsed || (parsed.alpha ?? 1) >= 1)
|
|
132
|
+
return color;
|
|
133
|
+
return formatHex(blend([page, parsed], 'normal'));
|
|
134
|
+
}
|
|
135
|
+
/** Every text and focus-ring pairing the components render, checked against WCAG AA. */
|
|
136
|
+
export function checkThemeContrast(colors) {
|
|
137
|
+
const check = ([fg, bg], min) => {
|
|
138
|
+
const background = onPage(colors[bg], colors.background);
|
|
139
|
+
const ratio = Math.round(contrastRatio(onPage(colors[fg], background), background) * 100) / 100;
|
|
140
|
+
return { pair: `${fg} on ${bg}`, ratio, min, pass: ratio >= min };
|
|
141
|
+
};
|
|
142
|
+
return [...TEXT_PAIRS.map((p) => check(p, 4.5)), ...UI_PAIRS.map((p) => check(p, 3))];
|
|
143
|
+
}
|
package/dist/metro.cjs
CHANGED
|
@@ -4,10 +4,11 @@ const { withUniwindConfig } = require('uniwind/metro');
|
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Packages that must resolve the real `react-native`. Uniwind's native resolver
|
|
7
|
-
* rewrites every `from 'react-native'` to its component wrapper
|
|
8
|
-
* components and
|
|
9
|
-
* `undefined` with no build error. `expo-asset` imports `AssetRegistry
|
|
10
|
-
* without it every `@expo/vector-icons` glyph is silently absent on
|
|
7
|
+
* rewrites every `from 'react-native'` to its component wrapper. The wrapper re-exports
|
|
8
|
+
* the components and most of the API, but not all of it, and a missing export arrives
|
|
9
|
+
* as `undefined` with no build error. `expo-asset` imports `AssetRegistry`, which the
|
|
10
|
+
* wrapper lacks, and without it every `@expo/vector-icons` glyph is silently absent on
|
|
11
|
+
* iOS and Android.
|
|
11
12
|
*/
|
|
12
13
|
const NEEDS_REAL_REACT_NATIVE = ['expo-asset', 'expo-font', '@expo/vector-icons'];
|
|
13
14
|
|