@thewhileloop/whileui 4.1.0 → 4.1.2
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 +120 -22
- package/dist/blocks/datepicker/date-range-picker-modal.js +3 -2
- package/dist/blocks/datepicker/datepicker-modal.js +3 -2
- package/dist/blocks/datepicker/time-picker-modal.js +3 -2
- package/dist/blocks/layout/confirm-action-sheet.js +3 -2
- package/dist/blocks/layout/index.d.ts +1 -0
- package/dist/blocks/layout/index.js +1 -0
- package/dist/blocks/layout/loading-state.d.ts +8 -0
- package/dist/blocks/layout/loading-state.js +12 -0
- package/dist/blocks/layout/sheet.js +21 -2
- package/dist/blocks/media/smart-image.js +5 -7
- package/dist/blocks/navigation/drawer-menu.js +10 -55
- package/dist/blocks/navigation/header.d.ts +2 -0
- package/dist/blocks/navigation/header.js +1 -1
- package/dist/components/alert/alert.js +4 -1
- package/dist/components/alert-dialog/alert-dialog.js +3 -2
- package/dist/components/avatar/avatar.d.ts +3 -2
- package/dist/components/avatar/avatar.js +19 -13
- package/dist/components/avatar/index.d.ts +1 -1
- package/dist/components/context-menu/context-menu.js +4 -3
- package/dist/components/dialog/dialog.js +4 -3
- package/dist/components/dropdown-menu/dropdown-menu.js +4 -3
- package/dist/components/hover-card/hover-card.js +5 -1
- package/dist/components/menubar/menubar.js +5 -4
- package/dist/components/popover/popover.js +5 -1
- package/dist/components/select/select.js +36 -15
- package/dist/components/text/text.d.ts +3 -0
- package/dist/components/text/text.js +2 -0
- package/dist/components/toast/toast.js +5 -1
- package/dist/components/toggle/toggle.d.ts +1 -1
- package/dist/components/toggle/toggle.js +1 -1
- package/dist/components/tooltip/tooltip.js +3 -1
- package/dist/index.d.ts +1 -1
- package/dist/lib/overlay-host.d.ts +26 -0
- package/dist/lib/overlay-host.js +77 -0
- package/dist/lib/overlay-presence.d.ts +15 -0
- package/dist/lib/overlay-presence.js +55 -0
- package/dist/lib/recipes.d.ts +1 -1
- package/dist/lib/recipes.js +24 -5
- package/dist/lib/theme-bridge.d.ts +1 -1
- package/dist/lib/theme-bridge.js +52 -18
- package/dist/metro.cjs +33 -0
- package/dist/metro.d.cts +21 -0
- package/package.json +6 -2
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
interface OverlayPresenceOptions {
|
|
2
|
+
visible: boolean;
|
|
3
|
+
openDuration: number;
|
|
4
|
+
closeDuration: number;
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Keeps an overlay mounted through its exit animation. `progress` runs 0 → 1 on open and
|
|
8
|
+
* back to 0 on close; `mounted` drops once the exit finishes. Reduced motion and the
|
|
9
|
+
* disabled test policy collapse both durations to zero.
|
|
10
|
+
*/
|
|
11
|
+
export declare function useOverlayPresence({ visible, openDuration, closeDuration, }: OverlayPresenceOptions): {
|
|
12
|
+
mounted: boolean;
|
|
13
|
+
progress: import("react-native-reanimated").SharedValue<number>;
|
|
14
|
+
};
|
|
15
|
+
export {};
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from 'react';
|
|
2
|
+
import { Easing, useSharedValue, withTiming } from 'react-native-reanimated';
|
|
3
|
+
import { useMotionPolicy } from './motion-policy';
|
|
4
|
+
/**
|
|
5
|
+
* Keeps an overlay mounted through its exit animation. `progress` runs 0 → 1 on open and
|
|
6
|
+
* back to 0 on close; `mounted` drops once the exit finishes. Reduced motion and the
|
|
7
|
+
* disabled test policy collapse both durations to zero.
|
|
8
|
+
*/
|
|
9
|
+
export function useOverlayPresence({ visible, openDuration, closeDuration, }) {
|
|
10
|
+
const motion = useMotionPolicy();
|
|
11
|
+
const progress = useSharedValue(0);
|
|
12
|
+
const [mounted, setMounted] = useState(visible);
|
|
13
|
+
const closeTimerRef = useRef(null);
|
|
14
|
+
const [prevVisible, setPrevVisible] = useState(visible);
|
|
15
|
+
// Open in the prop-change render so the first animation frame has a mounted panel;
|
|
16
|
+
// closing stays mounted on the timer until the exit animation finishes.
|
|
17
|
+
if (prevVisible !== visible) {
|
|
18
|
+
setPrevVisible(visible);
|
|
19
|
+
if (visible)
|
|
20
|
+
setMounted(true);
|
|
21
|
+
}
|
|
22
|
+
useEffect(() => {
|
|
23
|
+
if (!mounted)
|
|
24
|
+
return;
|
|
25
|
+
if (closeTimerRef.current) {
|
|
26
|
+
clearTimeout(closeTimerRef.current);
|
|
27
|
+
closeTimerRef.current = null;
|
|
28
|
+
}
|
|
29
|
+
const duration = motion.reduceMotion || !motion.animationsEnabled ? 0 : visible ? openDuration : closeDuration;
|
|
30
|
+
progress.value = withTiming(visible ? 1 : 0, {
|
|
31
|
+
duration,
|
|
32
|
+
reduceMotion: motion.reanimatedReduceMotion,
|
|
33
|
+
easing: visible ? Easing.bezier(0.2, 0.8, 0.2, 1) : Easing.bezier(0.4, 0, 1, 1),
|
|
34
|
+
});
|
|
35
|
+
if (!visible) {
|
|
36
|
+
closeTimerRef.current = setTimeout(() => setMounted(false), duration);
|
|
37
|
+
}
|
|
38
|
+
return () => {
|
|
39
|
+
if (closeTimerRef.current) {
|
|
40
|
+
clearTimeout(closeTimerRef.current);
|
|
41
|
+
closeTimerRef.current = null;
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
}, [
|
|
45
|
+
mounted,
|
|
46
|
+
visible,
|
|
47
|
+
progress,
|
|
48
|
+
openDuration,
|
|
49
|
+
closeDuration,
|
|
50
|
+
motion.animationsEnabled,
|
|
51
|
+
motion.reduceMotion,
|
|
52
|
+
motion.reanimatedReduceMotion,
|
|
53
|
+
]);
|
|
54
|
+
return { mounted, progress };
|
|
55
|
+
}
|
package/dist/lib/recipes.d.ts
CHANGED
package/dist/lib/recipes.js
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { Platform } from 'react-native';
|
|
2
|
+
import { formatRgb, parse } from 'culori';
|
|
1
3
|
import { useThemeColors } from './theme-colors';
|
|
2
4
|
import { useVisualTokens } from './visual-tokens';
|
|
3
5
|
export function controlRecipe(visual, size = 'default') {
|
|
@@ -92,14 +94,31 @@ export function shadowStyle(visual, colors, tier) {
|
|
|
92
94
|
: visual.shadowElevationLg;
|
|
93
95
|
if (opacity <= 0)
|
|
94
96
|
return {};
|
|
97
|
+
// Android below 9 cannot draw an outset box shadow and keeps the elevation fallback.
|
|
98
|
+
if (Platform.OS === 'android' && typeof Platform.Version === 'number' && Platform.Version < 28) {
|
|
99
|
+
return { elevation };
|
|
100
|
+
}
|
|
101
|
+
// A box shadow is clipped outside the border box, so it never shows through a
|
|
102
|
+
// translucent surface. Elevation and the legacy `shadow*` props draw under the whole
|
|
103
|
+
// view, which greyed every translucent card on Android. CSS blur is twice the
|
|
104
|
+
// Core Animation radius the tokens were tuned for.
|
|
95
105
|
return {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
106
|
+
boxShadow: [
|
|
107
|
+
{
|
|
108
|
+
offsetX: 0,
|
|
109
|
+
offsetY,
|
|
110
|
+
blurRadius: radius * 2,
|
|
111
|
+
color: shadowColorWithOpacity(colors.shadow, opacity),
|
|
112
|
+
},
|
|
113
|
+
],
|
|
101
114
|
};
|
|
102
115
|
}
|
|
116
|
+
function shadowColorWithOpacity(color, opacity) {
|
|
117
|
+
const parsed = parse(color);
|
|
118
|
+
if (!parsed)
|
|
119
|
+
return color;
|
|
120
|
+
return formatRgb({ ...parsed, alpha: opacity });
|
|
121
|
+
}
|
|
103
122
|
// Component-owned text sizes. Font family/weight stays on className
|
|
104
123
|
// (font-medium etc.) so app font mappings keep working.
|
|
105
124
|
export function typographyStyle(visual, role) {
|
|
@@ -23,7 +23,7 @@ export declare function applyThemeMode(mode: ThemeMode): void;
|
|
|
23
23
|
export declare function resolveThemeMode(adapter?: ThemeBridgeAdapter, fallback?: ThemeMode): Promise<ThemeMode>;
|
|
24
24
|
export declare function syncThemeMode(mode: ThemeMode, adapter?: ThemeBridgeAdapter): Promise<ThemeMode>;
|
|
25
25
|
export declare function useThemeBridge({ initialMode, adapter, }?: UseThemeBridgeOptions): UseThemeBridgeResult;
|
|
26
|
-
export declare function ThemeBridge({ mode, initialMode, adapter }: ThemeBridgeProps): import("react").FunctionComponentElement<Pick<ThemeBridgeProps, "
|
|
26
|
+
export declare function ThemeBridge({ mode, initialMode, adapter }: ThemeBridgeProps): import("react").FunctionComponentElement<Pick<ThemeBridgeProps, "adapter" | "initialMode">> | import("react").FunctionComponentElement<{
|
|
27
27
|
mode: ThemeMode;
|
|
28
28
|
adapter?: ThemeBridgeAdapter;
|
|
29
29
|
}>;
|
package/dist/lib/theme-bridge.js
CHANGED
|
@@ -1,6 +1,37 @@
|
|
|
1
|
-
import { createElement, useCallback, useEffect, useMemo,
|
|
1
|
+
import { createElement, useCallback, useEffect, useMemo, useReducer } from 'react';
|
|
2
2
|
import { Uniwind, useUniwind } from 'uniwind';
|
|
3
3
|
const modeCycle = ['light', 'dark', 'system'];
|
|
4
|
+
function themeBridgeReducer(state, action) {
|
|
5
|
+
if (action.type === 'cycle') {
|
|
6
|
+
return {
|
|
7
|
+
...state,
|
|
8
|
+
status: 'ready',
|
|
9
|
+
mode: modeCycle[(modeCycle.indexOf(state.mode) + 1) % modeCycle.length],
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
if (action.type === 'select') {
|
|
13
|
+
return { ...state, status: 'ready', mode: action.mode };
|
|
14
|
+
}
|
|
15
|
+
if (action.type === 'hydrate') {
|
|
16
|
+
if (state.adapter === action.adapter &&
|
|
17
|
+
state.initialMode === action.initialMode &&
|
|
18
|
+
state.status === (action.adapter ? 'hydrating' : 'ready')) {
|
|
19
|
+
return state;
|
|
20
|
+
}
|
|
21
|
+
return {
|
|
22
|
+
status: action.adapter ? 'hydrating' : 'ready',
|
|
23
|
+
mode: state.mode,
|
|
24
|
+
adapter: action.adapter,
|
|
25
|
+
initialMode: action.initialMode,
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
if (state.status !== 'hydrating' ||
|
|
29
|
+
state.adapter !== action.adapter ||
|
|
30
|
+
state.initialMode !== action.initialMode) {
|
|
31
|
+
return state;
|
|
32
|
+
}
|
|
33
|
+
return { ...state, status: 'ready', mode: action.mode };
|
|
34
|
+
}
|
|
4
35
|
export function applyThemeMode(mode) {
|
|
5
36
|
Uniwind.setTheme(mode);
|
|
6
37
|
}
|
|
@@ -23,16 +54,21 @@ export async function syncThemeMode(mode, adapter) {
|
|
|
23
54
|
}
|
|
24
55
|
export function useThemeBridge({ initialMode = 'system', adapter, } = {}) {
|
|
25
56
|
const { theme } = useUniwind();
|
|
26
|
-
const [
|
|
27
|
-
|
|
57
|
+
const [state, dispatch] = useReducer(themeBridgeReducer, {
|
|
58
|
+
status: adapter ? 'hydrating' : 'ready',
|
|
59
|
+
mode: initialMode,
|
|
60
|
+
adapter,
|
|
61
|
+
initialMode,
|
|
62
|
+
});
|
|
28
63
|
useEffect(() => {
|
|
29
64
|
let mounted = true;
|
|
65
|
+
dispatch({ type: 'hydrate', adapter, initialMode });
|
|
30
66
|
if (!adapter) {
|
|
31
67
|
return;
|
|
32
68
|
}
|
|
33
69
|
// `loadThemeMode` is the consumer's storage. If it rejects — a corrupt store, a
|
|
34
70
|
// keychain prompt the user dismissed — the app must still render in the initial
|
|
35
|
-
// mode
|
|
71
|
+
// mode rather than remaining in the hydrating phase forever.
|
|
36
72
|
void (async () => {
|
|
37
73
|
let next;
|
|
38
74
|
try {
|
|
@@ -44,35 +80,33 @@ export function useThemeBridge({ initialMode = 'system', adapter, } = {}) {
|
|
|
44
80
|
if (!mounted) {
|
|
45
81
|
return;
|
|
46
82
|
}
|
|
47
|
-
|
|
48
|
-
setHydrated(true);
|
|
83
|
+
dispatch({ type: 'hydrated', adapter, initialMode, mode: next });
|
|
49
84
|
})();
|
|
50
85
|
return () => {
|
|
51
86
|
mounted = false;
|
|
52
87
|
};
|
|
53
88
|
}, [adapter, initialMode]);
|
|
54
89
|
useEffect(() => {
|
|
55
|
-
if (
|
|
90
|
+
if (state.status !== 'ready' ||
|
|
91
|
+
state.adapter !== adapter ||
|
|
92
|
+
state.initialMode !== initialMode) {
|
|
56
93
|
return;
|
|
57
94
|
}
|
|
58
|
-
applyThemeMode(mode);
|
|
95
|
+
applyThemeMode(state.mode);
|
|
59
96
|
if (adapter) {
|
|
60
|
-
void adapter.saveThemeMode(mode);
|
|
97
|
+
void adapter.saveThemeMode(state.mode);
|
|
61
98
|
}
|
|
62
|
-
}, [adapter,
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
const currentIndex = modeCycle.indexOf(currentMode);
|
|
66
|
-
const nextIndex = (currentIndex + 1) % modeCycle.length;
|
|
67
|
-
return modeCycle[nextIndex];
|
|
68
|
-
});
|
|
99
|
+
}, [adapter, initialMode, state]);
|
|
100
|
+
const setMode = useCallback((mode) => {
|
|
101
|
+
dispatch({ type: 'select', mode });
|
|
69
102
|
}, []);
|
|
103
|
+
const cycleMode = useCallback(() => dispatch({ type: 'cycle' }), []);
|
|
70
104
|
return useMemo(() => ({
|
|
71
|
-
mode,
|
|
105
|
+
mode: state.mode,
|
|
72
106
|
resolvedTheme: theme,
|
|
73
107
|
setMode,
|
|
74
108
|
cycleMode,
|
|
75
|
-
}), [cycleMode, mode, theme]);
|
|
109
|
+
}), [cycleMode, setMode, state.mode, theme]);
|
|
76
110
|
}
|
|
77
111
|
function ControlledThemeBridge({ mode, adapter, }) {
|
|
78
112
|
useEffect(() => {
|
package/dist/metro.cjs
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { withUniwindConfig } = require('uniwind/metro');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Packages that must resolve the real `react-native`. Uniwind's native resolver
|
|
7
|
+
* rewrites every `from 'react-native'` to its component wrapper, which re-exports
|
|
8
|
+
* components and nothing else, so a package importing a non-component export gets
|
|
9
|
+
* `undefined` with no build error. `expo-asset` imports `AssetRegistry` that way, and
|
|
10
|
+
* without it every `@expo/vector-icons` glyph is silently absent on iOS and Android.
|
|
11
|
+
*/
|
|
12
|
+
const NEEDS_REAL_REACT_NATIVE = ['expo-asset', 'expo-font', '@expo/vector-icons'];
|
|
13
|
+
|
|
14
|
+
function withWhileUIMetro(config, options) {
|
|
15
|
+
const { exemptFromUniwindRewrite = [], ...uniwindOptions } = options;
|
|
16
|
+
const wrapped = withUniwindConfig(config, uniwindOptions);
|
|
17
|
+
const exempt = [...NEEDS_REAL_REACT_NATIVE, ...exemptFromUniwindRewrite];
|
|
18
|
+
const uniwindResolveRequest = wrapped.resolver.resolveRequest;
|
|
19
|
+
|
|
20
|
+
wrapped.resolver.resolveRequest = (context, moduleName, platform) => {
|
|
21
|
+
if (moduleName === 'react-native') {
|
|
22
|
+
const origin = context.originModulePath ?? '';
|
|
23
|
+
if (exempt.some((pkg) => origin.includes(`node_modules/${pkg}/`))) {
|
|
24
|
+
return context.resolveRequest(context, moduleName, platform);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
return uniwindResolveRequest(context, moduleName, platform);
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
return wrapped;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
module.exports = { withWhileUIMetro, NEEDS_REAL_REACT_NATIVE };
|
package/dist/metro.d.cts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { withUniwindConfig } from 'uniwind/metro';
|
|
2
|
+
|
|
3
|
+
type UniwindMetroOptions = Parameters<typeof withUniwindConfig>[1];
|
|
4
|
+
|
|
5
|
+
export interface WithWhileUIMetroOptions extends UniwindMetroOptions {
|
|
6
|
+
/** Extra packages that must import the real `react-native` instead of Uniwind's wrapper. */
|
|
7
|
+
exemptFromUniwindRewrite?: string[];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** Packages exempted from Uniwind's `react-native` rewrite by default. */
|
|
11
|
+
export const NEEDS_REAL_REACT_NATIVE: readonly string[];
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Wraps an Expo Metro config with Uniwind and keeps `expo-asset`, `expo-font` and
|
|
15
|
+
* `@expo/vector-icons` on the real `react-native`, so icon fonts load on device.
|
|
16
|
+
* Use this in `metro.config.js` instead of calling `withUniwindConfig` yourself.
|
|
17
|
+
*/
|
|
18
|
+
export function withWhileUIMetro<T extends Parameters<typeof withUniwindConfig>[0]>(
|
|
19
|
+
config: T,
|
|
20
|
+
options: WithWhileUIMetroOptions
|
|
21
|
+
): ReturnType<typeof withUniwindConfig>;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@thewhileloop/whileui",
|
|
3
|
-
"version": "4.1.
|
|
3
|
+
"version": "4.1.2",
|
|
4
4
|
"description": "WhileUI Native — Copy-paste components for React Native. You own the code.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -36,6 +36,10 @@
|
|
|
36
36
|
"types": "./dist/vite.d.ts",
|
|
37
37
|
"import": "./dist/vite.js"
|
|
38
38
|
},
|
|
39
|
+
"./metro": {
|
|
40
|
+
"types": "./dist/metro.d.cts",
|
|
41
|
+
"default": "./dist/metro.cjs"
|
|
42
|
+
},
|
|
39
43
|
"./tokens": {
|
|
40
44
|
"types": "./dist/tokens.d.ts",
|
|
41
45
|
"import": "./dist/tokens.js"
|
|
@@ -59,7 +63,7 @@
|
|
|
59
63
|
"access": "public"
|
|
60
64
|
},
|
|
61
65
|
"scripts": {
|
|
62
|
-
"build": "tsc -p tsconfig.build.json",
|
|
66
|
+
"build": "tsc -p tsconfig.build.json && cp src/metro.cjs src/metro.d.cts dist/",
|
|
63
67
|
"prepack": "cp ../../README.md README.md",
|
|
64
68
|
"postpack": "rm -f README.md",
|
|
65
69
|
"prepare": "bun run build",
|