panelui-native 0.6.0 → 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.
Files changed (133) hide show
  1. package/README.md +1 -1
  2. package/lib/module/components/accordion/index.js +7 -5
  3. package/lib/module/components/accordion/index.js.map +1 -1
  4. package/lib/module/components/alert/index.js +3 -6
  5. package/lib/module/components/alert/index.js.map +1 -1
  6. package/lib/module/components/bottom-sheet/index.js +67 -3
  7. package/lib/module/components/bottom-sheet/index.js.map +1 -1
  8. package/lib/module/components/button/index.js +36 -1
  9. package/lib/module/components/button/index.js.map +1 -1
  10. package/lib/module/components/empty-state/index.js +4 -5
  11. package/lib/module/components/empty-state/index.js.map +1 -1
  12. package/lib/module/components/inline-select/index.js +171 -46
  13. package/lib/module/components/inline-select/index.js.map +1 -1
  14. package/lib/module/components/input/index.js +28 -5
  15. package/lib/module/components/input/index.js.map +1 -1
  16. package/lib/module/components/input-group/index.js +0 -1
  17. package/lib/module/components/input-group/index.js.map +1 -1
  18. package/lib/module/components/item/index.js +359 -0
  19. package/lib/module/components/item/index.js.map +1 -0
  20. package/lib/module/components/label/index.js +5 -3
  21. package/lib/module/components/label/index.js.map +1 -1
  22. package/lib/module/components/message/index.js +318 -0
  23. package/lib/module/components/message/index.js.map +1 -0
  24. package/lib/module/components/scroll-fade/index.js +119 -71
  25. package/lib/module/components/scroll-fade/index.js.map +1 -1
  26. package/lib/module/components/select/index.js +35 -6
  27. package/lib/module/components/select/index.js.map +1 -1
  28. package/lib/module/components/shimmer/index.js +111 -64
  29. package/lib/module/components/shimmer/index.js.map +1 -1
  30. package/lib/module/components/surface/index.js +3 -2
  31. package/lib/module/components/surface/index.js.map +1 -1
  32. package/lib/module/components/switch/index.js +20 -1
  33. package/lib/module/components/switch/index.js.map +1 -1
  34. package/lib/module/components/toast/index.js +7 -8
  35. package/lib/module/components/toast/index.js.map +1 -1
  36. package/lib/module/components/typography/index.js +3 -3
  37. package/lib/module/hooks/index.js +1 -0
  38. package/lib/module/hooks/index.js.map +1 -1
  39. package/lib/module/hooks/use-breakpoint.js +2 -2
  40. package/lib/module/hooks/use-copy-to-clipboard.js +0 -2
  41. package/lib/module/hooks/use-copy-to-clipboard.js.map +1 -1
  42. package/lib/module/hooks/use-keyboard-avoidance.js +120 -0
  43. package/lib/module/hooks/use-keyboard-avoidance.js.map +1 -0
  44. package/lib/module/icons/index.js +4 -4
  45. package/lib/module/index.js +6 -0
  46. package/lib/module/index.js.map +1 -1
  47. package/lib/module/native/index.js +53 -0
  48. package/lib/module/native/index.js.map +1 -0
  49. package/lib/module/primitives/keyboard-avoider.js +40 -0
  50. package/lib/module/primitives/keyboard-avoider.js.map +1 -0
  51. package/lib/module/providers/panel-ui-provider.js +24 -4
  52. package/lib/module/providers/panel-ui-provider.js.map +1 -1
  53. package/lib/module/theme/use-theme.js +1 -1
  54. package/lib/typescript/src/components/accordion/index.d.ts +7 -5
  55. package/lib/typescript/src/components/accordion/index.d.ts.map +1 -1
  56. package/lib/typescript/src/components/alert/index.d.ts +3 -6
  57. package/lib/typescript/src/components/alert/index.d.ts.map +1 -1
  58. package/lib/typescript/src/components/bottom-sheet/index.d.ts +21 -1
  59. package/lib/typescript/src/components/bottom-sheet/index.d.ts.map +1 -1
  60. package/lib/typescript/src/components/button/index.d.ts +11 -0
  61. package/lib/typescript/src/components/button/index.d.ts.map +1 -1
  62. package/lib/typescript/src/components/empty-state/index.d.ts +4 -5
  63. package/lib/typescript/src/components/empty-state/index.d.ts.map +1 -1
  64. package/lib/typescript/src/components/inline-select/index.d.ts +29 -6
  65. package/lib/typescript/src/components/inline-select/index.d.ts.map +1 -1
  66. package/lib/typescript/src/components/input/index.d.ts +13 -0
  67. package/lib/typescript/src/components/input/index.d.ts.map +1 -1
  68. package/lib/typescript/src/components/input-group/index.d.ts +0 -1
  69. package/lib/typescript/src/components/input-group/index.d.ts.map +1 -1
  70. package/lib/typescript/src/components/item/index.d.ts +286 -0
  71. package/lib/typescript/src/components/item/index.d.ts.map +1 -0
  72. package/lib/typescript/src/components/label/index.d.ts +5 -3
  73. package/lib/typescript/src/components/label/index.d.ts.map +1 -1
  74. package/lib/typescript/src/components/message/index.d.ts +170 -0
  75. package/lib/typescript/src/components/message/index.d.ts.map +1 -0
  76. package/lib/typescript/src/components/scroll-fade/index.d.ts +18 -9
  77. package/lib/typescript/src/components/scroll-fade/index.d.ts.map +1 -1
  78. package/lib/typescript/src/components/select/index.d.ts +16 -1
  79. package/lib/typescript/src/components/select/index.d.ts.map +1 -1
  80. package/lib/typescript/src/components/shimmer/index.d.ts +38 -20
  81. package/lib/typescript/src/components/shimmer/index.d.ts.map +1 -1
  82. package/lib/typescript/src/components/surface/index.d.ts.map +1 -1
  83. package/lib/typescript/src/components/switch/index.d.ts +10 -0
  84. package/lib/typescript/src/components/switch/index.d.ts.map +1 -1
  85. package/lib/typescript/src/components/toast/index.d.ts +4 -4
  86. package/lib/typescript/src/components/toast/index.d.ts.map +1 -1
  87. package/lib/typescript/src/hooks/index.d.ts +1 -0
  88. package/lib/typescript/src/hooks/index.d.ts.map +1 -1
  89. package/lib/typescript/src/hooks/use-breakpoint.d.ts +2 -2
  90. package/lib/typescript/src/hooks/use-copy-to-clipboard.d.ts +0 -2
  91. package/lib/typescript/src/hooks/use-copy-to-clipboard.d.ts.map +1 -1
  92. package/lib/typescript/src/hooks/use-keyboard-avoidance.d.ts +20 -0
  93. package/lib/typescript/src/hooks/use-keyboard-avoidance.d.ts.map +1 -0
  94. package/lib/typescript/src/icons/index.d.ts +4 -4
  95. package/lib/typescript/src/index.d.ts +4 -0
  96. package/lib/typescript/src/index.d.ts.map +1 -1
  97. package/lib/typescript/src/native/index.d.ts +35 -0
  98. package/lib/typescript/src/native/index.d.ts.map +1 -0
  99. package/lib/typescript/src/primitives/keyboard-avoider.d.ts +22 -0
  100. package/lib/typescript/src/primitives/keyboard-avoider.d.ts.map +1 -0
  101. package/lib/typescript/src/providers/panel-ui-provider.d.ts +1 -1
  102. package/lib/typescript/src/providers/panel-ui-provider.d.ts.map +1 -1
  103. package/lib/typescript/src/theme/use-theme.d.ts +1 -1
  104. package/package.json +12 -3
  105. package/src/components/accordion/index.tsx +7 -5
  106. package/src/components/alert/index.tsx +3 -6
  107. package/src/components/bottom-sheet/index.tsx +82 -2
  108. package/src/components/button/index.tsx +52 -0
  109. package/src/components/empty-state/index.tsx +4 -5
  110. package/src/components/inline-select/index.tsx +191 -44
  111. package/src/components/input/index.tsx +39 -3
  112. package/src/components/input-group/index.tsx +0 -1
  113. package/src/components/item/index.tsx +387 -0
  114. package/src/components/label/index.tsx +5 -3
  115. package/src/components/message/index.tsx +312 -0
  116. package/src/components/scroll-fade/index.tsx +163 -51
  117. package/src/components/select/index.tsx +54 -9
  118. package/src/components/shimmer/index.tsx +151 -58
  119. package/src/components/surface/index.tsx +3 -2
  120. package/src/components/switch/index.tsx +27 -1
  121. package/src/components/toast/index.tsx +7 -8
  122. package/src/components/typography/index.tsx +3 -3
  123. package/src/hooks/index.ts +5 -0
  124. package/src/hooks/use-breakpoint.ts +2 -2
  125. package/src/hooks/use-copy-to-clipboard.ts +0 -2
  126. package/src/hooks/use-keyboard-avoidance.ts +140 -0
  127. package/src/icons/index.tsx +4 -4
  128. package/src/index.ts +32 -0
  129. package/src/native/index.ts +79 -0
  130. package/src/primitives/keyboard-avoider.tsx +49 -0
  131. package/src/providers/panel-ui-provider.tsx +30 -9
  132. package/src/theme/use-theme.ts +1 -1
  133. package/theme.css +5 -3
