@thewhileloop/whileui 4.2.0 → 4.2.3
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 +2 -1
- package/dist/blocks/layout/sheet.js +1 -1
- package/dist/blocks/navigation/drawer-menu.js +1 -1
- package/dist/components/select/select.js +7 -2
- package/dist/lib/theme-colors.d.ts +4 -3
- package/dist/lib/theme-colors.js +26 -1
- package/dist/lib/visual-tokens.js +13 -0
- package/dist/metro.cjs +7 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1490,7 +1490,8 @@ itself to the collision-available viewport, and wraps long option labels inside
|
|
|
1490
1490
|
| blurTintToken | `'surfaceElevated' \| 'surfaceTranslucent' \| 'card' \| 'popover'` | `'popover'` |
|
|
1491
1491
|
|
|
1492
1492
|
`SelectContent` keeps its listbox inside the safe area by default; pass `insets` only to
|
|
1493
|
-
change that margin.
|
|
1493
|
+
change that margin. A list taller than the screen scrolls inside the listbox on every
|
|
1494
|
+
platform. Inside a Dialog, Sheet or other WhileUI overlay it renders in that
|
|
1494
1495
|
overlay's layer automatically. Option values must be non-empty strings: the web
|
|
1495
1496
|
primitive rejects `""`, so model "none" as an explicit value such as `'none'`.
|
|
1496
1497
|
|
|
@@ -79,7 +79,7 @@ export function Sheet({ open, onOpenChange, children, maxHeight = 'full', maxWid
|
|
|
79
79
|
widthStyle,
|
|
80
80
|
frostedSurface.surfaceStyle,
|
|
81
81
|
];
|
|
82
|
-
return (_jsx(OverlayHost, { open: mounted, animationType: "none", onRequestClose: onClose, children: _jsxs(View, { className: "flex-1 justify-end", children: [_jsx(Animated.View, { style: [
|
|
82
|
+
return (_jsx(OverlayHost, { open: mounted, animationType: "none", onRequestClose: open ? onClose : undefined, children: _jsxs(View, { className: "flex-1 justify-end", children: [_jsx(Animated.View, { style: [
|
|
83
83
|
StyleSheet.absoluteFill,
|
|
84
84
|
{ backgroundColor: frosted ? 'transparent' : colors.overlay },
|
|
85
85
|
backdropStyle,
|
|
@@ -125,5 +125,5 @@ export function DrawerMenu({ visible, onClose, sections, activeKey, onSelect, he
|
|
|
125
125
|
frostedSurface.surfaceStyle,
|
|
126
126
|
style,
|
|
127
127
|
], className: drawerPanelClassName(frosted, className), children: [frostedSurface.overlay, _jsxs(View, { className: "flex-1 pb-8", style: { paddingTop: contentTopPadding }, children: [header && _jsx(View, { className: "px-5 pb-4 mb-2 border-b border-border", children: header }), _jsx(DrawerSectionList, { sections: sections, activeKey: activeKey, onSelect: handleItemPress, interaction: interaction, visual: visual }), footer && _jsx(View, { className: "px-5 py-4 mt-auto border-t border-border", children: footer })] })] })] }));
|
|
128
|
-
return (_jsx(OverlayHost, { open: mounted, animationType: "none", onRequestClose: onClose, children: drawerTree }));
|
|
128
|
+
return (_jsx(OverlayHost, { open: mounted, animationType: "none", onRequestClose: visible ? onClose : undefined, children: drawerTree }));
|
|
129
129
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { jsx as _jsx, Fragment as _Fragment, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import * as React from 'react';
|
|
3
|
-
import { Platform, ScrollView, StyleSheet, View, } from 'react-native';
|
|
3
|
+
import { Platform, ScrollView, StyleSheet, useWindowDimensions, View, } from 'react-native';
|
|
4
4
|
import * as SelectPrimitive from '@rn-primitives/select';
|
|
5
5
|
import { FullWindowOverlay as RNFullWindowOverlay } from 'react-native-screens';
|
|
6
6
|
import { Text } from '../text';
|
|
@@ -68,6 +68,11 @@ const SelectContent = React.forwardRef(({ className, children, portalHost, posit
|
|
|
68
68
|
// without every caller reading the safe area itself.
|
|
69
69
|
const defaultInsets = useOverlayInsets();
|
|
70
70
|
const resolvedInsets = { ...defaultInsets, ...insets };
|
|
71
|
+
// Web caps the listbox with the primitive's available-height variable. Native has
|
|
72
|
+
// no equivalent: a list longer than the screen ran under the status bar with
|
|
73
|
+
// nothing to scroll, so the options get a scroll owner bounded by the insets.
|
|
74
|
+
const { height: windowHeight } = useWindowDimensions();
|
|
75
|
+
const nativeMaxHeight = windowHeight - resolvedInsets.top - resolvedInsets.bottom;
|
|
71
76
|
// Radix applies its collision variables to the listbox wrapper, while
|
|
72
77
|
// @rn-primitives renders our visual surface one level inside it. Keep the sizing
|
|
73
78
|
// on that surface as inline web CSS: the Viewport's own `width: 100%` otherwise
|
|
@@ -94,7 +99,7 @@ const SelectContent = React.forwardRef(({ className, children, portalHost, posit
|
|
|
94
99
|
}, className: cn('border-border relative z-50 overflow-hidden', frosted ? 'bg-transparent' : 'bg-popover', Platform.select({
|
|
95
100
|
web: cn('overflow-y-auto overflow-x-hidden', props.side === 'bottom' && 'translate-y-1', props.side === 'top' && '-translate-y-1'),
|
|
96
101
|
native: 'p-1',
|
|
97
|
-
}), className), ...props, children: [frostedSurface.overlay, _jsx(SelectPrimitive.Viewport, { className: "p-1", children: children })] }) }) }) }));
|
|
102
|
+
}), className), ...props, children: [frostedSurface.overlay, _jsx(SelectPrimitive.Viewport, { className: "p-1", children: Platform.OS === 'web' ? (children) : (_jsx(ScrollView, { style: { maxHeight: nativeMaxHeight }, bounces: false, keyboardShouldPersistTaps: "handled", children: children })) })] }) }) }) }));
|
|
98
103
|
});
|
|
99
104
|
SelectContent.displayName = 'SelectContent';
|
|
100
105
|
const SelectItem = React.forwardRef(({ className, children, style: styleProp, ...props }, ref) => {
|
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
import { type ThemeColors } from './theme-contract';
|
|
2
2
|
export type { ThemeColors } from './theme-contract';
|
|
3
3
|
export declare function useThemeColors(): ThemeColors;
|
|
4
|
-
|
|
5
|
-
export declare function useIconColors(): {
|
|
4
|
+
export interface IconColors {
|
|
6
5
|
foreground: string;
|
|
7
6
|
muted: string;
|
|
8
7
|
primary: string;
|
|
@@ -12,6 +11,8 @@ export declare function useIconColors(): {
|
|
|
12
11
|
success: string;
|
|
13
12
|
info: string;
|
|
14
13
|
destructive: string;
|
|
15
|
-
}
|
|
14
|
+
}
|
|
15
|
+
/** Icon colors derived from theme. For @expo/vector-icons which need hex values. */
|
|
16
|
+
export declare function useIconColors(): IconColors;
|
|
16
17
|
/** Alias for useThemeColors. Returns RN-safe color strings (hex/rgb/rgba) for RefreshControl, LinearGradient, charts. */
|
|
17
18
|
export declare const useThemeTokens: typeof useThemeColors;
|
package/dist/lib/theme-colors.js
CHANGED
|
@@ -41,10 +41,29 @@ function resolveThemeToken(cssValue, appValue) {
|
|
|
41
41
|
}
|
|
42
42
|
return parseThemeColor(cssValue) ?? parseThemeColor(appValue);
|
|
43
43
|
}
|
|
44
|
+
// Every themed component calls this hook on every render, and parsing 104 colour
|
|
45
|
+
// strings plus deriving the palette cost about 0.16 ms per call in Node, several times
|
|
46
|
+
// that under Hermes. The inputs only change with the theme, so the result is derived
|
|
47
|
+
// once per distinct snapshot and shared: the same object comes back until the CSS
|
|
48
|
+
// variables change, which also keeps it stable for downstream memoisation.
|
|
49
|
+
const resolvedThemeColors = new Map();
|
|
50
|
+
const RESOLVED_THEME_LIMIT = 8;
|
|
44
51
|
export function useThemeColors() {
|
|
45
52
|
const { theme } = useUniwind();
|
|
46
53
|
const values = useCSSVariable(THEME_TOKEN_VARIABLES);
|
|
47
54
|
const scheme = theme === 'dark' ? 'dark' : 'light';
|
|
55
|
+
const key = `${scheme}\u0001${values.join('\u0001')}`;
|
|
56
|
+
const cached = resolvedThemeColors.get(key);
|
|
57
|
+
if (cached)
|
|
58
|
+
return cached;
|
|
59
|
+
const resolved = resolveThemeColors(values, scheme);
|
|
60
|
+
if (resolvedThemeColors.size >= RESOLVED_THEME_LIMIT) {
|
|
61
|
+
resolvedThemeColors.delete(resolvedThemeColors.keys().next().value);
|
|
62
|
+
}
|
|
63
|
+
resolvedThemeColors.set(key, resolved);
|
|
64
|
+
return resolved;
|
|
65
|
+
}
|
|
66
|
+
function resolveThemeColors(values, scheme) {
|
|
48
67
|
const fallbackPalette = scheme === 'dark' ? WHILEUI_DARK_THEME_COLORS : WHILEUI_LIGHT_THEME_COLORS;
|
|
49
68
|
const defined = {};
|
|
50
69
|
let index = 0;
|
|
@@ -61,10 +80,14 @@ export function useThemeColors() {
|
|
|
61
80
|
const derived = deriveThemeColors(base, scheme);
|
|
62
81
|
return { ...derived, ...defined, placeholder: derived.placeholder };
|
|
63
82
|
}
|
|
83
|
+
const iconColorsByTheme = new WeakMap();
|
|
64
84
|
/** Icon colors derived from theme. For @expo/vector-icons which need hex values. */
|
|
65
85
|
export function useIconColors() {
|
|
66
86
|
const c = useThemeColors();
|
|
67
|
-
|
|
87
|
+
const cached = iconColorsByTheme.get(c);
|
|
88
|
+
if (cached)
|
|
89
|
+
return cached;
|
|
90
|
+
const icons = {
|
|
68
91
|
foreground: c.foreground,
|
|
69
92
|
muted: c.mutedForeground,
|
|
70
93
|
primary: c.primary,
|
|
@@ -75,6 +98,8 @@ export function useIconColors() {
|
|
|
75
98
|
info: c.info,
|
|
76
99
|
destructive: c.destructive,
|
|
77
100
|
};
|
|
101
|
+
iconColorsByTheme.set(c, icons);
|
|
102
|
+
return icons;
|
|
78
103
|
}
|
|
79
104
|
/** Alias for useThemeColors. Returns RN-safe color strings (hex/rgb/rgba) for RefreshControl, LinearGradient, charts. */
|
|
80
105
|
export const useThemeTokens = useThemeColors;
|
|
@@ -28,11 +28,24 @@ function resolveVisualToken(spec, value) {
|
|
|
28
28
|
}
|
|
29
29
|
return parseRatio(value, spec.fallback, spec.min ?? 0, spec.max ?? 1);
|
|
30
30
|
}
|
|
31
|
+
// Resolved once per distinct CSS snapshot and shared, for the same reason as
|
|
32
|
+
// `useThemeColors`: 83 parses per component per render add up on a list screen, and a
|
|
33
|
+
// stable object lets callers memoise on it.
|
|
34
|
+
const resolvedVisualTokens = new Map();
|
|
35
|
+
const RESOLVED_VISUAL_LIMIT = 8;
|
|
31
36
|
export function useVisualTokens() {
|
|
32
37
|
const values = useCSSVariable(VISUAL_TOKEN_VARIABLES);
|
|
38
|
+
const key = values.join('\u0001');
|
|
39
|
+
const cached = resolvedVisualTokens.get(key);
|
|
40
|
+
if (cached)
|
|
41
|
+
return cached;
|
|
33
42
|
const resolved = {};
|
|
34
43
|
VISUAL_TOKEN_SPECS.forEach((spec, index) => {
|
|
35
44
|
resolved[spec.key] = resolveVisualToken(spec, values[index]);
|
|
36
45
|
});
|
|
46
|
+
if (resolvedVisualTokens.size >= RESOLVED_VISUAL_LIMIT) {
|
|
47
|
+
resolvedVisualTokens.delete(resolvedVisualTokens.keys().next().value);
|
|
48
|
+
}
|
|
49
|
+
resolvedVisualTokens.set(key, resolved);
|
|
37
50
|
return resolved;
|
|
38
51
|
}
|
package/dist/metro.cjs
CHANGED
|
@@ -12,6 +12,11 @@ const { withUniwindConfig } = require('uniwind/metro');
|
|
|
12
12
|
const NEEDS_REAL_REACT_NATIVE = ['expo-asset', 'expo-font', '@expo/vector-icons'];
|
|
13
13
|
|
|
14
14
|
function withWhileUIMetro(config, options) {
|
|
15
|
+
if (!options || typeof options.cssEntryFile !== 'string') {
|
|
16
|
+
throw new Error(
|
|
17
|
+
"withWhileUIMetro(config, options): pass Uniwind's options, at least { cssEntryFile: './global.css' }"
|
|
18
|
+
);
|
|
19
|
+
}
|
|
15
20
|
const { exemptFromUniwindRewrite = [], ...uniwindOptions } = options;
|
|
16
21
|
const wrapped = withUniwindConfig(config, uniwindOptions);
|
|
17
22
|
const exempt = [...NEEDS_REAL_REACT_NATIVE, ...exemptFromUniwindRewrite];
|
|
@@ -19,7 +24,8 @@ function withWhileUIMetro(config, options) {
|
|
|
19
24
|
|
|
20
25
|
wrapped.resolver.resolveRequest = (context, moduleName, platform) => {
|
|
21
26
|
if (moduleName === 'react-native') {
|
|
22
|
-
|
|
27
|
+
// Metro reports Windows paths with backslashes; match either separator.
|
|
28
|
+
const origin = (context.originModulePath ?? '').replace(/\\/g, '/');
|
|
23
29
|
if (exempt.some((pkg) => origin.includes(`node_modules/${pkg}/`))) {
|
|
24
30
|
return context.resolveRequest(context, moduleName, platform);
|
|
25
31
|
}
|