panelui-native 0.13.0 → 0.14.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 (91) hide show
  1. package/README.md +39 -23
  2. package/lib/module/components/button/index.js +12 -9
  3. package/lib/module/components/button/index.js.map +1 -1
  4. package/lib/module/components/chip/index.js +313 -0
  5. package/lib/module/components/chip/index.js.map +1 -0
  6. package/lib/module/components/frame/index.js +41 -39
  7. package/lib/module/components/frame/index.js.map +1 -1
  8. package/lib/module/components/heatmap-chart/index.js +772 -0
  9. package/lib/module/components/heatmap-chart/index.js.map +1 -0
  10. package/lib/module/components/input/index.js +4 -0
  11. package/lib/module/components/input/index.js.map +1 -1
  12. package/lib/module/components/scroll-canvas/index.js +175 -0
  13. package/lib/module/components/scroll-canvas/index.js.map +1 -0
  14. package/lib/module/components/scroll-text/index.js +213 -0
  15. package/lib/module/components/scroll-text/index.js.map +1 -0
  16. package/lib/module/components/select/index.js +20 -24
  17. package/lib/module/components/select/index.js.map +1 -1
  18. package/lib/module/components/slider/index.js +2 -5
  19. package/lib/module/components/slider/index.js.map +1 -1
  20. package/lib/module/components/switch/index.js +15 -17
  21. package/lib/module/components/switch/index.js.map +1 -1
  22. package/lib/module/components/thinking-orb/index.js +741 -0
  23. package/lib/module/components/thinking-orb/index.js.map +1 -0
  24. package/lib/module/components/toggle-button/index.js +12 -3
  25. package/lib/module/components/toggle-button/index.js.map +1 -1
  26. package/lib/module/hooks/index.js +1 -0
  27. package/lib/module/hooks/index.js.map +1 -1
  28. package/lib/module/hooks/use-keyboard-avoidance.js +99 -59
  29. package/lib/module/hooks/use-keyboard-avoidance.js.map +1 -1
  30. package/lib/module/hooks/use-reveal-progress.js +75 -0
  31. package/lib/module/hooks/use-reveal-progress.js.map +1 -0
  32. package/lib/module/index.js +6 -0
  33. package/lib/module/index.js.map +1 -1
  34. package/lib/module/native/index.js.map +1 -1
  35. package/lib/module/primitives/keyboard-avoider.js +21 -2
  36. package/lib/module/primitives/keyboard-avoider.js.map +1 -1
  37. package/lib/module/primitives/scroll-progress.js +114 -0
  38. package/lib/module/primitives/scroll-progress.js.map +1 -0
  39. package/lib/typescript/src/components/button/index.d.ts +3 -4
  40. package/lib/typescript/src/components/button/index.d.ts.map +1 -1
  41. package/lib/typescript/src/components/chip/index.d.ts +304 -0
  42. package/lib/typescript/src/components/chip/index.d.ts.map +1 -0
  43. package/lib/typescript/src/components/frame/index.d.ts +15 -13
  44. package/lib/typescript/src/components/frame/index.d.ts.map +1 -1
  45. package/lib/typescript/src/components/heatmap-chart/index.d.ts +260 -0
  46. package/lib/typescript/src/components/heatmap-chart/index.d.ts.map +1 -0
  47. package/lib/typescript/src/components/input/index.d.ts +19 -4
  48. package/lib/typescript/src/components/input/index.d.ts.map +1 -1
  49. package/lib/typescript/src/components/scroll-canvas/index.d.ts +66 -0
  50. package/lib/typescript/src/components/scroll-canvas/index.d.ts.map +1 -0
  51. package/lib/typescript/src/components/scroll-text/index.d.ts +46 -0
  52. package/lib/typescript/src/components/scroll-text/index.d.ts.map +1 -0
  53. package/lib/typescript/src/components/select/index.d.ts.map +1 -1
  54. package/lib/typescript/src/components/slider/index.d.ts.map +1 -1
  55. package/lib/typescript/src/components/switch/index.d.ts.map +1 -1
  56. package/lib/typescript/src/components/thinking-orb/index.d.ts +28 -0
  57. package/lib/typescript/src/components/thinking-orb/index.d.ts.map +1 -0
  58. package/lib/typescript/src/components/toggle-button/index.d.ts +12 -0
  59. package/lib/typescript/src/components/toggle-button/index.d.ts.map +1 -1
  60. package/lib/typescript/src/hooks/index.d.ts +2 -1
  61. package/lib/typescript/src/hooks/index.d.ts.map +1 -1
  62. package/lib/typescript/src/hooks/use-keyboard-avoidance.d.ts +16 -3
  63. package/lib/typescript/src/hooks/use-keyboard-avoidance.d.ts.map +1 -1
  64. package/lib/typescript/src/hooks/use-reveal-progress.d.ts +56 -0
  65. package/lib/typescript/src/hooks/use-reveal-progress.d.ts.map +1 -0
  66. package/lib/typescript/src/index.d.ts +6 -0
  67. package/lib/typescript/src/index.d.ts.map +1 -1
  68. package/lib/typescript/src/native/index.d.ts +15 -7
  69. package/lib/typescript/src/native/index.d.ts.map +1 -1
  70. package/lib/typescript/src/primitives/keyboard-avoider.d.ts +30 -3
  71. package/lib/typescript/src/primitives/keyboard-avoider.d.ts.map +1 -1
  72. package/lib/typescript/src/primitives/scroll-progress.d.ts +61 -0
  73. package/lib/typescript/src/primitives/scroll-progress.d.ts.map +1 -0
  74. package/package.json +1 -1
  75. package/src/components/button/index.tsx +13 -11
  76. package/src/components/frame/index.tsx +39 -50
  77. package/src/components/heatmap-chart/index.tsx +1024 -0
  78. package/src/components/input/index.tsx +23 -4
  79. package/src/components/scroll-canvas/index.tsx +220 -0
  80. package/src/components/scroll-text/index.tsx +277 -0
  81. package/src/components/select/index.tsx +4 -9
  82. package/src/components/slider/index.tsx +4 -7
  83. package/src/components/switch/index.tsx +5 -8
  84. package/src/components/thinking-orb/index.tsx +792 -0
  85. package/src/hooks/index.ts +6 -0
  86. package/src/hooks/use-keyboard-avoidance.ts +111 -65
  87. package/src/hooks/use-reveal-progress.ts +106 -0
  88. package/src/index.ts +38 -0
  89. package/src/native/index.ts +15 -7
  90. package/src/primitives/keyboard-avoider.tsx +41 -4
  91. package/src/primitives/scroll-progress.tsx +167 -0
