panelui-native 0.5.3 → 0.7.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 +9 -5
- package/lib/module/components/accordion/index.js +7 -5
- package/lib/module/components/accordion/index.js.map +1 -1
- package/lib/module/components/alert/index.js +3 -6
- package/lib/module/components/alert/index.js.map +1 -1
- package/lib/module/components/bottom-sheet/index.js +67 -3
- package/lib/module/components/bottom-sheet/index.js.map +1 -1
- package/lib/module/components/button/index.js +36 -1
- package/lib/module/components/button/index.js.map +1 -1
- package/lib/module/components/empty-state/index.js +4 -5
- package/lib/module/components/empty-state/index.js.map +1 -1
- package/lib/module/components/inline-select/index.js +171 -46
- package/lib/module/components/inline-select/index.js.map +1 -1
- package/lib/module/components/input/index.js +28 -5
- package/lib/module/components/input/index.js.map +1 -1
- package/lib/module/components/input-group/index.js +0 -1
- package/lib/module/components/input-group/index.js.map +1 -1
- package/lib/module/components/item/index.js +359 -0
- package/lib/module/components/item/index.js.map +1 -0
- package/lib/module/components/label/index.js +5 -3
- package/lib/module/components/label/index.js.map +1 -1
- package/lib/module/components/message/index.js +318 -0
- package/lib/module/components/message/index.js.map +1 -0
- package/lib/module/components/scroll-fade/index.js +204 -0
- package/lib/module/components/scroll-fade/index.js.map +1 -0
- package/lib/module/components/select/index.js +35 -6
- package/lib/module/components/select/index.js.map +1 -1
- package/lib/module/components/shimmer/index.js +167 -0
- package/lib/module/components/shimmer/index.js.map +1 -0
- package/lib/module/components/surface/index.js +65 -0
- package/lib/module/components/surface/index.js.map +1 -0
- package/lib/module/components/switch/index.js +20 -1
- package/lib/module/components/switch/index.js.map +1 -1
- package/lib/module/components/toast/index.js +7 -8
- package/lib/module/components/toast/index.js.map +1 -1
- package/lib/module/components/typography/index.js +3 -3
- package/lib/module/hooks/index.js +10 -0
- package/lib/module/hooks/index.js.map +1 -0
- package/lib/module/hooks/use-breakpoint.js +39 -0
- package/lib/module/hooks/use-breakpoint.js.map +1 -0
- package/lib/module/hooks/use-copy-to-clipboard.js +60 -0
- package/lib/module/hooks/use-copy-to-clipboard.js.map +1 -0
- package/lib/module/hooks/use-debounced-value.js +24 -0
- package/lib/module/hooks/use-debounced-value.js.map +1 -0
- package/lib/module/hooks/use-disclosure.js +43 -0
- package/lib/module/hooks/use-disclosure.js.map +1 -0
- package/lib/module/hooks/use-keyboard-avoidance.js +120 -0
- package/lib/module/hooks/use-keyboard-avoidance.js.map +1 -0
- package/lib/module/hooks/use-keyboard.js +36 -0
- package/lib/module/hooks/use-keyboard.js.map +1 -0
- package/lib/module/hooks/use-previous.js +25 -0
- package/lib/module/hooks/use-previous.js.map +1 -0
- package/lib/module/icons/index.js +4 -4
- package/lib/module/index.js +12 -0
- package/lib/module/index.js.map +1 -1
- package/lib/module/native/index.js +53 -0
- package/lib/module/native/index.js.map +1 -0
- package/lib/module/primitives/keyboard-avoider.js +40 -0
- package/lib/module/primitives/keyboard-avoider.js.map +1 -0
- package/lib/module/providers/panel-ui-provider.js +24 -4
- package/lib/module/providers/panel-ui-provider.js.map +1 -1
- package/lib/module/theme/use-theme.js +1 -1
- package/lib/typescript/src/components/accordion/index.d.ts +7 -5
- package/lib/typescript/src/components/accordion/index.d.ts.map +1 -1
- package/lib/typescript/src/components/alert/index.d.ts +3 -6
- package/lib/typescript/src/components/alert/index.d.ts.map +1 -1
- package/lib/typescript/src/components/bottom-sheet/index.d.ts +21 -1
- package/lib/typescript/src/components/bottom-sheet/index.d.ts.map +1 -1
- package/lib/typescript/src/components/button/index.d.ts +11 -0
- package/lib/typescript/src/components/button/index.d.ts.map +1 -1
- package/lib/typescript/src/components/empty-state/index.d.ts +4 -5
- package/lib/typescript/src/components/empty-state/index.d.ts.map +1 -1
- package/lib/typescript/src/components/inline-select/index.d.ts +29 -6
- package/lib/typescript/src/components/inline-select/index.d.ts.map +1 -1
- package/lib/typescript/src/components/input/index.d.ts +13 -0
- package/lib/typescript/src/components/input/index.d.ts.map +1 -1
- package/lib/typescript/src/components/input-group/index.d.ts +0 -1
- package/lib/typescript/src/components/input-group/index.d.ts.map +1 -1
- package/lib/typescript/src/components/item/index.d.ts +286 -0
- package/lib/typescript/src/components/item/index.d.ts.map +1 -0
- package/lib/typescript/src/components/label/index.d.ts +5 -3
- package/lib/typescript/src/components/label/index.d.ts.map +1 -1
- package/lib/typescript/src/components/message/index.d.ts +170 -0
- package/lib/typescript/src/components/message/index.d.ts.map +1 -0
- package/lib/typescript/src/components/scroll-fade/index.d.ts +47 -0
- package/lib/typescript/src/components/scroll-fade/index.d.ts.map +1 -0
- package/lib/typescript/src/components/select/index.d.ts +16 -1
- package/lib/typescript/src/components/select/index.d.ts.map +1 -1
- package/lib/typescript/src/components/shimmer/index.d.ts +60 -0
- package/lib/typescript/src/components/shimmer/index.d.ts.map +1 -0
- package/lib/typescript/src/components/surface/index.d.ts +30 -0
- package/lib/typescript/src/components/surface/index.d.ts.map +1 -0
- package/lib/typescript/src/components/switch/index.d.ts +10 -0
- package/lib/typescript/src/components/switch/index.d.ts.map +1 -1
- package/lib/typescript/src/components/toast/index.d.ts +4 -4
- package/lib/typescript/src/components/toast/index.d.ts.map +1 -1
- package/lib/typescript/src/hooks/index.d.ts +8 -0
- package/lib/typescript/src/hooks/index.d.ts.map +1 -0
- package/lib/typescript/src/hooks/use-breakpoint.d.ts +32 -0
- package/lib/typescript/src/hooks/use-breakpoint.d.ts.map +1 -0
- package/lib/typescript/src/hooks/use-copy-to-clipboard.d.ts +28 -0
- package/lib/typescript/src/hooks/use-copy-to-clipboard.d.ts.map +1 -0
- package/lib/typescript/src/hooks/use-debounced-value.d.ts +13 -0
- package/lib/typescript/src/hooks/use-debounced-value.d.ts.map +1 -0
- package/lib/typescript/src/hooks/use-disclosure.d.ts +32 -0
- package/lib/typescript/src/hooks/use-disclosure.d.ts.map +1 -0
- package/lib/typescript/src/hooks/use-keyboard-avoidance.d.ts +20 -0
- package/lib/typescript/src/hooks/use-keyboard-avoidance.d.ts.map +1 -0
- package/lib/typescript/src/hooks/use-keyboard.d.ts +20 -0
- package/lib/typescript/src/hooks/use-keyboard.d.ts.map +1 -0
- package/lib/typescript/src/hooks/use-previous.d.ts +15 -0
- package/lib/typescript/src/hooks/use-previous.d.ts.map +1 -0
- package/lib/typescript/src/icons/index.d.ts +4 -4
- package/lib/typescript/src/index.d.ts +8 -0
- package/lib/typescript/src/index.d.ts.map +1 -1
- package/lib/typescript/src/native/index.d.ts +35 -0
- package/lib/typescript/src/native/index.d.ts.map +1 -0
- package/lib/typescript/src/primitives/keyboard-avoider.d.ts +22 -0
- package/lib/typescript/src/primitives/keyboard-avoider.d.ts.map +1 -0
- package/lib/typescript/src/providers/panel-ui-provider.d.ts +1 -1
- package/lib/typescript/src/providers/panel-ui-provider.d.ts.map +1 -1
- package/lib/typescript/src/theme/use-theme.d.ts +1 -1
- package/package.json +19 -3
- package/src/components/accordion/index.tsx +7 -5
- package/src/components/alert/index.tsx +3 -6
- package/src/components/bottom-sheet/index.tsx +82 -2
- package/src/components/button/index.tsx +52 -0
- package/src/components/empty-state/index.tsx +4 -5
- package/src/components/inline-select/index.tsx +191 -44
- package/src/components/input/index.tsx +39 -3
- package/src/components/input-group/index.tsx +0 -1
- package/src/components/item/index.tsx +387 -0
- package/src/components/label/index.tsx +5 -3
- package/src/components/message/index.tsx +312 -0
- package/src/components/scroll-fade/index.tsx +280 -0
- package/src/components/select/index.tsx +54 -9
- package/src/components/shimmer/index.tsx +228 -0
- package/src/components/surface/index.tsx +64 -0
- package/src/components/switch/index.tsx +27 -1
- package/src/components/toast/index.tsx +7 -8
- package/src/components/typography/index.tsx +3 -3
- package/src/hooks/index.ts +24 -0
- package/src/hooks/use-breakpoint.ts +52 -0
- package/src/hooks/use-copy-to-clipboard.ts +79 -0
- package/src/hooks/use-debounced-value.ts +23 -0
- package/src/hooks/use-disclosure.ts +57 -0
- package/src/hooks/use-keyboard-avoidance.ts +140 -0
- package/src/hooks/use-keyboard.ts +43 -0
- package/src/hooks/use-previous.ts +24 -0
- package/src/icons/index.tsx +4 -4
- package/src/index.ts +41 -0
- package/src/native/index.ts +79 -0
- package/src/primitives/keyboard-avoider.tsx +49 -0
- package/src/providers/panel-ui-provider.tsx +30 -9
- package/src/theme/use-theme.ts +1 -1
- package/theme.css +23 -3
|
@@ -7,6 +7,7 @@ import Animated, {
|
|
|
7
7
|
withSpring,
|
|
8
8
|
} from 'react-native-reanimated';
|
|
9
9
|
import { tv, type VariantProps } from 'tailwind-variants';
|
|
10
|
+
import { getNativeUI } from '../../native';
|
|
10
11
|
|
|
11
12
|
const SPRING = { damping: 18, stiffness: 250, mass: 0.5 } as const;
|
|
12
13
|
|
|
@@ -37,6 +38,16 @@ export interface SwitchProps extends VariantProps<typeof switchVariants> {
|
|
|
37
38
|
value: boolean;
|
|
38
39
|
onValueChange?: (value: boolean) => void;
|
|
39
40
|
disabled?: boolean;
|
|
41
|
+
/**
|
|
42
|
+
* Render the platform's own switch instead of this one. Requires the
|
|
43
|
+
* optional `@expo/ui` package; without it this prop does nothing.
|
|
44
|
+
*
|
|
45
|
+
* **Theme tokens do not apply** — the platform draws the control, so
|
|
46
|
+
* `className` and `size` are ignored.
|
|
47
|
+
*/
|
|
48
|
+
native?: boolean;
|
|
49
|
+
/** Text label drawn beside the control. Native mode only. */
|
|
50
|
+
label?: string;
|
|
40
51
|
}
|
|
41
52
|
|
|
42
53
|
/**
|
|
@@ -44,8 +55,9 @@ export interface SwitchProps extends VariantProps<typeof switchVariants> {
|
|
|
44
55
|
* UI thread; toggling never re-renders beyond the value change itself.
|
|
45
56
|
*/
|
|
46
57
|
export const Switch = forwardRef<View, SwitchProps>(
|
|
47
|
-
({ className, value, onValueChange, disabled, size = 'md' }, ref) => {
|
|
58
|
+
({ className, value, onValueChange, disabled, size = 'md', native, label }, ref) => {
|
|
48
59
|
const progress = useSharedValue(value ? 1 : 0);
|
|
60
|
+
const nativeUI = native ? getNativeUI() : null;
|
|
49
61
|
const slots = switchVariants({ size, disabled: !!disabled });
|
|
50
62
|
|
|
51
63
|
useEffect(() => {
|
|
@@ -62,6 +74,20 @@ export const Switch = forwardRef<View, SwitchProps>(
|
|
|
62
74
|
opacity: progress.value,
|
|
63
75
|
}));
|
|
64
76
|
|
|
77
|
+
if (nativeUI) {
|
|
78
|
+
const { Host, Switch: NativeSwitch } = nativeUI;
|
|
79
|
+
return (
|
|
80
|
+
<Host matchContents>
|
|
81
|
+
<NativeSwitch
|
|
82
|
+
value={value}
|
|
83
|
+
onValueChange={(next: boolean) => onValueChange?.(next)}
|
|
84
|
+
label={label}
|
|
85
|
+
disabled={disabled}
|
|
86
|
+
/>
|
|
87
|
+
</Host>
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
65
91
|
return (
|
|
66
92
|
<Pressable
|
|
67
93
|
ref={ref}
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Toast — transient notification.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
4
|
+
* Three ways to raise one, because the right amount of ceremony differs: a
|
|
5
|
+
* bare string, a config object with a title, description and action, or a
|
|
6
|
+
* custom component when none of that fits. The anatomy underneath is the same
|
|
7
|
+
* in all three.
|
|
8
8
|
*
|
|
9
9
|
* The viewport is mounted for you by PanelUIProvider; `useToast()` works
|
|
10
10
|
* anywhere below it.
|
|
@@ -71,8 +71,7 @@ const STACK_SCALE_STEP = 0.97;
|
|
|
71
71
|
const EDGE_INSET = 16;
|
|
72
72
|
|
|
73
73
|
/*
|
|
74
|
-
* Entering
|
|
75
|
-
* src/components/toast/toast.animation.ts.
|
|
74
|
+
* Entering and exiting animations.
|
|
76
75
|
*
|
|
77
76
|
* `mass(3)` is doing the important work: a heavy spring settles without the
|
|
78
77
|
* visible overshoot a default-mass one gives. `withInitialValues` keeps
|
|
@@ -285,7 +284,7 @@ export const Toast = Object.assign(ToastRoot, {
|
|
|
285
284
|
*
|
|
286
285
|
* Swiping is vertical and direction-aware: dragging toward the screen edge the
|
|
287
286
|
* toast came from dismisses it, dragging the other way rubber-bands against a
|
|
288
|
-
* hard limit.
|
|
287
|
+
* hard limit.
|
|
289
288
|
*/
|
|
290
289
|
function ToastSlot({
|
|
291
290
|
item,
|
|
@@ -376,7 +375,7 @@ function ToastSlot({
|
|
|
376
375
|
* Stack position plus the gesture transform.
|
|
377
376
|
*
|
|
378
377
|
* The whole deck takes the *newest* toast's height so the cards behind read
|
|
379
|
-
* as a uniform stack rather than a ragged pile
|
|
378
|
+
* as a uniform stack rather than a ragged pile.
|
|
380
379
|
*/
|
|
381
380
|
const stackStyle = useAnimatedStyle(() => {
|
|
382
381
|
// Fall back to this toast's own height until the front one is measured.
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Typography — semantic text presets.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* primitive so every preset keeps `className` passthrough and theme colours.
|
|
4
|
+
* The type scale in one place, so headings stay consistent instead of being
|
|
5
|
+
* rebuilt out of size and weight classes at each call site. Built on the Text
|
|
6
|
+
* primitive, so every preset keeps `className` passthrough and theme colours.
|
|
7
7
|
*/
|
|
8
8
|
import { forwardRef } from 'react';
|
|
9
9
|
import { View, type Text as RNText, type ViewProps } from 'react-native';
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
export {
|
|
2
|
+
useCopyToClipboard,
|
|
3
|
+
type UseCopyToClipboardOptions,
|
|
4
|
+
type UseCopyToClipboardResult,
|
|
5
|
+
} from './use-copy-to-clipboard';
|
|
6
|
+
export {
|
|
7
|
+
useDisclosure,
|
|
8
|
+
type UseDisclosureOptions,
|
|
9
|
+
type UseDisclosureResult,
|
|
10
|
+
} from './use-disclosure';
|
|
11
|
+
export {
|
|
12
|
+
useBreakpoint,
|
|
13
|
+
BREAKPOINTS,
|
|
14
|
+
type Breakpoint,
|
|
15
|
+
type UseBreakpointResult,
|
|
16
|
+
} from './use-breakpoint';
|
|
17
|
+
export { useKeyboard, type UseKeyboardResult } from './use-keyboard';
|
|
18
|
+
export {
|
|
19
|
+
useKeyboardAvoidance,
|
|
20
|
+
type UseKeyboardAvoidanceOptions,
|
|
21
|
+
type UseKeyboardAvoidanceResult,
|
|
22
|
+
} from './use-keyboard-avoidance';
|
|
23
|
+
export { useDebouncedValue } from './use-debounced-value';
|
|
24
|
+
export { usePrevious } from './use-previous';
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { useWindowDimensions } from 'react-native';
|
|
2
|
+
|
|
3
|
+
/** Tailwind's breakpoints, minus the ones no phone or tablet reaches. */
|
|
4
|
+
export const BREAKPOINTS = { sm: 640, md: 768, lg: 1024, xl: 1280 } as const;
|
|
5
|
+
|
|
6
|
+
export type Breakpoint = keyof typeof BREAKPOINTS;
|
|
7
|
+
|
|
8
|
+
export interface UseBreakpointResult {
|
|
9
|
+
/** Largest breakpoint the window currently satisfies, or `base`. */
|
|
10
|
+
current: Breakpoint | 'base';
|
|
11
|
+
/** True when the window is at least this wide. */
|
|
12
|
+
isAtLeast: (breakpoint: Breakpoint) => boolean;
|
|
13
|
+
width: number;
|
|
14
|
+
height: number;
|
|
15
|
+
isLandscape: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Responsive state from the window size — the React Native answer to a media
|
|
20
|
+
* query, which has no equivalent here.
|
|
21
|
+
*
|
|
22
|
+
* For styling, prefer Uniwind's responsive class prefixes (`md:flex-row`).
|
|
23
|
+
* Reach for this when the *behaviour* changes, not just the look — rendering
|
|
24
|
+
* a sheet on a phone and a dialog on a tablet, say.
|
|
25
|
+
*
|
|
26
|
+
* ```tsx
|
|
27
|
+
* const { isAtLeast } = useBreakpoint();
|
|
28
|
+
* return isAtLeast('md') ? <Dialog …/> : <BottomSheet …/>;
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
export function useBreakpoint(): UseBreakpointResult {
|
|
32
|
+
const { width, height } = useWindowDimensions();
|
|
33
|
+
|
|
34
|
+
const current: Breakpoint | 'base' =
|
|
35
|
+
width >= BREAKPOINTS.xl
|
|
36
|
+
? 'xl'
|
|
37
|
+
: width >= BREAKPOINTS.lg
|
|
38
|
+
? 'lg'
|
|
39
|
+
: width >= BREAKPOINTS.md
|
|
40
|
+
? 'md'
|
|
41
|
+
: width >= BREAKPOINTS.sm
|
|
42
|
+
? 'sm'
|
|
43
|
+
: 'base';
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
current,
|
|
47
|
+
isAtLeast: (breakpoint) => width >= BREAKPOINTS[breakpoint],
|
|
48
|
+
width,
|
|
49
|
+
height,
|
|
50
|
+
isLandscape: width > height,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
export interface UseCopyToClipboardOptions {
|
|
4
|
+
/** Milliseconds before `copied` flips back. */
|
|
5
|
+
timeout?: number;
|
|
6
|
+
onCopy?: () => void;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface UseCopyToClipboardResult {
|
|
10
|
+
/** Writes `value` to the clipboard and sets `copied`. */
|
|
11
|
+
copy: (value: string) => Promise<void>;
|
|
12
|
+
copied: boolean;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Copy text to the clipboard, with a temporary "copied" state.
|
|
17
|
+
*
|
|
18
|
+
* Requires `expo-clipboard`, which is an optional peer dependency — React
|
|
19
|
+
* Native removed `Clipboard` from core, and pulling in a required native
|
|
20
|
+
* module for one hook would cost every consumer who never calls it:
|
|
21
|
+
*
|
|
22
|
+
* ```sh
|
|
23
|
+
* npx expo install expo-clipboard
|
|
24
|
+
* ```
|
|
25
|
+
*
|
|
26
|
+
* ```tsx
|
|
27
|
+
* const { copy, copied } = useCopyToClipboard();
|
|
28
|
+
* <Button onPress={() => copy(url)}>{copied ? 'Copied' : 'Copy link'}</Button>
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
export function useCopyToClipboard({
|
|
32
|
+
timeout = 2000,
|
|
33
|
+
onCopy,
|
|
34
|
+
}: UseCopyToClipboardOptions = {}): UseCopyToClipboardResult {
|
|
35
|
+
const [copied, setCopied] = useState(false);
|
|
36
|
+
const timer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
37
|
+
|
|
38
|
+
// Never leave a timer running against an unmounted component.
|
|
39
|
+
useEffect(
|
|
40
|
+
() => () => {
|
|
41
|
+
if (timer.current) clearTimeout(timer.current);
|
|
42
|
+
},
|
|
43
|
+
[]
|
|
44
|
+
);
|
|
45
|
+
|
|
46
|
+
const copy = useCallback(
|
|
47
|
+
async (value: string) => {
|
|
48
|
+
const setString = await loadClipboard();
|
|
49
|
+
await setString(value);
|
|
50
|
+
|
|
51
|
+
setCopied(true);
|
|
52
|
+
onCopy?.();
|
|
53
|
+
|
|
54
|
+
if (timer.current) clearTimeout(timer.current);
|
|
55
|
+
if (timeout > 0) {
|
|
56
|
+
timer.current = setTimeout(() => setCopied(false), timeout);
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
[timeout, onCopy]
|
|
60
|
+
);
|
|
61
|
+
|
|
62
|
+
return { copy, copied };
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Resolves expo-clipboard lazily, so apps that never call this hook are not
|
|
67
|
+
* asked to install it. The import is awaited rather than top-level for the
|
|
68
|
+
* same reason.
|
|
69
|
+
*/
|
|
70
|
+
async function loadClipboard(): Promise<(value: string) => Promise<unknown>> {
|
|
71
|
+
try {
|
|
72
|
+
const clipboard = await import('expo-clipboard');
|
|
73
|
+
return clipboard.setStringAsync;
|
|
74
|
+
} catch {
|
|
75
|
+
throw new Error(
|
|
76
|
+
'useCopyToClipboard requires expo-clipboard. Install it with: npx expo install expo-clipboard'
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A copy of `value` that only updates once it has stopped changing for
|
|
5
|
+
* `delay` ms — for search fields, where you want the typed value on screen
|
|
6
|
+
* immediately but the query fired once.
|
|
7
|
+
*
|
|
8
|
+
* ```tsx
|
|
9
|
+
* const [query, setQuery] = useState('');
|
|
10
|
+
* const debounced = useDebouncedValue(query, 300);
|
|
11
|
+
* useEffect(() => { search(debounced); }, [debounced]);
|
|
12
|
+
* ```
|
|
13
|
+
*/
|
|
14
|
+
export function useDebouncedValue<T>(value: T, delay = 300): T {
|
|
15
|
+
const [debounced, setDebounced] = useState(value);
|
|
16
|
+
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
const timer = setTimeout(() => setDebounced(value), delay);
|
|
19
|
+
return () => clearTimeout(timer);
|
|
20
|
+
}, [value, delay]);
|
|
21
|
+
|
|
22
|
+
return debounced;
|
|
23
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { useCallback, useMemo, useState } from 'react';
|
|
2
|
+
|
|
3
|
+
export interface UseDisclosureOptions {
|
|
4
|
+
defaultOpen?: boolean;
|
|
5
|
+
/** Controlled open state. Pass this to drive it from outside. */
|
|
6
|
+
open?: boolean;
|
|
7
|
+
onOpenChange?: (open: boolean) => void;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface UseDisclosureResult {
|
|
11
|
+
isOpen: boolean;
|
|
12
|
+
open: () => void;
|
|
13
|
+
close: () => void;
|
|
14
|
+
toggle: () => void;
|
|
15
|
+
/** Spread onto Dialog, BottomSheet or Select. */
|
|
16
|
+
props: { open: boolean; onOpenChange: (open: boolean) => void };
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Open/closed state for an overlay.
|
|
21
|
+
*
|
|
22
|
+
* The same controlled-or-uncontrolled pattern Dialog, BottomSheet and Select
|
|
23
|
+
* implement internally, lifted out for when you need to drive one from
|
|
24
|
+
* elsewhere — closing a sheet after a request settles, say.
|
|
25
|
+
*
|
|
26
|
+
* ```tsx
|
|
27
|
+
* const sheet = useDisclosure();
|
|
28
|
+
* <Button onPress={sheet.open}>Share</Button>
|
|
29
|
+
* <BottomSheet {...sheet.props}>…</BottomSheet>
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
export function useDisclosure({
|
|
33
|
+
defaultOpen = false,
|
|
34
|
+
open: controlledOpen,
|
|
35
|
+
onOpenChange,
|
|
36
|
+
}: UseDisclosureOptions = {}): UseDisclosureResult {
|
|
37
|
+
const [uncontrolledOpen, setUncontrolledOpen] = useState(defaultOpen);
|
|
38
|
+
const isControlled = controlledOpen !== undefined;
|
|
39
|
+
const isOpen = isControlled ? controlledOpen : uncontrolledOpen;
|
|
40
|
+
|
|
41
|
+
const setOpen = useCallback(
|
|
42
|
+
(next: boolean) => {
|
|
43
|
+
if (!isControlled) setUncontrolledOpen(next);
|
|
44
|
+
onOpenChange?.(next);
|
|
45
|
+
},
|
|
46
|
+
[isControlled, onOpenChange]
|
|
47
|
+
);
|
|
48
|
+
|
|
49
|
+
const open = useCallback(() => setOpen(true), [setOpen]);
|
|
50
|
+
const close = useCallback(() => setOpen(false), [setOpen]);
|
|
51
|
+
const toggle = useCallback(() => setOpen(!isOpen), [setOpen, isOpen]);
|
|
52
|
+
|
|
53
|
+
return useMemo(
|
|
54
|
+
() => ({ isOpen, open, close, toggle, props: { open: isOpen, onOpenChange: setOpen } }),
|
|
55
|
+
[isOpen, open, close, toggle, setOpen]
|
|
56
|
+
);
|
|
57
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* useKeyboardAvoidance — lift an element just clear of the software keyboard.
|
|
3
|
+
*
|
|
4
|
+
* `KeyboardAvoidingView` shifts or pads an entire subtree by the full keyboard
|
|
5
|
+
* height regardless of where the element actually sits, which over-scrolls
|
|
6
|
+
* short forms and does nothing useful for an element already above the fold.
|
|
7
|
+
* This measures the element and moves it by exactly the overlap — and not at
|
|
8
|
+
* all when there is none.
|
|
9
|
+
*
|
|
10
|
+
* ## Install the keyboard controller
|
|
11
|
+
*
|
|
12
|
+
* ```sh
|
|
13
|
+
* npx expo install react-native-keyboard-controller
|
|
14
|
+
* ```
|
|
15
|
+
*
|
|
16
|
+
* It is an optional peer, but on Android it is close to required. Reanimated's
|
|
17
|
+
* own `useAnimatedKeyboard` — the fallback used when the controller is absent —
|
|
18
|
+
* is deprecated in Reanimated 4, and merely *calling* it switches Android out
|
|
19
|
+
* of `adjustResize` into manual inset handling for the whole app. That is a
|
|
20
|
+
* global side effect from a local hook, and it is why keyboard avoidance built
|
|
21
|
+
* on it tends to work on iOS and break on Android.
|
|
22
|
+
*
|
|
23
|
+
* With the controller installed, wrap your app in its `KeyboardProvider` —
|
|
24
|
+
* `PanelUIProvider` does this for you when the package is present.
|
|
25
|
+
*
|
|
26
|
+
* ```tsx
|
|
27
|
+
* const { ref, onLayout, animatedStyle } = useKeyboardAvoidance();
|
|
28
|
+
*
|
|
29
|
+
* <Animated.View ref={ref} onLayout={onLayout} style={animatedStyle}>
|
|
30
|
+
* <TextInput />
|
|
31
|
+
* </Animated.View>
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
import { useCallback } from 'react';
|
|
35
|
+
import { useWindowDimensions, type LayoutChangeEvent, type View } from 'react-native';
|
|
36
|
+
import {
|
|
37
|
+
useAnimatedKeyboard,
|
|
38
|
+
useAnimatedRef,
|
|
39
|
+
useAnimatedStyle,
|
|
40
|
+
useSharedValue,
|
|
41
|
+
type AnimatedRef,
|
|
42
|
+
type SharedValue,
|
|
43
|
+
} from 'react-native-reanimated';
|
|
44
|
+
|
|
45
|
+
export interface UseKeyboardAvoidanceOptions {
|
|
46
|
+
/** Set false to leave the element where it is. */
|
|
47
|
+
enabled?: boolean;
|
|
48
|
+
/** Gap to keep between the element's bottom edge and the keyboard. */
|
|
49
|
+
offset?: number;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface UseKeyboardAvoidanceResult {
|
|
53
|
+
/** Attach to the element that should stay visible. */
|
|
54
|
+
ref: AnimatedRef<View>;
|
|
55
|
+
/** Attach to the same element — it is how the resting position is measured. */
|
|
56
|
+
onLayout: (event: LayoutChangeEvent) => void;
|
|
57
|
+
/** Apply to the same element. */
|
|
58
|
+
animatedStyle: ReturnType<typeof useAnimatedStyle>;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
type KeyboardHeightHook = () => SharedValue<number>;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Resolved once, at module load, so the hook below always calls the same
|
|
65
|
+
* underlying hook — swapping between them per render would break the rules of
|
|
66
|
+
* hooks.
|
|
67
|
+
*/
|
|
68
|
+
const useKeyboardHeight: KeyboardHeightHook = (() => {
|
|
69
|
+
try {
|
|
70
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
71
|
+
const controller = require('react-native-keyboard-controller');
|
|
72
|
+
if (typeof controller?.useReanimatedKeyboardAnimation === 'function') {
|
|
73
|
+
return () => {
|
|
74
|
+
// The controller reports height as a negative offset, matching the
|
|
75
|
+
// translation you would apply. Normalise it to a positive height so
|
|
76
|
+
// the arithmetic below reads the same either way.
|
|
77
|
+
const { height } = controller.useReanimatedKeyboardAnimation();
|
|
78
|
+
return height as SharedValue<number>;
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
} catch {
|
|
82
|
+
// Not installed — fall through.
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return () => useAnimatedKeyboard().height;
|
|
86
|
+
})();
|
|
87
|
+
|
|
88
|
+
/** True when the keyboard controller is driving this, rather than the fallback. */
|
|
89
|
+
export function hasKeyboardController(): boolean {
|
|
90
|
+
try {
|
|
91
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
92
|
+
return typeof require('react-native-keyboard-controller')
|
|
93
|
+
?.useReanimatedKeyboardAnimation === 'function';
|
|
94
|
+
} catch {
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function useKeyboardAvoidance({
|
|
100
|
+
enabled = true,
|
|
101
|
+
offset = 16,
|
|
102
|
+
}: UseKeyboardAvoidanceOptions = {}): UseKeyboardAvoidanceResult {
|
|
103
|
+
const ref = useAnimatedRef<View>();
|
|
104
|
+
const rawHeight = useKeyboardHeight();
|
|
105
|
+
const { height: screenHeight } = useWindowDimensions();
|
|
106
|
+
|
|
107
|
+
/** Window-space bottom edge of the element with no translation applied. */
|
|
108
|
+
const restingBottom = useSharedValue(0);
|
|
109
|
+
|
|
110
|
+
const measure = useCallback(() => {
|
|
111
|
+
ref.current?.measureInWindow((_x, y, _width, height) => {
|
|
112
|
+
if (height > 0) restingBottom.value = y + height;
|
|
113
|
+
});
|
|
114
|
+
}, [ref, restingBottom]);
|
|
115
|
+
|
|
116
|
+
const onLayout = useCallback(
|
|
117
|
+
(_event: LayoutChangeEvent) => {
|
|
118
|
+
// measureInWindow is only meaningful once the view is attached and
|
|
119
|
+
// positioned, which is a frame later than onLayout on both platforms.
|
|
120
|
+
requestAnimationFrame(measure);
|
|
121
|
+
},
|
|
122
|
+
[measure]
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
const animatedStyle = useAnimatedStyle(() => {
|
|
126
|
+
// Both sources are normalised to a positive height here.
|
|
127
|
+
const keyboardHeight = Math.abs(rawHeight.value);
|
|
128
|
+
|
|
129
|
+
if (!enabled || keyboardHeight === 0 || restingBottom.value === 0) {
|
|
130
|
+
return { transform: [{ translateY: 0 }] };
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const keyboardTop = screenHeight - keyboardHeight;
|
|
134
|
+
const overlap = restingBottom.value + offset - keyboardTop;
|
|
135
|
+
|
|
136
|
+
return { transform: [{ translateY: overlap > 0 ? -overlap : 0 }] };
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
return { ref, onLayout, animatedStyle };
|
|
140
|
+
}
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
import { useEffect, useState } from 'react';
|
|
2
|
+
import { Keyboard, Platform } from 'react-native';
|
|
3
|
+
|
|
4
|
+
export interface UseKeyboardResult {
|
|
5
|
+
/** Height of the keyboard in px, or 0 when hidden. */
|
|
6
|
+
height: number;
|
|
7
|
+
isVisible: boolean;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Keyboard height and visibility, on the JS thread.
|
|
12
|
+
*
|
|
13
|
+
* Use this when the value drives rendering — showing a hint, swapping a
|
|
14
|
+
* layout. For *animating* against the keyboard, use Reanimated's
|
|
15
|
+
* `useAnimatedKeyboard` instead: it runs on the UI thread and tracks the
|
|
16
|
+
* keyboard frame-for-frame, where this fires once per show/hide.
|
|
17
|
+
*
|
|
18
|
+
* ```tsx
|
|
19
|
+
* const { isVisible } = useKeyboard();
|
|
20
|
+
* {isVisible ? null : <Footer />}
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
export function useKeyboard(): UseKeyboardResult {
|
|
24
|
+
const [height, setHeight] = useState(0);
|
|
25
|
+
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
// iOS reports the frame before it animates; Android only on completion.
|
|
28
|
+
const showEvent = Platform.OS === 'ios' ? 'keyboardWillShow' : 'keyboardDidShow';
|
|
29
|
+
const hideEvent = Platform.OS === 'ios' ? 'keyboardWillHide' : 'keyboardDidHide';
|
|
30
|
+
|
|
31
|
+
const show = Keyboard.addListener(showEvent, (event) =>
|
|
32
|
+
setHeight(event.endCoordinates.height)
|
|
33
|
+
);
|
|
34
|
+
const hide = Keyboard.addListener(hideEvent, () => setHeight(0));
|
|
35
|
+
|
|
36
|
+
return () => {
|
|
37
|
+
show.remove();
|
|
38
|
+
hide.remove();
|
|
39
|
+
};
|
|
40
|
+
}, []);
|
|
41
|
+
|
|
42
|
+
return { height, isVisible: height > 0 };
|
|
43
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { useEffect, useRef } from 'react';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* The value from the previous render, or `undefined` on the first.
|
|
5
|
+
*
|
|
6
|
+
* Useful for reacting to a transition rather than a state — firing only when
|
|
7
|
+
* something *became* true.
|
|
8
|
+
*
|
|
9
|
+
* ```tsx
|
|
10
|
+
* const wasOpen = usePrevious(isOpen);
|
|
11
|
+
* useEffect(() => {
|
|
12
|
+
* if (isOpen && !wasOpen) track('sheet_opened');
|
|
13
|
+
* }, [isOpen, wasOpen]);
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
export function usePrevious<T>(value: T): T | undefined {
|
|
17
|
+
const ref = useRef<T | undefined>(undefined);
|
|
18
|
+
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
ref.current = value;
|
|
21
|
+
}, [value]);
|
|
22
|
+
|
|
23
|
+
return ref.current;
|
|
24
|
+
}
|
package/src/icons/index.tsx
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
* Icon set.
|
|
3
3
|
*
|
|
4
4
|
* Two families live here:
|
|
5
|
-
* - Stroked 24×24 icons
|
|
6
|
-
*
|
|
7
|
-
* - Filled 16×16 status icons for Alert and Toast indicators
|
|
8
|
-
*
|
|
5
|
+
* - Stroked 24×24 icons on Lucide geometry, for chrome such as chevrons and
|
|
6
|
+
* the close button.
|
|
7
|
+
* - Filled 16×16 status icons for Alert and Toast indicators, which read
|
|
8
|
+
* better small than a stroked equivalent would.
|
|
9
9
|
*
|
|
10
10
|
* Every icon resolves its colour in the same order: an explicit `color` prop,
|
|
11
11
|
* then the colour inherited from an enclosing `IconColorProvider`, then its
|
package/src/index.ts
CHANGED
|
@@ -17,11 +17,18 @@ export {
|
|
|
17
17
|
// Primitives
|
|
18
18
|
export { Portal, PortalHost, PortalProvider } from './primitives/portal';
|
|
19
19
|
export { Text, type TextProps } from './primitives/text';
|
|
20
|
+
export {
|
|
21
|
+
KeyboardAvoider,
|
|
22
|
+
type KeyboardAvoiderProps,
|
|
23
|
+
} from './primitives/keyboard-avoider';
|
|
20
24
|
export {
|
|
21
25
|
AnimatedPressable,
|
|
22
26
|
type AnimatedPressableProps,
|
|
23
27
|
} from './primitives/animated-pressable';
|
|
24
28
|
|
|
29
|
+
// Native UI bridge
|
|
30
|
+
export { hasNativeUI } from './native';
|
|
31
|
+
|
|
25
32
|
// Components
|
|
26
33
|
export {
|
|
27
34
|
Accordion,
|
|
@@ -83,13 +90,43 @@ export {
|
|
|
83
90
|
type InputGroupInputProps,
|
|
84
91
|
type InputGroupDecoratorProps,
|
|
85
92
|
} from './components/input-group';
|
|
93
|
+
export {
|
|
94
|
+
Item,
|
|
95
|
+
type ItemProps,
|
|
96
|
+
type ItemGroupProps,
|
|
97
|
+
type ItemSeparatorProps,
|
|
98
|
+
type ItemMediaProps,
|
|
99
|
+
type ItemContentProps,
|
|
100
|
+
type ItemTitleProps,
|
|
101
|
+
type ItemDescriptionProps,
|
|
102
|
+
type ItemActionsProps,
|
|
103
|
+
type ItemHeaderProps,
|
|
104
|
+
type ItemFooterProps,
|
|
105
|
+
} from './components/item';
|
|
86
106
|
export { Label, type LabelProps, type LabelTextProps } from './components/label';
|
|
107
|
+
export {
|
|
108
|
+
Message,
|
|
109
|
+
type MessageProps,
|
|
110
|
+
type MessageGroupProps,
|
|
111
|
+
type MessageAvatarProps,
|
|
112
|
+
type MessageContentProps,
|
|
113
|
+
type MessageHeaderProps,
|
|
114
|
+
type MessageBubbleProps,
|
|
115
|
+
type MessageBubbleContentProps,
|
|
116
|
+
type MessageFooterProps,
|
|
117
|
+
type MessageActionsProps,
|
|
118
|
+
} from './components/message';
|
|
87
119
|
export { Progress, type ProgressProps } from './components/progress';
|
|
88
120
|
export {
|
|
89
121
|
RadioGroup,
|
|
90
122
|
type RadioGroupProps,
|
|
91
123
|
type RadioGroupItemProps,
|
|
92
124
|
} from './components/radio-group';
|
|
125
|
+
export {
|
|
126
|
+
ScrollFade,
|
|
127
|
+
type ScrollFadeProps,
|
|
128
|
+
} from './components/scroll-fade';
|
|
129
|
+
export { Shimmer, type ShimmerProps } from './components/shimmer';
|
|
93
130
|
export { Skeleton, type SkeletonProps } from './components/skeleton';
|
|
94
131
|
export { Spinner, type SpinnerProps } from './components/spinner';
|
|
95
132
|
export {
|
|
@@ -102,6 +139,7 @@ export {
|
|
|
102
139
|
type StepState,
|
|
103
140
|
type StepsOrientation,
|
|
104
141
|
} from './components/steps';
|
|
142
|
+
export { Surface, type SurfaceProps } from './components/surface';
|
|
105
143
|
export { Switch, type SwitchProps } from './components/switch';
|
|
106
144
|
export {
|
|
107
145
|
Timeline,
|
|
@@ -166,5 +204,8 @@ export {
|
|
|
166
204
|
type IconProps,
|
|
167
205
|
} from './icons';
|
|
168
206
|
|
|
207
|
+
// Hooks
|
|
208
|
+
export * from './hooks';
|
|
209
|
+
|
|
169
210
|
// Utils
|
|
170
211
|
export { cn } from './utils/cn';
|