@@ -1,24 +1,29 @@
1
1
  /**
2
- * Shimmer — an animated highlight sweeping across content.
2
+ * Shimmer — a highlight that sweeps *through* content rather than over it.
3
3
  *
4
- * The React Native equivalent of shadcn-ui/ui's `shimmer` utility
5
- * (https://ui.shadcn.com/docs/utils/shimmer). That one is CSS:
6
- * `background-clip: text` with a `color-mix()` highlight. React Native has
7
- * neither, so the highlight is drawn as an SVG gradient band swept across an
8
- * overlay by Reanimated, on the UI thread.
9
- *
10
- * The visible difference: the band passes *over* the content rather than
11
- * being clipped to glyph shapes. On body-sized text the two read the same; on
12
- * very large text the web version is crisper.
4
+ * The content is used as a mask, and a gradient band is swept behind it on the
5
+ * UI thread. Because the content is the mask, the highlight is clipped to the
6
+ * glyph shapes: text reads as though the letters themselves are catching a
7
+ * light, which is the "thinking…" treatment familiar from AI chat interfaces.
13
8
  *
14
9
  * ```tsx
15
- * <Shimmer>
16
- * <Text muted>Generating response…</Text>
17
- * </Shimmer>
10
+ * <Shimmer>Thinking…</Shimmer>
18
11
  * ```
12
+ *
13
+ * Set `as="view"` to shimmer arbitrary children (skeleton blocks, cards) — the
14
+ * mask is then the rendered subtree's alpha instead of a line of text.
19
15
  */