@@ -17,9 +17,15 @@ export {
17
17
  export { useKeyboard, type UseKeyboardResult } from './use-keyboard';
18
18
  export {
19
19
  useKeyboardAvoidance,
20
+ type KeyboardAvoidanceMode,
20
21
  type UseKeyboardAvoidanceOptions,
21
22
  type UseKeyboardAvoidanceResult,
22
23
  } from './use-keyboard-avoidance';
24
+ export {
25
+ useRevealProgress,
26
+ type UseRevealProgressOptions,
27
+ type UseRevealProgressResult,
28
+ } from './use-reveal-progress';
23
29
  export {
24
30
  useScrollSections,
25
31
  type UseScrollSectionsOptions,
@@ -1,11 +1,24 @@
1
1
  /**
2
- * useKeyboardAvoidance — lift an element just clear of the software keyboard.
2
+ * useKeyboardAvoidance — keep an element clear of the software keyboard.
3
3
  *
4
4
  * `KeyboardAvoidingView` shifts or pads an entire subtree by the full keyboard
5
5
  * height regardless of where the element actually sits, which over-scrolls
6
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.
7
+ * This works from the element's own position instead.
8
+ *
9
+ * Two modes, because "get out of the keyboard's way" and "ride the keyboard"
10
+ * are different jobs:
11
+ *
12
+ * - **`lift`** (default) — for a field sitting in the page's flow. The element
13
+ * is measured every frame while the keyboard is up and moved by exactly the
14
+ * current overlap, so it follows the scroll: scroll it clear and the lift
15
+ * decays to nothing, scroll it back under and the lift returns. Measuring
16
+ * once and holding the result is what leaves a field hanging out of its own
17
+ * slot the moment the page moves underneath it.
18
+ * - **`dock`** — for an absolutely-positioned composer, toolbar or search bar
19
+ * pinned near the bottom edge. There is nothing to measure: the element
20
+ * simply travels with the keyboard, less whatever bottom inset it is already
21
+ * sitting above.
9
22
  *
10
23
  * ## Install the keyboard controller
11
24
  *
@@ -31,19 +44,34 @@
31
44
  * </Animated.View>
32
45
  * ```
33
46
  */
34
- import { useCallback, useEffect } from 'react';
47
+ import { useCallback, useEffect, useState } from 'react';
35
48
  import { useWindowDimensions, type LayoutChangeEvent, type View } from 'react-native';
36
49
  import {
37
50
  measure,
51
+ runOnJS,
38
52
  useAnimatedKeyboard,
39
53
  useAnimatedReaction,
40
54
  useAnimatedRef,
41
55
  useAnimatedStyle,
56
+ useFrameCallback,
42
57
  useSharedValue,
58
+ withTiming,
43
59
  type AnimatedRef,
44
60
  type SharedValue,
45
61
  } from 'react-native-reanimated';
46
62
 
63
+ /** How long a lifted element takes to settle back after it stops being active. */
64
+ const SETTLE_DURATION = 200;
65
+
66
+ /**
67
+ * Movement below this is dropped. The element's measured position already
68
+ * includes the translation applied on the previous frame, so the loop reads
69
+ * its own output — a dead band keeps sub-pixel rounding from making it hum.
70
+ */
71
+ const EPSILON = 0.5;
72
+
73
+ export type KeyboardAvoidanceMode = 'lift' | 'dock';
74
+
47
75
  export interface UseKeyboardAvoidanceOptions {
48
76
  /** Set false to leave the element where it is. */
49
77
  enabled?: boolean;
@@ -56,14 +84,26 @@ export interface UseKeyboardAvoidanceOptions {
56
84
  * is tapped, and they all arrive at the same place on top of each other.
57
85
  */
58
86
  active?: boolean;
59
- /** Gap to keep between the element's bottom edge and the keyboard. */
87
+ /**
88
+ * `lift` moves an in-flow element by its overlap with the keyboard and
89
+ * tracks it as the page scrolls. `dock` travels with the keyboard outright,
90
+ * for an element already pinned to the bottom edge.
91
+ */
92
+ mode?: KeyboardAvoidanceMode;
93
+ /** Gap to keep between the element's bottom edge and the keyboard. `lift` only. */
60
94
  offset?: number;
95
+ /**
96
+ * How far above the bottom edge the element already sits — usually the safe
97
+ * area inset it is offset by. Subtracted from the travel, since the keyboard
98
+ * covers that strip too. `dock` only.
99
+ */
100
+ bottomInset?: number;
61
101
  }
62
102
 
63
103
  export interface UseKeyboardAvoidanceResult {
64
104
  /** Attach to the element that should stay visible. */
65
105
  ref: AnimatedRef<View>;
66
- /** Attach to the same element — it is how the resting position is measured. */
106
+ /** Attach to the same element, so a re-layout at rest cannot leave it offset. */
67
107
  onLayout: (event: LayoutChangeEvent) => void;
68
108
  /** Apply to the same element. */
69
109
  animatedStyle: ReturnType<typeof useAnimatedStyle>;
@@ -110,96 +150,102 @@ export function hasKeyboardController(): boolean {
110
150
  export function useKeyboardAvoidance({
111
151
  enabled = true,
112
152
  active = true,
153
+ mode = 'lift',
113
154
  offset = 16,
155
+ bottomInset = 0,
114
156
  }: UseKeyboardAvoidanceOptions = {}): UseKeyboardAvoidanceResult {
115
157
  const ref = useAnimatedRef<View>();
116
158
  const rawHeight = useKeyboardHeight();
117
159
  const { height: screenHeight } = useWindowDimensions();
118
160
 
119
- /** Window-space bottom edge of the element with no translation applied. */
120
- const restingBottom = useSharedValue(0);
161
+ /** The translation currently applied, in pixels. Zero or negative. */
162
+ const translation = useSharedValue(0);
121
163
 
122
- // `active` is a plain prop and the reaction below is a worklet, so it is
123
- // mirrored rather than closed over — written in an effect, because touching
124
- // a shared value during render is a Reanimated strict-mode violation.
164
+ // `active` is a plain prop and the worklets below cannot read props, so it is
165
+ // mirrored — written in an effect, because touching a shared value during
166
+ // render is a Reanimated strict-mode violation.
125
167
  const isActive = useSharedValue(active && enabled);
126
168
  useEffect(() => {
127
169
  isActive.value = active && enabled;
128
170
  }, [active, enabled, isActive]);
129
171
 
130
172
  /*
131
- * The resting position is taken the moment this element becomes the one that
132
- * should move, on the UI thread, rather than once at layout time.
173
+ * The whole of `lift` is this callback, and it runs only while the element is
174
+ * the active one *and* the keyboard is up — see the reaction below.
133
175
  *
134
- * Measuring at layout is wrong in two ways that both show up as "avoidance
135
- * does nothing". A field inside an overlay is laid out before the overlay
136
- * knows where it goes — it is parked off-screen at that point, so the stored
137
- * position is a large negative number that is nonetheless non-zero, and the
138
- * arithmetic below happily concludes there is no overlap. And a field that
139
- * has scrolled since it was laid out is measured where it used to be.
176
+ * The element is re-measured every frame rather than once, because every
177
+ * interesting thing that moves it happens after the keyboard opens: the page
178
+ * scrolls, a sheet settles, content above it grows. A position captured at
179
+ * the moment of focus is right for exactly one frame, and the element spends
180
+ * the rest of the time holding an offset that belongs to where it used to be.
140
181
  *
141
- * Keying off "active and the keyboard is up" rather than off the keyboard
142
- * alone matters for the second field you tap: moving straight from one field
143
- * to another never closes the keyboard, so there is no opening to react to,
144
- * and a hook watching only the keyboard would never measure it.
145
- *
146
- * At the instant it becomes active the element has no translation applied,
147
- * so what is measured then is the honest resting position.
182
+ * What is measured already includes the translation applied on the previous
183
+ * frame, so that is subtracted back out to recover the honest resting edge.
184
+ * Without it the callback would chase its own output down the screen.
185
+ */
186
+ const track = useFrameCallback(() => {
187
+ 'worklet';
188
+ const keyboardHeight = Math.abs(rawHeight.value);
189
+ if (keyboardHeight === 0) return;
190
+
191
+ const frame = measure(ref);
192
+ if (!frame || frame.height <= 0) return;
193
+
194
+ const restingBottom = frame.pageY + frame.height - translation.value;
195
+ const keyboardTop = screenHeight - keyboardHeight;
196
+ const overlap = restingBottom + offset - keyboardTop;
197
+ const next = overlap > 0 ? -overlap : 0;
198
+
199
+ if (Math.abs(next - translation.value) > EPSILON) translation.value = next;
200
+ }, false);
201
+
202
+ /*
203
+ * Tracking is switched on the transition rather than left running, so a
204
+ * screen full of fields costs nothing until one of them is being typed into.
205
+ * `setActive` lives on the JS side, hence the hop.
148
206
  */
207
+ const [tracking, setTracking] = useState(false);
149
208
  useAnimatedReaction(
150
209
  () => isActive.value && Math.abs(rawHeight.value) > 0,
151
- (shouldLift, wasLifting) => {
152
- if (shouldLift === wasLifting) return;
153
-
154
- if (!shouldLift) {
155
- // Forget it, so the next time round measures wherever the element has
156
- // got to in the meantime.
157
- restingBottom.value = 0;
158
- return;
159
- }
160
-
161
- const frame = measure(ref);
162
- if (frame && frame.height > 0 && frame.pageY >= 0) {
163
- restingBottom.value = frame.pageY + frame.height;
210
+ (shouldTrack, wasTracking) => {
211
+ if (shouldTrack === wasTracking) return;
212
+ runOnJS(setTracking)(shouldTrack);
213
+
214
+ // Moving straight from one field to another never closes the keyboard,
215
+ // so the field being left has nothing to follow back down — it is sent
216
+ // home explicitly, or it stays hanging where the keyboard left it.
217
+ if (!shouldTrack && translation.value !== 0) {
218
+ translation.value = withTiming(0, { duration: SETTLE_DURATION });
164
219
  }
165
220
  }
166
221
  );
167
222
 
168
- const seed = useCallback(() => {
169
- // Skipped only when a position is already known and the keyboard is up —
170
- // the element is lifted then, and this would store the lifted position as
171
- // the resting one. With no position yet there is no translation to undo,
172
- // so measuring is safe whatever the keyboard is doing.
173
- if (restingBottom.value !== 0 && Math.abs(rawHeight.value) > 0) return;
174
-
175
- ref.current?.measureInWindow((_x, y, _width, height) => {
176
- if (height > 0 && y >= 0) restingBottom.value = y + height;
177
- });
178
- }, [ref, rawHeight, restingBottom]);
223
+ const { setActive } = track;
224
+ useEffect(() => {
225
+ setActive(tracking && mode === 'lift');
226
+ return () => setActive(false);
227
+ }, [tracking, mode, setActive]);
179
228
 
180
229
  const onLayout = useCallback(
181
230
  (_event: LayoutChangeEvent) => {
182
- // Covers the case the reaction above cannot: a keyboard that is already
183
- // up when this element mounts, where there is no transition to react to.
184
- // measureInWindow is only meaningful once the view is attached and
185
- // positioned, which is a frame later than onLayout on both platforms.
186
- requestAnimationFrame(seed);
231
+ // A layout pass while the element is at rest means its slot moved for
232
+ // some reason other than the keyboard. Anything left over from the last
233
+ // lift belongs to the old slot.
234
+ if (!tracking && translation.value !== 0) translation.value = 0;
187
235
  },
188
- [seed]
236
+ [tracking, translation]
189
237
  );
190
238
 
191
239
  const animatedStyle = useAnimatedStyle(() => {
192
- // Both sources are normalised to a positive height here.
193
- const keyboardHeight = Math.abs(rawHeight.value);
194
-
195
- if (!isActive.value || keyboardHeight === 0 || restingBottom.value === 0) {
196
- return { transform: [{ translateY: 0 }] };
240
+ if (mode === 'dock') {
241
+ if (!isActive.value) return { transform: [{ translateY: 0 }] };
242
+ // Both sources are normalised to a positive height here.
243
+ const keyboardHeight = Math.abs(rawHeight.value);
244
+ const travel = Math.max(keyboardHeight - bottomInset, 0);
245
+ return { transform: [{ translateY: -travel }] };
197
246
  }
198
247
 
199
- const keyboardTop = screenHeight - keyboardHeight;
200
- const overlap = restingBottom.value + offset - keyboardTop;
201
-
202
- return { transform: [{ translateY: overlap > 0 ? -overlap : 0 }] };
248
+ return { transform: [{ translateY: translation.value }] };
203
249
  });
204
250
 
205
251
  return { ref, onLayout, animatedStyle };
@@ -0,0 +1,106 @@
1
+ /**
2
+ * useRevealProgress — how far an element has travelled through the viewport.
3
+ *
4
+ * The number every scroll-driven effect is really made of: `0` before the
5
+ * element has arrived, `1` once it has passed, and a smooth scrub between the
6
+ * two. What an effect *does* with it — recolour, fade, zoom, step through
7
+ * frames — is the only part that differs.
8
+ *
9
+ * The window is set the way a scroll-trigger's is: `start` and `end` are
10
+ * positions down the viewport, as fractions of its height. Progress is `0`
11
+ * while the element's **top** is still below `start`, and reaches `1` when its
12
+ * **bottom** passes `end` — so a tall block scrubs across its own height rather
13
+ * than snapping the moment its first line appears.
14
+ *
15
+ * ```tsx
16
+ * const { ref, progress } = useRevealProgress({ start: 0.9, end: 0.5 });
17
+ *
18
+ * <Animated.View ref={ref} style={useAnimatedStyle(() => ({
19
+ * opacity: progress.value,
20
+ * }))} />
21
+ * ```
22
+ *
23
+ * The element is measured on the UI thread whenever the scroll position
24
+ * changes — not every frame, and not once at layout. Once is wrong the moment
25
+ * anything above it resizes; every frame is a measurement pass for a value that
26
+ * only changes when the scroller moves.
27
+ */
28
+ import { useWindowDimensions, type View } from 'react-native';
29
+ import {
30
+ measure,
31
+ useAnimatedRef,
32
+ useDerivedValue,
33
+ type AnimatedRef,
34
+ type SharedValue,
35
+ } from 'react-native-reanimated';
36
+ import { useScrollProgress } from '../primitives/scroll-progress';
37
+
38
+ export interface UseRevealProgressOptions {
39
+ /**
40
+ * Where down the viewport the element's top sits when progress is `0`, as a
41
+ * fraction of the viewport height. `0.9` starts it just before it appears.
42
+ */
43
+ start?: number;
44
+ /**
45
+ * Where down the viewport the element's bottom sits when progress reaches
46
+ * `1`. Smaller values mean a longer scrub.
47
+ */
48
+ end?: number;
49
+ /**
50
+ * Drive the effect from a value of your own instead of from the scroll
51
+ * position. Anything passed here is used as-is and nothing is measured.
52
+ */
53
+ progress?: SharedValue<number>;
54
+ /** Set false to hold progress at `1`, as though the element had passed. */
55
+ enabled?: boolean;
56
+ }
57
+
58
+ export interface UseRevealProgressResult {
59
+ /** Attach to the element whose travel drives the effect. */
60
+ ref: AnimatedRef<View>;
61
+ /** `0` before, `1` after, scrubbed between. */
62
+ progress: SharedValue<number>;
63
+ }
64
+
65
+ export function useRevealProgress({
66
+ start = 0.9,
67
+ end = 0.5,
68
+ progress: external,
69
+ enabled = true,
70
+ }: UseRevealProgressOptions = {}): UseRevealProgressResult {
71
+ const ref = useAnimatedRef<View>();
72
+ const scroll = useScrollProgress();
73
+ const { height: windowHeight } = useWindowDimensions();
74
+
75
+ const offset = scroll?.offset;
76
+ const viewportValue = scroll?.viewport;
77
+ const topValue = scroll?.top;
78
+
79
+ const derived = useDerivedValue(() => {
80
+ if (!enabled) return 1;
81
+
82
+ // Read so the scroll position is a real dependency of this value. The
83
+ // element's own position is not observable, and the scroller moving
84
+ // underneath it is the only thing that changes it while it is on screen.
85
+ const scrolled = offset ? offset.value : 0;
86
+ const viewport = viewportValue?.value || windowHeight;
87
+ const viewportTop = topValue?.value ?? 0;
88
+ if (viewport <= 0 || Number.isNaN(scrolled)) return 0;
89
+
90
+ const frame = measure(ref);
91
+ if (!frame || frame.height <= 0) return 0;
92
+
93
+ const top = frame.pageY - viewportTop;
94
+ const from = start * viewport;
95
+ const to = end * viewport - frame.height;
96
+ const span = from - to;
97
+ if (span <= 0) return top <= to ? 1 : 0;
98
+
99
+ const value = (from - top) / span;
100
+ return value < 0 ? 0 : value > 1 ? 1 : value;
101
+ });
102
+
103
+ // An external value is passed straight through, so a caller driving the
104
+ // effect by hand never pays for the measurement.
105
+ return { ref, progress: external ?? derived };
106
+ }
package/src/index.ts CHANGED
@@ -25,6 +25,12 @@ export {
25
25
  AnimatedPressable,
26
26
  type AnimatedPressableProps,
27
27
  } from './primitives/animated-pressable';
28
+ export {
29
+ ScrollProgress,
30
+ useScrollProgress,
31
+ type ScrollProgressProps,
32
+ type ScrollProgressValue,
33
+ } from './primitives/scroll-progress';
28
34
 
29
35
  // Native UI bridge
30
36
  export { hasNativeUI } from './native';
@@ -95,6 +101,22 @@ export {
95
101
  type FrameSectionProps,
96
102
  type FrameVariant,
97
103
  } from './components/frame';
104
+ export {
105
+ HeatmapChart,
106
+ useHeatmapChart,
107
+ buildHeatmapCalendar,
108
+ type HeatmapChartProps,
109
+ type HeatmapCellsProps,
110
+ type HeatmapSeparatorProps,
111
+ type HeatmapXAxisProps,
112
+ type HeatmapYAxisProps,
113
+ type HeatmapTooltipProps,
114
+ type HeatmapLegendProps,
115
+ type HeatmapColumn,
116
+ type HeatmapBin,
117
+ type HeatmapCell,
118
+ type HeatmapLayout,
119
+ } from './components/heatmap-chart';
98
120
  export {
99
121
  Select,
100
122
  type SelectProps,
@@ -222,6 +244,22 @@ export {
222
244
  } from './components/scroll-fade';
223
245
  export { Separator, type SeparatorProps } from './components/separator';
224
246
  export { Shimmer, type ShimmerProps } from './components/shimmer';
247
+ export {
248
+ ScrollText,
249
+ type ScrollTextProps,
250
+ type ScrollTextEffect,
251
+ type ScrollTextSplit,
252
+ } from './components/scroll-text';
253
+ export {
254
+ ScrollCanvas,
255
+ type ScrollCanvasProps,
256
+ type ScrollCanvasEffect,
257
+ } from './components/scroll-canvas';
258
+ export {
259
+ ThinkingOrb,
260
+ type ThinkingOrbProps,
261
+ type ThinkingOrbState,
262
+ } from './components/thinking-orb';
225
263
  export { Skeleton, type SkeletonProps } from './components/skeleton';
226
264
  export { Slider, type SliderProps } from './components/slider';
227
265
  export { Spinner, type SpinnerProps } from './components/spinner';
@@ -28,13 +28,15 @@ interface NativeUIModule {
28
28
  /**
29
29
  * Whether the host resizes itself to the platform content.
30
30
  *
31
- * Every control here gives the host an explicit height instead, and takes
32
- * its width from ordinary layout. The measurement arrives a frame late and
33
- * again whenever the platform's content changes, so a host left to work
34
- * its own size out renders at nothing and then jumps — on first paint, and
35
- * again on first press. The per-axis form does not help; the answer is not
36
- * to ask. Only the natively-presented sheet still uses it, where the host
37
- * is a zero-size anchor rather than the control itself.
31
+ * This is on for every control here, and it is the whole answer to the
32
+ * jump. Sizing the *host* and leaving the control unsized inside it hands
33
+ * the platform a box it never agreed to: it lays out against its own
34
+ * intrinsic size, and settles into the box on the first thing that forces
35
+ * a second pass — which for a button is the first press.
36
+ *
37
+ * The per-axis form is for a control with no intrinsic width, like a
38
+ * slider or a picker: the width comes from ordinary layout and only the
39
+ * height is reported back.
38
40
  */
39
41
  matchContents?: boolean | { vertical?: boolean; horizontal?: boolean };
40
42
  style?: unknown;
@@ -50,6 +52,12 @@ interface NativeUIModule {
50
52
  matchContents?: boolean;
51
53
  style?: unknown;
52
54
  }>;
55
+ /**
56
+ * `style` here is not a React Native style — it is the small portable subset
57
+ * (`width`, `height`, padding, `backgroundColor`, `borderRadius`, `opacity`)
58
+ * that the toolkit compiles into real SwiftUI and Compose modifiers. It is
59
+ * how a control is given a definite size without the host having to guess.
60
+ */
53
61
  Button: ComponentType<Record<string, unknown>>;
54
62
  Switch: ComponentType<Record<string, unknown>>;
55
63
  Slider: ComponentType<Record<string, unknown>>;
@@ -1,14 +1,32 @@
1
1
  /**
2
- * KeyboardAvoider — a view that lifts itself just clear of the keyboard.
2
+ * KeyboardAvoider — a view that keeps itself clear of the keyboard.
3
3
  *
4
4
  * A thin wrapper over `useKeyboardAvoidance` for the common case of "keep this
5
5
  * box visible", so a form does not have to wire the ref, layout handler and
6
6
  * animated style by hand.
7
+ *
8
+ * ```tsx
9
+ * // In the flow of a scrolling page: lifts by the overlap, follows the scroll.
10
+ * <KeyboardAvoider active={focused}>…</KeyboardAvoider>
11
+ *
12
+ * // Pinned to the bottom edge: rides the keyboard up and back down.
13
+ * <KeyboardAvoider
14
+ * mode="dock"
15
+ * bottomInset={insets.bottom}
16
+ * className="absolute left-0 right-0"
17
+ * style={{ bottom: insets.bottom + 16 }}
18
+ * >
19
+ * …
20
+ * </KeyboardAvoider>
21
+ * ```
7
22
  */
8
23
  import type { ReactNode } from 'react';
9
24
  import type { ViewProps } from 'react-native';
10
25
  import Animated from 'react-native-reanimated';
11
- import { useKeyboardAvoidance } from '../hooks/use-keyboard-avoidance';
26
+ import {
27
+ useKeyboardAvoidance,
28
+ type KeyboardAvoidanceMode,
29
+ } from '../hooks/use-keyboard-avoidance';
12
30
 
13
31
  export interface KeyboardAvoiderProps extends ViewProps {
14
32
  className?: string;
@@ -21,8 +39,19 @@ export interface KeyboardAvoiderProps extends ViewProps {
21
39
  * screen stay put.
22
40
  */
23
41
  active?: boolean;
24
- /** Gap to keep between the content's bottom edge and the keyboard. */
42
+ /**
43
+ * `lift` moves in-flow content up by its overlap with the keyboard and keeps
44
+ * tracking it as the page scrolls. `dock` travels with the keyboard, for a
45
+ * bar already pinned near the bottom edge.
46
+ */
47
+ mode?: KeyboardAvoidanceMode;
48
+ /** Gap to keep between the content's bottom edge and the keyboard. `lift` only. */
25
49
  offset?: number;
50
+ /**
51
+ * How far above the bottom edge the bar already sits — usually the safe area
52
+ * inset it is positioned by. `dock` only.
53
+ */
54
+ bottomInset?: number;
26
55
  children?: ReactNode;
27
56
  }
28
57
 
@@ -30,13 +59,21 @@ export function KeyboardAvoider({
30
59
  className,
31
60
  enabled = true,
32
61
  active = true,
62
+ mode = 'lift',
33
63
  offset = 16,
64
+ bottomInset = 0,
34
65
  children,
35
66
  style,
36
67
  onLayout,
37
68
  ...props
38
69
  }: KeyboardAvoiderProps) {
39
- const avoidance = useKeyboardAvoidance({ enabled, active, offset });
70
+ const avoidance = useKeyboardAvoidance({
71
+ enabled,
72
+ active,
73
+ mode,
74
+ offset,
75
+ bottomInset,
76
+ });
40
77
 
41
78
  return (
42
79
  <Animated.View