20
16
  import { useEffect, useState, type ReactNode } from 'react';
21
- import { View, type LayoutChangeEvent, type ViewProps } from 'react-native';
17
+ import {
18
+ StyleSheet,
19
+ View,
20
+ type LayoutChangeEvent,
21
+ type StyleProp,
22
+ type TextStyle,
23
+ type ViewProps,
24
+ } from 'react-native';
25
+ import MaskedView from '@react-native-masked-view/masked-view';
26
+ import { LinearGradient } from 'expo-linear-gradient';
22
27
  import Animated, {
23
28
  cancelAnimation,
24
29
  Easing,
@@ -28,35 +33,64 @@ import Animated, {
28
33
  withRepeat,
29
34
  withTiming,
30
35
  } from 'react-native-reanimated';
31
- import Svg, { Defs, LinearGradient, Rect, Stop } from 'react-native-svg';
32
36
  import { useCSSVariable } from 'uniwind';
37
+ import { Text } from '../../primitives/text';
33
38
  import { cn } from '../../utils/cn';
34
39
 
35
40
  export interface ShimmerProps extends ViewProps {
36
41
  className?: string;
42
+ /**
43
+ * `text` renders `children` as a single styled string and masks the sweep to
44
+ * the glyphs. `view` masks the sweep to whatever subtree you pass.
45
+ */
46
+ as?: 'text' | 'view';
37
47
  /** Milliseconds for one sweep. */
38
48
  duration?: number;
39
- /** Sweep once instead of looping. */
49
+ /** Width of the highlight band, as a multiple of the content width. */
50
+ spread?: number;
51
+ /** Colour of the content at rest. Defaults to the theme's muted foreground. */
52
+ baseColor?: string;
53
+ /** Colour at the centre of the sweep. Defaults to the theme's foreground. */
54
+ shimmerColor?: string;
55
+ /** `loop` restarts from the left; `ping-pong` reverses on each pass. */
56
+ mode?: 'loop' | 'ping-pong';
57
+ /** Sweep once instead of repeating. */
40
58
  once?: boolean;
41
59
  /** Sweep right-to-left. */
42
60
  reverse?: boolean;
43
- /** Width of the highlight band, as a fraction of the content width. */
44
- spread?: number;
45
- /** Highlight colour. Defaults to the theme's foreground. */
61
+ /** Set false to render the content statically without animating. */
62
+ enabled?: boolean;
63
+ /** Extra classes for the text when `as="text"`. */
64
+ textClassName?: string;
65
+ /** Extra styles for the text when `as="text"`. */
66
+ textStyle?: StyleProp<TextStyle>;
67
+ /**
68
+ * @deprecated Use `shimmerColor`.
69
+ */
46
70
  color?: string;
47
- /** Peak opacity of the highlight. */
71
+ /**
72
+ * Peak opacity of the highlight.
73
+ * @deprecated Set `shimmerColor` to a colour with the alpha you want.
74
+ */
48
75
  intensity?: number;
49
76
  children?: ReactNode;
50
77
  }
51
78
 
52
79
  export function Shimmer({
53
80
  className,
81
+ as = 'text',
54
82
  duration = 2000,
83
+ spread = 2,
84
+ baseColor,
85
+ shimmerColor,
86
+ mode = 'loop',
55
87
  once = false,
56
88
  reverse = false,
57
- spread = 0.4,
89
+ enabled = true,
90
+ textClassName,
91
+ textStyle,
58
92
  color,
59
- intensity = 0.45,
93
+ intensity,
60
94
  children,
61
95
  ...props
62
96
  }: ShimmerProps) {
@@ -64,28 +98,39 @@ export function Shimmer({
64
98
  const progress = useSharedValue(0);
65
99
  const reducedMotion = useReducedMotion();
66
100
 
101
+ const themeMuted = useCSSVariable('--color-muted-foreground');
67
102
  const themeForeground = useCSSVariable('--color-foreground');
103
+
104
+ const base =
105
+ baseColor ?? (typeof themeMuted === 'string' ? themeMuted : '#a1a1aa');
68
106
  const highlight =
69
- color ?? (typeof themeForeground === 'string' ? themeForeground : '#ffffff');
107
+ shimmerColor ??
108
+ color ??
109
+ (typeof themeForeground === 'string' ? themeForeground : '#ffffff');
110
+
111
+ // The band is wider than the content so the highlight ramps in and out
112
+ // instead of appearing at full strength against the edge.
113
+ const bandWidth = Math.max(width * spread, 1);
114
+ const animating = enabled && !reducedMotion && width > 0;
70
115
 
71
116
  useEffect(() => {
72
- // Respect the OS setting rather than animating regardless — the same
73
- // thing shadcn's utility does with prefers-reduced-motion.
74
- if (reducedMotion || width === 0) return;
117
+ if (!animating) {
118
+ cancelAnimation(progress);
119
+ progress.value = 0;
120
+ return;
121
+ }
75
122
 
76
123
  progress.value = 0;
77
124
  progress.value = withRepeat(
78
125
  withTiming(1, { duration, easing: Easing.linear }),
79
126
  once ? 1 : -1,
80
- false
127
+ mode === 'ping-pong'
81
128
  );
82
129
 
83
130
  return () => cancelAnimation(progress);
84
- }, [duration, once, reducedMotion, width, progress]);
131
+ }, [animating, duration, mode, once, progress]);
85
132
 
86
- const bandWidth = Math.max(width * spread, 1);
87
-
88
- const bandStyle = useAnimatedStyle(() => {
133
+ const sweepStyle = useAnimatedStyle(() => {
89
134
  // Travel from fully off one edge to fully off the other.
90
135
  const from = -bandWidth;
91
136
  const to = width;
@@ -100,36 +145,84 @@ export function Shimmer({
100
145
  props.onLayout?.(event);
101
146
  };
102
147
 
148
+ const content =
149
+ as === 'text' ? (
150
+ <Text className={textClassName} style={[{ color: base }, textStyle]}>
151
+ {children}
152
+ </Text>
153
+ ) : (
154
+ children
155
+ );
156
+
157
+ // Without a measured width there is nothing to sweep across, and masking an
158
+ // unmeasured subtree just costs a layer — render the content plainly.
159
+ if (!animating) {
160
+ return (
161
+ <View {...props} onLayout={onLayout} className={className}>
162
+ {content}
163
+ </View>
164
+ );
165
+ }
166
+
167
+ const highlightWithAlpha =
168
+ intensity !== undefined ? withAlpha(highlight, intensity) : highlight;
169
+
103
170
  return (
104
- <View
105
- {...props}
106
- onLayout={onLayout}
107
- className={cn('overflow-hidden', className)}
108
- >
109
- {children}
110
-
111
- {reducedMotion || width === 0 ? null : (
112
- <Animated.View
113
- pointerEvents="none"
114
- style={[
115
- { position: 'absolute', top: 0, bottom: 0, width: bandWidth },
116
- bandStyle,
117
- ]}
118
- >
119
- <Svg width="100%" height="100%">
120
- <Defs>
121
- <LinearGradient id="shimmer" x1="0" y1="0" x2="1" y2="0">
122
- <Stop offset="0" stopColor={highlight} stopOpacity={0} />
123
- <Stop offset="0.5" stopColor={highlight} stopOpacity={intensity} />
124
- <Stop offset="1" stopColor={highlight} stopOpacity={0} />
125
- </LinearGradient>
126
- </Defs>
127
- <Rect x="0" y="0" width="100%" height="100%" fill="url(#shimmer)" />
128
- </Svg>
129
- </Animated.View>
130
- )}
171
+ <View {...props} onLayout={onLayout} className={cn('overflow-hidden', className)}>
172
+ <MaskedView maskElement={<View>{content}</View>}>
173
+ {/* The mask only exposes the glyphs, so this layer is what they are
174
+ painted with: flat base colour, plus a moving gradient band. */}
175
+ <View style={{ backgroundColor: base }}>
176
+ <View style={{ opacity: 0 }}>{content}</View>
177
+ <Animated.View
178
+ pointerEvents="none"
179
+ style={[
180
+ StyleSheet.absoluteFill,
181
+ { width: bandWidth, left: 0, right: undefined },
182
+ sweepStyle,
183
+ ]}
184
+ >
185
+ <LinearGradient
186
+ colors={[base, highlightWithAlpha, base]}
187
+ locations={[0, 0.5, 1]}
188
+ start={{ x: 0, y: 0 }}
189
+ end={{ x: 1, y: 0 }}
190
+ style={StyleSheet.absoluteFill}
191
+ />
192
+ </Animated.View>
193
+ </View>
194
+ </MaskedView>
131
195
  </View>
132
196
  );
133
197
  }
134
198
 
135
199
  Shimmer.displayName = 'Shimmer';
200
+
201
+ /** Applies an alpha to a `#rgb`/`#rrggbb`/`rgb()` colour. Supports `intensity`. */
202
+ function withAlpha(color: string, alpha: number): string {
203
+ const clamped = Math.max(0, Math.min(1, alpha));
204
+
205
+ if (color.startsWith('#')) {
206
+ const hex = color.slice(1);
207
+ const full =
208
+ hex.length === 3
209
+ ? hex
210
+ .split('')
211
+ .map((c) => c + c)
212
+ .join('')
213
+ : hex.slice(0, 6);
214
+ const r = parseInt(full.slice(0, 2), 16);
215
+ const g = parseInt(full.slice(2, 4), 16);
216
+ const b = parseInt(full.slice(4, 6), 16);
217
+ if (Number.isNaN(r + g + b)) return color;
218
+ return `rgba(${r}, ${g}, ${b}, ${clamped})`;
219
+ }
220
+
221
+ const channels = color.match(/rgba?\(([^)]+)\)/)?.[1];
222
+ if (channels) {
223
+ const [r, g, b] = channels.split(',').map((part) => part.trim());
224
+ return `rgba(${r}, ${g}, ${b}, ${clamped})`;
225
+ }
226
+
227
+ return color;
228
+ }
@@ -1,8 +1,9 @@
1
1
  /**
2
2
  * Surface — an elevated container.
3
3
  *
4
- * Adapted from: heroui-inc/heroui-native src/components/surface/
5
- * (the variant ladder and the nesting model for building hierarchy).
4
+ * The variants form a ladder rather than a palette: each step is one level
5
+ * further from the background, and nesting them is how you build depth without
6
+ * hardcoding a colour anywhere.
6
7
  *
7
8
  * Both the fill and the corner radius follow the active theme: the variants
8
9
  * resolve to themed `--color-surface-*` tokens, and `rounded-3xl` resolves
@@ -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
- * Adapted from: heroui-inc/heroui-native src/components/toast/
5
- * (the three usage patterns — string, config object, custom component — the
6
- * Title / Description / Action / Close anatomy, placement, and swipe-to-
7
- * dismiss). Styled with Coss UI tokens and PanelUI's Alert status palette.
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/exiting animations, from heroui-inc/heroui-native
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. Matches heroui-native's toast.animation.ts.
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 — matching heroui-native.
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
- * Adapted from: heroui-inc/heroui-native (Typography `type` scale and the
5
- * Heading / Paragraph / Code compound parts), built on PanelUI's Text
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';
@@ -15,5 +15,10 @@ export {
15
15
  type UseBreakpointResult,
16
16
  } from './use-breakpoint';
17
17
  export { useKeyboard, type UseKeyboardResult } from './use-keyboard';
18
+ export {
19
+ useKeyboardAvoidance,
20
+ type UseKeyboardAvoidanceOptions,
21
+ type UseKeyboardAvoidanceResult,
22
+ } from './use-keyboard-avoidance';
18
23
  export { useDebouncedValue } from './use-debounced-value';
19
24
  export { usePrevious } from './use-previous';
@@ -16,8 +16,8 @@ export interface UseBreakpointResult {
16
16
  }
17
17
 
18
18
  /**
19
- * Responsive state from the window size — the React Native answer to
20
- * coss.com/ui's `useMediaQuery`, which has no equivalent here.
19
+ * Responsive state from the window size — the React Native answer to a media
20
+ * query, which has no equivalent here.
21
21
  *
22
22
  * For styling, prefer Uniwind's responsive class prefixes (`md:flex-row`).
23
23
  * Reach for this when the *behaviour* changes, not just the look — rendering
@@ -15,8 +15,6 @@ export interface UseCopyToClipboardResult {
15
15
  /**
16
16
  * Copy text to the clipboard, with a temporary "copied" state.
17
17
  *
18
- * Mirrors coss.com/ui's `useCopyToClipboard`.
19
- *
20
18
  * Requires `expo-clipboard`, which is an optional peer dependency — React
21
19
  * Native removed `Clipboard` from core, and pulling in a required native
22
20
  * module for one hook would cost every consumer who never calls it:
@@ -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
+ }
@@ -2,10 +2,10 @@
2
2
  * Icon set.
3
3
  *
4
4
  * Two families live here:
5
- * - Stroked 24×24 icons (Lucide geometry, as used by shadcn-ui/ui) for chrome
6
- * such as chevrons and the close button.
7
- * - Filled 16×16 status icons for Alert and Toast indicators.
8
- * Adapted from: heroui-inc/heroui-native src/components/alert/{default,success,warning}-icon.tsx
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,7 +90,32 @@ 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,