@oxyhq/bloom 0.79.1 → 0.80.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 (52) hide show
  1. package/lib/commonjs/scroll/index.web.js +52 -18
  2. package/lib/commonjs/scroll/index.web.js.map +1 -1
  3. package/lib/commonjs/scroll/scrollable.web.js +9 -3
  4. package/lib/commonjs/scroll/scrollable.web.js.map +1 -1
  5. package/lib/commonjs/tabs/Tabs.js +263 -63
  6. package/lib/commonjs/tabs/Tabs.js.map +1 -1
  7. package/lib/commonjs/tabs/expo-router/RouterTabs.js +215 -0
  8. package/lib/commonjs/tabs/expo-router/RouterTabs.js.map +1 -0
  9. package/lib/commonjs/tabs/expo-router/index.js +13 -0
  10. package/lib/commonjs/tabs/expo-router/index.js.map +1 -0
  11. package/lib/module/scroll/index.web.js +52 -18
  12. package/lib/module/scroll/index.web.js.map +1 -1
  13. package/lib/module/scroll/scrollable.web.js +9 -3
  14. package/lib/module/scroll/scrollable.web.js.map +1 -1
  15. package/lib/module/tabs/Tabs.js +271 -65
  16. package/lib/module/tabs/Tabs.js.map +1 -1
  17. package/lib/module/tabs/expo-router/RouterTabs.js +210 -0
  18. package/lib/module/tabs/expo-router/RouterTabs.js.map +1 -0
  19. package/lib/module/tabs/expo-router/index.js +10 -0
  20. package/lib/module/tabs/expo-router/index.js.map +1 -0
  21. package/lib/typescript/commonjs/scroll/index.web.d.ts.map +1 -1
  22. package/lib/typescript/commonjs/scroll/scrollable.web.d.ts +18 -5
  23. package/lib/typescript/commonjs/scroll/scrollable.web.d.ts.map +1 -1
  24. package/lib/typescript/commonjs/tabs/Tabs.d.ts +41 -1
  25. package/lib/typescript/commonjs/tabs/Tabs.d.ts.map +1 -1
  26. package/lib/typescript/commonjs/tabs/expo-router/RouterTabs.d.ts +80 -0
  27. package/lib/typescript/commonjs/tabs/expo-router/RouterTabs.d.ts.map +1 -0
  28. package/lib/typescript/commonjs/tabs/expo-router/index.d.ts +9 -0
  29. package/lib/typescript/commonjs/tabs/expo-router/index.d.ts.map +1 -0
  30. package/lib/typescript/commonjs/tabs/types.d.ts +48 -4
  31. package/lib/typescript/commonjs/tabs/types.d.ts.map +1 -1
  32. package/lib/typescript/module/scroll/index.web.d.ts.map +1 -1
  33. package/lib/typescript/module/scroll/scrollable.web.d.ts +18 -5
  34. package/lib/typescript/module/scroll/scrollable.web.d.ts.map +1 -1
  35. package/lib/typescript/module/tabs/Tabs.d.ts +41 -1
  36. package/lib/typescript/module/tabs/Tabs.d.ts.map +1 -1
  37. package/lib/typescript/module/tabs/expo-router/RouterTabs.d.ts +80 -0
  38. package/lib/typescript/module/tabs/expo-router/RouterTabs.d.ts.map +1 -0
  39. package/lib/typescript/module/tabs/expo-router/index.d.ts +9 -0
  40. package/lib/typescript/module/tabs/expo-router/index.d.ts.map +1 -0
  41. package/lib/typescript/module/tabs/types.d.ts +48 -4
  42. package/lib/typescript/module/tabs/types.d.ts.map +1 -1
  43. package/package.json +15 -1
  44. package/src/__tests__/Tabs.test.tsx +187 -0
  45. package/src/__tests__/optional-peer-imports.test.ts +5 -0
  46. package/src/__tests__/scroll-web.test.tsx +255 -2
  47. package/src/scroll/index.web.tsx +55 -19
  48. package/src/scroll/scrollable.web.ts +30 -8
  49. package/src/tabs/Tabs.tsx +352 -77
  50. package/src/tabs/expo-router/RouterTabs.tsx +268 -0
  51. package/src/tabs/expo-router/index.ts +8 -0
  52. package/src/tabs/types.ts +48 -4
@@ -0,0 +1,210 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * expo-router bindings for the in-screen tab strip.
5
+ *
6
+ * This subpath (`@oxyhq/bloom/tabs/expo-router`) is the ONLY place in the tabs
7
+ * feature allowed to import `expo-router`, which is why it is a separate entry:
8
+ * the core `@oxyhq/bloom/tabs` stays usable in an app with a different router,
9
+ * or none, and the optional peer is never pulled into a bundle that does not
10
+ * ask for it. Same split as `tab-bar` / `tab-bar/expo-router` and
11
+ * `scroll` / `scroll/expo-router`.
12
+ *
13
+ * Everything routing-shaped lives here on purpose: which tab the route means,
14
+ * what a committed swipe navigates to, and which links to warm. The strip
15
+ * itself stays presentational and knows only about measured geometry.
16
+ */
17
+ import { useCallback, useEffect, useMemo, useRef } from 'react';
18
+ import { Gesture, GestureDetector } from 'react-native-gesture-handler';
19
+ import { runOnJS } from 'react-native-reanimated';
20
+ import { router, usePathname } from 'expo-router';
21
+ import { Tabs, TabsTrigger } from "../Tabs.js";
22
+ import { jsx as _jsx } from "react/jsx-runtime";
23
+ /**
24
+ * Resolve which item the current route belongs to.
25
+ *
26
+ * LONGEST matching prefix, on a segment boundary — not an exact match. All
27
+ * three properties matter:
28
+ *
29
+ * - A prefix is needed because a tab may own a subtree (`/feeds/1/topics` and
30
+ * anything under it), and an exact match would drop the underline the moment
31
+ * the user goes one level deeper.
32
+ * - LONGEST is needed because the default tab's href is a prefix of every
33
+ * sibling's (`/@ana` vs `/@ana/replies`), so a first-match rule would light
34
+ * up the default tab on every tab.
35
+ * - The segment boundary is what stops `/feeds/12` from matching `/feeds/1`.
36
+ *
37
+ * Returns `undefined` when nothing matches, which is a real state — a strip
38
+ * rendered by a LAYOUT keeps rendering while a child route outside the tab set
39
+ * is showing — and leaves the underline hidden rather than parked on a lie.
40
+ */
41
+ function matchActiveValue(items, pathname) {
42
+ let best;
43
+ for (const item of items) {
44
+ // `Href` is a string or an object; only a string form can be compared to a
45
+ // pathname, and an object form describes params we cannot resolve here.
46
+ if (typeof item.href !== 'string') continue;
47
+ // Compare paths only: a query string describes what the tab shows, not
48
+ // which tab it is, and a trailing slash is the same route either way.
49
+ const [path = ''] = item.href.split('?');
50
+ const href = path.replace(/\/$/, '');
51
+ const isMatch = pathname === href || pathname.startsWith(`${href}/`);
52
+ if (!isMatch) continue;
53
+ if (!best || href.length > best.length) {
54
+ best = {
55
+ value: item.value,
56
+ length: href.length
57
+ };
58
+ }
59
+ }
60
+ return best?.value;
61
+ }
62
+
63
+ /**
64
+ * A tab strip whose selection IS the route.
65
+ *
66
+ * Render it in the LAYOUT that owns the tabs, above the `<Slot/>` that renders
67
+ * them — not inside the tab screens themselves. That placement is the whole
68
+ * point, and it is load-bearing twice over: a strip inside the screens is
69
+ * unmounted and rebuilt by every navigation, so its underline has nothing to
70
+ * animate from and the chrome blanks while the incoming route's chunk loads;
71
+ * and a GESTURE inside the screens would be destroyed by the very navigation it
72
+ * commits, mid-release.
73
+ *
74
+ * ```tsx
75
+ * // app/(app)/[username]/_layout.tsx
76
+ * <RouterTabs
77
+ * items={[
78
+ * { value: 'posts', label: 'Posts', href: `/${username}` },
79
+ * { value: 'replies', label: 'Replies', href: `/${username}/replies` },
80
+ * ]}
81
+ * />
82
+ * <Slot />
83
+ * ```
84
+ */
85
+ export function RouterTabs({
86
+ items,
87
+ onReselect,
88
+ swipeEnabled = true,
89
+ ...tabsProps
90
+ }) {
91
+ const pathname = usePathname();
92
+ const activeValue = useMemo(() => matchActiveValue(items, pathname), [items, pathname]);
93
+ const dragRef = useRef(null);
94
+ // What releasing right now would commit. A ref rather than state: it is
95
+ // written on every frame of the drag and read once, and re-rendering the
96
+ // strip mid-gesture would fight the animation the gesture is driving.
97
+ const pendingRef = useRef(null);
98
+ const navigateTo = useCallback(value => {
99
+ const target = items.find(item => item.value === value);
100
+ if (!target) return;
101
+ // PUSH, not replace: a history entry per tab is what makes Back return to
102
+ // the tab the reader came from instead of leaving the screen entirely.
103
+ router.push(target.href);
104
+ }, [items]);
105
+ const handlePress = useCallback(item => {
106
+ if (item.value === activeValue) {
107
+ onReselect?.(item.value);
108
+ return;
109
+ }
110
+ navigateTo(item.value);
111
+ }, [activeValue, onReselect, navigateTo]);
112
+ const onDrag = useCallback(translationX => {
113
+ pendingRef.current = dragRef.current?.drag(translationX) ?? null;
114
+ }, []);
115
+
116
+ // `onEnd` does not fire for every terminated gesture, and `onFinalize` fires
117
+ // for all of them — including after `onEnd`. Guarding on the ref being
118
+ // non-null is what makes the second call a no-op instead of a double commit.
119
+ const releasedRef = useRef(true);
120
+ const onRelease = useCallback(() => {
121
+ if (releasedRef.current) return;
122
+ releasedRef.current = true;
123
+ const committed = pendingRef.current;
124
+ pendingRef.current = null;
125
+ // The underline commits first and instantly: it lives in the layout, it
126
+ // survives the swap, and it is the honest signal the gesture was accepted.
127
+ // The content area arrives when its chunk does.
128
+ dragRef.current?.release(committed);
129
+ if (committed !== null) navigateTo(committed);
130
+ }, [navigateTo]);
131
+ const onBegin = useCallback(() => {
132
+ releasedRef.current = false;
133
+ }, []);
134
+ const panGesture = useMemo(() => Gesture.Pan()
135
+ // Horizontal intent only. This is a hint to the gesture system and is
136
+ // NOT what protects vertical scrolling on web — see `touchAction` below.
137
+ .activeOffsetX([-12, 12]).failOffsetY([-12, 12]).onBegin(() => {
138
+ runOnJS(onBegin)();
139
+ }).onUpdate(event => {
140
+ runOnJS(onDrag)(event.translationX);
141
+ }).onFinalize(() => {
142
+ runOnJS(onRelease)();
143
+ }), [onBegin, onDrag, onRelease]);
144
+
145
+ // Warm every OTHER tab's async chunk. That fetch is the ~300ms the content
146
+ // area would otherwise spend empty after a commit, and a swipe offers no
147
+ // hover to warm on, so it is done up front rather than on interaction.
148
+ //
149
+ // `router.prefetch` — the public imperative API (declared in expo-router's
150
+ // `global-state/router.d.ts`) — rather than wrapping each trigger in
151
+ // `<Link prefetch asChild>`. Both reasons are about the WRAPPER, not about
152
+ // prefetching: `asChild` is Radix's `Slot`, which COMPOSES event handlers, so
153
+ // the trigger's own `onPress` and the link's navigation would both run — and
154
+ // the link's push knows nothing about `onReselect`, so re-tapping the active
155
+ // tab would push a duplicate entry and make Back appear to do nothing. It
156
+ // also injects an `href` a `Pressable` ignores. The prefetching itself is
157
+ // identical: `<Link prefetch>` renders `<Prefetch href/>` beside its element
158
+ // and does nothing else.
159
+ //
160
+ // Known trade: a real `<Link>` would render a web anchor, which is what gives
161
+ // middle-click and open-in-new-tab. A tab strip arguably wants that; getting
162
+ // both needs the trigger to render an anchor itself, which is a change to the
163
+ // core rather than to this file.
164
+ useEffect(() => {
165
+ for (const item of items) {
166
+ if (item.value === activeValue || item.disabled) continue;
167
+ router.prefetch(item.href);
168
+ }
169
+ }, [items, activeValue]);
170
+ const strip =
171
+ /*#__PURE__*/
172
+ // `hasSelection` is what stops the underline asserting a tab while a
173
+ // NON-TAB sibling route is showing — the layout that renders this strip
174
+ // renders those too (a profile's `/followers` beside its tab routes).
175
+ _jsx(Tabs, {
176
+ ...tabsProps,
177
+ ref: dragRef,
178
+ hasSelection: activeValue !== undefined,
179
+ children: items.map(item => /*#__PURE__*/_jsx(TabsTrigger, {
180
+ value: item.value,
181
+ label: item.label,
182
+ icon: item.icon,
183
+ count: item.count,
184
+ disabled: item.disabled
185
+ // Focus, never a press, is what drives the underline — so a deep
186
+ // link, a browser Back and a back gesture animate exactly like a tap.
187
+ ,
188
+ isFocused: item.value === activeValue,
189
+ onPress: () => handlePress(item)
190
+ }, item.value))
191
+ });
192
+ if (!swipeEnabled) return strip;
193
+ return (
194
+ /*#__PURE__*/
195
+ // `touchAction="pan-y"` is MANDATORY and is the landmine of this file.
196
+ // RNGH sets the detector's view to `touch-action: none` on web
197
+ // (`GestureHandlerWebDelegate.js`), which disables browser-native VERTICAL
198
+ // scrolling on that element. `failOffsetY` above does not protect against
199
+ // it: that is a JS decision taken after `touch-action` has already told the
200
+ // browser not to scroll. Removing this prop is silent on native and breaks
201
+ // scrolling on mobile web.
202
+ _jsx(GestureDetector, {
203
+ gesture: panGesture,
204
+ touchAction: "pan-y",
205
+ children: strip
206
+ })
207
+ );
208
+ }
209
+ RouterTabs.displayName = 'RouterTabs';
210
+ //# sourceMappingURL=RouterTabs.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["useCallback","useEffect","useMemo","useRef","Gesture","GestureDetector","runOnJS","router","usePathname","Tabs","TabsTrigger","jsx","_jsx","matchActiveValue","items","pathname","best","item","href","path","split","replace","isMatch","startsWith","length","value","RouterTabs","onReselect","swipeEnabled","tabsProps","activeValue","dragRef","pendingRef","navigateTo","target","find","push","handlePress","onDrag","translationX","current","drag","releasedRef","onRelease","committed","release","onBegin","panGesture","Pan","activeOffsetX","failOffsetY","onUpdate","event","onFinalize","disabled","prefetch","strip","ref","hasSelection","undefined","children","map","label","icon","count","isFocused","onPress","gesture","touchAction","displayName"],"sourceRoot":"../../../../src","sources":["tabs/expo-router/RouterTabs.tsx"],"mappings":"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","ignoreList":[]}
@@ -0,0 +1,10 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Entry for `@oxyhq/bloom/tabs/expo-router`. The component lives in a sibling
5
+ * module because this file is the published entry path and must stay stable;
6
+ * the split also keeps the router binding in one place, which is what the
7
+ * optional-peer allowlist asserts about.
8
+ */
9
+ export { RouterTabs } from "./RouterTabs.js";
10
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["RouterTabs"],"sourceRoot":"../../../../src","sources":["tabs/expo-router/index.ts"],"mappings":";;AAAA;AACA;AACA;AACA;AACA;AACA;AACA,SAASA,UAAU,QAAQ,iBAAc","ignoreList":[]}
@@ -1 +1 @@
1
- {"version":3,"file":"index.web.d.ts","sourceRoot":"","sources":["../../../../src/scroll/index.web.tsx"],"names":[],"mappings":"AA6CA,OAAO,KAAK,EACV,wBAAwB,EACxB,uBAAuB,EACvB,2BAA2B,EAC5B,MAAM,SAAS,CAAC;AAEjB,OAAO,EAAE,yBAAyB,EAAE,MAAM,WAAW,CAAC;AAEtD,YAAY,EACV,iBAAiB,EACjB,gBAAgB,EAChB,wBAAwB,EACxB,8BAA8B,EAC9B,uBAAuB,EACvB,mBAAmB,EACnB,2BAA2B,GAC5B,MAAM,SAAS,CAAC;AA0DjB;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,uBAAuB,EAC/B,OAAO,CAAC,EAAE,2BAA2B,GACpC,wBAAwB,CA4J1B"}
1
+ {"version":3,"file":"index.web.d.ts","sourceRoot":"","sources":["../../../../src/scroll/index.web.tsx"],"names":[],"mappings":"AA6CA,OAAO,KAAK,EACV,wBAAwB,EACxB,uBAAuB,EACvB,2BAA2B,EAC5B,MAAM,SAAS,CAAC;AAEjB,OAAO,EAAE,yBAAyB,EAAE,MAAM,WAAW,CAAC;AAEtD,YAAY,EACV,iBAAiB,EACjB,gBAAgB,EAChB,wBAAwB,EACxB,8BAA8B,EAC9B,uBAAuB,EACvB,mBAAmB,EACnB,2BAA2B,GAC5B,MAAM,SAAS,CAAC;AA0DjB;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,uBAAuB,EAC/B,OAAO,CAAC,EAAE,2BAA2B,GACpC,wBAAwB,CAgM1B"}
@@ -8,17 +8,30 @@ export interface ResolvedScroller {
8
8
  getOffset: () => number;
9
9
  setOffset: (offset: number) => void;
10
10
  /**
11
- * Whether the scroll container can currently hold a non-zero offset, i.e.
12
- * its content is taller than its viewport (`scrollHeight > clientHeight`).
11
+ * The largest offset the container can currently hold `scrollHeight -
12
+ * clientHeight`, clamped at 0. Answers two questions the save path needs:
13
+ * whether a 0 read is real (`maxOffset > 0`) and whether an offset is sitting
14
+ * exactly at the bottom, which is the signature of a browser clamp.
13
15
  *
14
16
  * React Navigation's web stack collapses a hidden background screen so its
15
17
  * content height drops to the viewport height; while collapsed the container
16
18
  * cannot be scrolled and its `scrollTop` is forced to 0. The hook uses this
17
19
  * to ignore a spurious 0 read coming from a collapsed container rather than
18
- * persisting it over a previously-saved good offset. The `'window'` scroller
19
- * is never collapsed by the navigator, so it always reports `true`.
20
+ * persisting it over a previously-saved good offset.
21
+ *
22
+ * The DOCUMENT collapses the same way, so the `'window'` sentinel answers the
23
+ * same question honestly rather than hardcoding `true`. It used to, on the
24
+ * premise that the navigator never collapses the document — false under a
25
+ * tabbed navigator, where every tab stays MOUNTED and non-focused ones are
26
+ * `display: none` (expo-router's `ui/TabSlot`, and react-native-screens' web
27
+ * `Screen` at `activityState === 0`). Measured in Chrome 150: switching from
28
+ * a tall tab to a short one drops `documentElement.scrollHeight` from 8040 to
29
+ * the 800px viewport, forces `scrollY` to 0 and dispatches two `scroll`
30
+ * events — which the outgoing tab's still-attached listener would otherwise
31
+ * persist as 0 over its real offset, because blur is emitted from an effect
32
+ * that has not run yet.
20
33
  */
21
- canScroll: () => boolean;
34
+ getMaxOffset: () => number;
22
35
  }
23
36
  /**
24
37
  * Build a {@link ResolvedScroller} for a target. The window sentinel reads and
@@ -1 +1 @@
1
- {"version":3,"file":"scrollable.web.d.ts","sourceRoot":"","sources":["../../../../src/scroll/scrollable.web.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAoB,uBAAuB,EAAE,MAAM,SAAS,CAAC;AAEzE;;;;GAIG;AACH,MAAM,WAAW,gBAAgB;IAC/B,SAAS,EAAE,MAAM,MAAM,CAAC;IACxB,SAAS,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IACpC;;;;;;;;;;OAUG;IACH,SAAS,EAAE,MAAM,OAAO,CAAC;CAC1B;AAuCD;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,uBAAuB,GAAG,gBAAgB,CAyBhF"}
1
+ {"version":3,"file":"scrollable.web.d.ts","sourceRoot":"","sources":["../../../../src/scroll/scrollable.web.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAoB,uBAAuB,EAAE,MAAM,SAAS,CAAC;AAEzE;;;;GAIG;AACH,MAAM,WAAW,gBAAgB;IAC/B,SAAS,EAAE,MAAM,MAAM,CAAC;IACxB,SAAS,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IACpC;;;;;;;;;;;;;;;;;;;;;;;OAuBG;IACH,YAAY,EAAE,MAAM,MAAM,CAAC;CAC5B;AAuCD;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,uBAAuB,GAAG,gBAAgB,CAkChF"}
@@ -1,6 +1,46 @@
1
1
  import React from 'react';
2
2
  import type { TabsProps, TabsTriggerProps, TabsContentProps } from './types';
3
- export declare const Tabs: React.NamedExoticComponent<TabsProps>;
3
+ /**
4
+ * The imperative surface a horizontal drag gesture drives the underline through.
5
+ *
6
+ * It lives here rather than in the gesture's own code because every quantity the
7
+ * drag needs — where the neighbouring trigger is, how wide it is, whether there
8
+ * IS a neighbour — is measured layout the strip already holds and nothing
9
+ * outside it can see. The gesture supplies one number, the finger's delta; the
10
+ * strip decides what that means.
11
+ *
12
+ * Obtained via a `ref` on `Tabs`. The gesture belongs in the LAYOUT beside the
13
+ * strip, never inside a tab screen — a gesture mounted in a screen is destroyed
14
+ * by the navigation it commits, mid-release.
15
+ */
16
+ export interface TabsDragController {
17
+ /**
18
+ * Feed the raw horizontal finger delta (`translationX`).
19
+ *
20
+ * The underline travels OPPOSITE the finger, because that is where the
21
+ * incoming tab is: dragging left reveals the NEXT tab, which sits to the
22
+ * right. Travel is clamped at the neighbour — the underline stops exactly
23
+ * where it would land, so it never promises more than the release delivers —
24
+ * and at the first/last tab it rubber-bands instead.
25
+ *
26
+ * Returns the value that releasing NOW would commit, or `null` for "no
27
+ * commit": either the drag has not passed the threshold, or there is no
28
+ * neighbour in that direction.
29
+ */
30
+ drag(translationX: number): string | null;
31
+ /**
32
+ * End the drag. Pass the value `drag` last returned to keep the underline
33
+ * where the finger left it while the caller navigates; pass `null` to spring
34
+ * it home.
35
+ *
36
+ * On a commit the offset is FOLDED into the underline's base position rather
37
+ * than zeroed, so the focus change that follows springs from where the finger
38
+ * actually let go. Zeroing first would snap backwards for a frame and then
39
+ * animate forwards from the wrong place.
40
+ */
41
+ release(committed: string | null): void;
42
+ }
43
+ export declare const Tabs: React.NamedExoticComponent<TabsProps & React.RefAttributes<TabsDragController>>;
4
44
  export declare const TabsTrigger: React.NamedExoticComponent<TabsTriggerProps>;
5
45
  export declare const TabsContent: React.NamedExoticComponent<TabsContentProps>;
6
46
  //# sourceMappingURL=Tabs.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"Tabs.d.ts","sourceRoot":"","sources":["../../../../src/tabs/Tabs.tsx"],"names":[],"mappings":"AAAA,OAAO,KAQN,MAAM,OAAO,CAAC;AAiBf,OAAO,KAAK,EAAE,SAAS,EAAE,gBAAgB,EAAE,gBAAgB,EAAe,MAAM,SAAS,CAAC;AA4Q1F,eAAO,MAAM,IAAI,uCAAyB,CAAC;AAG3C,eAAO,MAAM,WAAW,8CAAqB,CAAC;AAG9C,eAAO,MAAM,WAAW,8CAA0B,CAAC"}
1
+ {"version":3,"file":"Tabs.d.ts","sourceRoot":"","sources":["../../../../src/tabs/Tabs.tsx"],"names":[],"mappings":"AAAA,OAAO,KAUN,MAAM,OAAO,CAAC;AA4Bf,OAAO,KAAK,EAAE,SAAS,EAAE,gBAAgB,EAAE,gBAAgB,EAAe,MAAM,SAAS,CAAC;AAwD1F;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,kBAAkB;IACjC;;;;;;;;;;;;OAYG;IACH,IAAI,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;IAC1C;;;;;;;;;OASG;IACH,OAAO,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,CAAC;CACzC;AAmbD,eAAO,MAAM,IAAI,iFAAyB,CAAC;AAG3C,eAAO,MAAM,WAAW,8CAAqB,CAAC;AAG9C,eAAO,MAAM,WAAW,8CAA0B,CAAC"}
@@ -0,0 +1,80 @@
1
+ /**
2
+ * expo-router bindings for the in-screen tab strip.
3
+ *
4
+ * This subpath (`@oxyhq/bloom/tabs/expo-router`) is the ONLY place in the tabs
5
+ * feature allowed to import `expo-router`, which is why it is a separate entry:
6
+ * the core `@oxyhq/bloom/tabs` stays usable in an app with a different router,
7
+ * or none, and the optional peer is never pulled into a bundle that does not
8
+ * ask for it. Same split as `tab-bar` / `tab-bar/expo-router` and
9
+ * `scroll` / `scroll/expo-router`.
10
+ *
11
+ * Everything routing-shaped lives here on purpose: which tab the route means,
12
+ * what a committed swipe navigates to, and which links to warm. The strip
13
+ * itself stays presentational and knows only about measured geometry.
14
+ */
15
+ import { type ReactNode } from 'react';
16
+ import { type Href } from 'expo-router';
17
+ import type { TabsProps } from '../types';
18
+ export interface RouterTabItem {
19
+ /** Stable id for the tab. Used as a React key and a callback argument. */
20
+ value: string;
21
+ /** Tab label text. */
22
+ label: string;
23
+ /** The route this tab shows. Must be the tab's EXACT path. */
24
+ href: Href;
25
+ /** Icon rendered before the label. */
26
+ icon?: ReactNode;
27
+ /** Optional tally beside the label; `0` or omitted renders nothing. */
28
+ count?: number;
29
+ disabled?: boolean;
30
+ }
31
+ export type RouterTabsProps = Omit<TabsProps, 'value' | 'onValueChange' | 'hasSelection' | 'children'> & {
32
+ items: RouterTabItem[];
33
+ /**
34
+ * Called instead of navigating when the ALREADY-active tab is pressed.
35
+ *
36
+ * Without it a re-tap would push a second history entry for the route the
37
+ * user is already on, so Back would appear to do nothing. Re-tapping the
38
+ * active tab conventionally means "refresh" or "scroll to top", and that is
39
+ * the caller's to define.
40
+ */
41
+ onReselect?: (value: string) => void;
42
+ /**
43
+ * Enable the horizontal swipe between neighbouring tabs. Defaults to `true`.
44
+ *
45
+ * The gesture is INDICATOR-LED: the underline tracks the finger toward the
46
+ * neighbour and commits on release past a threshold, while the content stays
47
+ * where it is. That asymmetry is deliberate — the underline has somewhere
48
+ * real to go and can complete its journey, whereas under a `<Slot/>` there is
49
+ * no neighbouring screen mounted to bring in, so translating the content
50
+ * would only open a blank gutter on the trailing edge.
51
+ */
52
+ swipeEnabled?: boolean;
53
+ };
54
+ /**
55
+ * A tab strip whose selection IS the route.
56
+ *
57
+ * Render it in the LAYOUT that owns the tabs, above the `<Slot/>` that renders
58
+ * them — not inside the tab screens themselves. That placement is the whole
59
+ * point, and it is load-bearing twice over: a strip inside the screens is
60
+ * unmounted and rebuilt by every navigation, so its underline has nothing to
61
+ * animate from and the chrome blanks while the incoming route's chunk loads;
62
+ * and a GESTURE inside the screens would be destroyed by the very navigation it
63
+ * commits, mid-release.
64
+ *
65
+ * ```tsx
66
+ * // app/(app)/[username]/_layout.tsx
67
+ * <RouterTabs
68
+ * items={[
69
+ * { value: 'posts', label: 'Posts', href: `/${username}` },
70
+ * { value: 'replies', label: 'Replies', href: `/${username}/replies` },
71
+ * ]}
72
+ * />
73
+ * <Slot />
74
+ * ```
75
+ */
76
+ export declare function RouterTabs({ items, onReselect, swipeEnabled, ...tabsProps }: RouterTabsProps): import("react/jsx-runtime").JSX.Element;
77
+ export declare namespace RouterTabs {
78
+ var displayName: string;
79
+ }
80
+ //# sourceMappingURL=RouterTabs.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"RouterTabs.d.ts","sourceRoot":"","sources":["../../../../../src/tabs/expo-router/RouterTabs.tsx"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,EAA2C,KAAK,SAAS,EAAE,MAAM,OAAO,CAAC;AAGhF,OAAO,EAAuB,KAAK,IAAI,EAAE,MAAM,aAAa,CAAC;AAG7D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,UAAU,CAAC;AAE1C,MAAM,WAAW,aAAa;IAC5B,0EAA0E;IAC1E,KAAK,EAAE,MAAM,CAAC;IACd,sBAAsB;IACtB,KAAK,EAAE,MAAM,CAAC;IACd,8DAA8D;IAC9D,IAAI,EAAE,IAAI,CAAC;IACX,sCAAsC;IACtC,IAAI,CAAC,EAAE,SAAS,CAAC;IACjB,uEAAuE;IACvE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,OAAO,CAAC;CACpB;AAED,MAAM,MAAM,eAAe,GAAG,IAAI,CAChC,SAAS,EAGT,OAAO,GAAG,eAAe,GAAG,cAAc,GAAG,UAAU,CACxD,GAAG;IACF,KAAK,EAAE,aAAa,EAAE,CAAC;IACvB;;;;;;;OAOG;IACH,UAAU,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACrC;;;;;;;;;OASG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;CACxB,CAAC;AAuCF;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,UAAU,CAAC,EACzB,KAAK,EACL,UAAU,EACV,YAAmB,EACnB,GAAG,SAAS,EACb,EAAE,eAAe,2CAwIjB;yBA7Ie,UAAU"}
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Entry for `@oxyhq/bloom/tabs/expo-router`. The component lives in a sibling
3
+ * module because this file is the published entry path and must stay stable;
4
+ * the split also keeps the router binding in one place, which is what the
5
+ * optional-peer allowlist asserts about.
6
+ */
7
+ export { RouterTabs } from './RouterTabs';
8
+ export type { RouterTabItem, RouterTabsProps } from './RouterTabs';
9
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../../src/tabs/expo-router/index.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,OAAO,EAAE,UAAU,EAAE,MAAM,cAAc,CAAC;AAC1C,YAAY,EAAE,aAAa,EAAE,eAAe,EAAE,MAAM,cAAc,CAAC"}
@@ -1,10 +1,38 @@
1
1
  import type { StyleProp, ViewStyle, TextStyle } from 'react-native';
2
2
  export type TabsVariant = 'underline' | 'filled' | 'outlined';
3
3
  export interface TabsProps {
4
- /** The currently selected tab value. */
5
- value: string;
6
- /** Called when a tab is selected. */
7
- onValueChange: (value: string) => void;
4
+ /**
5
+ * The currently selected tab value — the CONTROLLED path, where the bar owns
6
+ * the underline and reports presses through {@link TabsProps.onValueChange}.
7
+ *
8
+ * OMIT it for the focus-driven path, where each trigger supplies its own
9
+ * {@link TabsTriggerProps.isFocused} and performs its own navigation. That is
10
+ * how `@oxyhq/bloom/tabs/expo-router` keeps the underline correct through
11
+ * deep links, browser Back and back gestures — none of which pass through a
12
+ * press handler. Providing both would put two writers on one shared value.
13
+ */
14
+ value?: string;
15
+ /** Called when a tab is selected. Controlled path only. */
16
+ onValueChange?: (value: string) => void;
17
+ /**
18
+ * Focus-driven path only: whether ANY trigger is currently focused.
19
+ *
20
+ * `false` fades the underline out where it stands. This is not the same as
21
+ * "the selection moved", and it is not hypothetical: a strip rendered by a
22
+ * LAYOUT keeps rendering while the layout's other, non-tab children are
23
+ * showing — a profile's `/followers` sits beside its tab routes under one
24
+ * layout — and without this the underline stays parked on whichever tab was
25
+ * last visited, asserting a selection that is not there.
26
+ *
27
+ * Fading is the whole response; the underline is deliberately NOT moved,
28
+ * because there is no position that means "nowhere" and travelling to a
29
+ * sentinel would drag it across every tab on the way out.
30
+ *
31
+ * `RouterTabs` sets it, so an app author never passes it by hand. Defaults to
32
+ * `true`, which is correct for the controlled path (a `value` naming no
33
+ * trigger simply leaves the underline where it was, as it always has).
34
+ */
35
+ hasSelection?: boolean;
8
36
  /** Visual variant for the tab bar. */
9
37
  variant?: TabsVariant;
10
38
  /**
@@ -26,8 +54,24 @@ export interface TabsTriggerProps {
26
54
  label: string;
27
55
  /** Icon rendered before the label. */
28
56
  icon?: React.ReactNode;
57
+ /**
58
+ * Optional tally rendered as a small muted number beside the label. Omitted
59
+ * (or `0`) renders nothing. The underline tracks the trigger's measured
60
+ * width, so a count never changes where it sits.
61
+ */
62
+ count?: number;
63
+ /**
64
+ * Whether the ROUTER considers this tab focused — the focus-driven path.
65
+ * Supplying it makes this trigger, not the bar, the writer of the shared
66
+ * underline, and suppresses {@link TabsProps.onValueChange} on press because
67
+ * navigation is then the caller's job. Leave it `undefined` on the controlled
68
+ * path.
69
+ */
70
+ isFocused?: boolean;
29
71
  /** Whether this tab is disabled. */
30
72
  disabled?: boolean;
73
+ /** Extra press handler. Runs on both paths, before any selection is reported. */
74
+ onPress?: () => void;
31
75
  style?: StyleProp<ViewStyle>;
32
76
  textStyle?: StyleProp<TextStyle>;
33
77
  }
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../src/tabs/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAEpE,MAAM,MAAM,WAAW,GAAG,WAAW,GAAG,QAAQ,GAAG,UAAU,CAAC;AAE9D,MAAM,WAAW,SAAS;IACxB,wCAAwC;IACxC,KAAK,EAAE,MAAM,CAAC;IACd,qCAAqC;IACrC,aAAa,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACvC,sCAAsC;IACtC,OAAO,CAAC,EAAE,WAAW,CAAC;IACtB;;;;;OAKG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,qDAAqD;IACrD,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;IAC1B,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC7B,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,gBAAgB;IAC/B,yCAAyC;IACzC,KAAK,EAAE,MAAM,CAAC;IACd,sBAAsB;IACtB,KAAK,EAAE,MAAM,CAAC;IACd,sCAAsC;IACtC,IAAI,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IACvB,oCAAoC;IACpC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC7B,SAAS,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;CAClC;AAED,MAAM,WAAW,gBAAgB;IAC/B,+CAA+C;IAC/C,KAAK,EAAE,MAAM,CAAC;IACd,qBAAqB;IACrB,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;IAC1B,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;CAC9B"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../src/tabs/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,cAAc,CAAC;AAEpE,MAAM,MAAM,WAAW,GAAG,WAAW,GAAG,QAAQ,GAAG,UAAU,CAAC;AAE9D,MAAM,WAAW,SAAS;IACxB;;;;;;;;;OASG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,2DAA2D;IAC3D,aAAa,CAAC,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IACxC;;;;;;;;;;;;;;;;;OAiBG;IACH,YAAY,CAAC,EAAE,OAAO,CAAC;IACvB,sCAAsC;IACtC,OAAO,CAAC,EAAE,WAAW,CAAC;IACtB;;;;;OAKG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,qDAAqD;IACrD,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;IAC1B,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC7B,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,gBAAgB;IAC/B,yCAAyC;IACzC,KAAK,EAAE,MAAM,CAAC;IACd,sBAAsB;IACtB,KAAK,EAAE,MAAM,CAAC;IACd,sCAAsC;IACtC,IAAI,CAAC,EAAE,KAAK,CAAC,SAAS,CAAC;IACvB;;;;OAIG;IACH,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;;;;;OAMG;IACH,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,oCAAoC;IACpC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,iFAAiF;IACjF,OAAO,CAAC,EAAE,MAAM,IAAI,CAAC;IACrB,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;IAC7B,SAAS,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;CAClC;AAED,MAAM,WAAW,gBAAgB;IAC/B,+CAA+C;IAC/C,KAAK,EAAE,MAAM,CAAC;IACd,qBAAqB;IACrB,QAAQ,EAAE,KAAK,CAAC,SAAS,CAAC;IAC1B,KAAK,CAAC,EAAE,SAAS,CAAC,SAAS,CAAC,CAAC;CAC9B"}
@@ -1 +1 @@
1
- {"version":3,"file":"index.web.d.ts","sourceRoot":"","sources":["../../../../src/scroll/index.web.tsx"],"names":[],"mappings":"AA6CA,OAAO,KAAK,EACV,wBAAwB,EACxB,uBAAuB,EACvB,2BAA2B,EAC5B,MAAM,SAAS,CAAC;AAEjB,OAAO,EAAE,yBAAyB,EAAE,MAAM,WAAW,CAAC;AAEtD,YAAY,EACV,iBAAiB,EACjB,gBAAgB,EAChB,wBAAwB,EACxB,8BAA8B,EAC9B,uBAAuB,EACvB,mBAAmB,EACnB,2BAA2B,GAC5B,MAAM,SAAS,CAAC;AA0DjB;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,uBAAuB,EAC/B,OAAO,CAAC,EAAE,2BAA2B,GACpC,wBAAwB,CA4J1B"}
1
+ {"version":3,"file":"index.web.d.ts","sourceRoot":"","sources":["../../../../src/scroll/index.web.tsx"],"names":[],"mappings":"AA6CA,OAAO,KAAK,EACV,wBAAwB,EACxB,uBAAuB,EACvB,2BAA2B,EAC5B,MAAM,SAAS,CAAC;AAEjB,OAAO,EAAE,yBAAyB,EAAE,MAAM,WAAW,CAAC;AAEtD,YAAY,EACV,iBAAiB,EACjB,gBAAgB,EAChB,wBAAwB,EACxB,8BAA8B,EAC9B,uBAAuB,EACvB,mBAAmB,EACnB,2BAA2B,GAC5B,MAAM,SAAS,CAAC;AA0DjB;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,uBAAuB,EAC/B,OAAO,CAAC,EAAE,2BAA2B,GACpC,wBAAwB,CAgM1B"}
@@ -8,17 +8,30 @@ export interface ResolvedScroller {
8
8
  getOffset: () => number;
9
9
  setOffset: (offset: number) => void;
10
10
  /**
11
- * Whether the scroll container can currently hold a non-zero offset, i.e.
12
- * its content is taller than its viewport (`scrollHeight > clientHeight`).
11
+ * The largest offset the container can currently hold `scrollHeight -
12
+ * clientHeight`, clamped at 0. Answers two questions the save path needs:
13
+ * whether a 0 read is real (`maxOffset > 0`) and whether an offset is sitting
14
+ * exactly at the bottom, which is the signature of a browser clamp.
13
15
  *
14
16
  * React Navigation's web stack collapses a hidden background screen so its
15
17
  * content height drops to the viewport height; while collapsed the container
16
18
  * cannot be scrolled and its `scrollTop` is forced to 0. The hook uses this
17
19
  * to ignore a spurious 0 read coming from a collapsed container rather than
18
- * persisting it over a previously-saved good offset. The `'window'` scroller
19
- * is never collapsed by the navigator, so it always reports `true`.
20
+ * persisting it over a previously-saved good offset.
21
+ *
22
+ * The DOCUMENT collapses the same way, so the `'window'` sentinel answers the
23
+ * same question honestly rather than hardcoding `true`. It used to, on the
24
+ * premise that the navigator never collapses the document — false under a
25
+ * tabbed navigator, where every tab stays MOUNTED and non-focused ones are
26
+ * `display: none` (expo-router's `ui/TabSlot`, and react-native-screens' web
27
+ * `Screen` at `activityState === 0`). Measured in Chrome 150: switching from
28
+ * a tall tab to a short one drops `documentElement.scrollHeight` from 8040 to
29
+ * the 800px viewport, forces `scrollY` to 0 and dispatches two `scroll`
30
+ * events — which the outgoing tab's still-attached listener would otherwise
31
+ * persist as 0 over its real offset, because blur is emitted from an effect
32
+ * that has not run yet.
20
33
  */
21
- canScroll: () => boolean;
34
+ getMaxOffset: () => number;
22
35
  }
23
36
  /**
24
37
  * Build a {@link ResolvedScroller} for a target. The window sentinel reads and
@@ -1 +1 @@
1
- {"version":3,"file":"scrollable.web.d.ts","sourceRoot":"","sources":["../../../../src/scroll/scrollable.web.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAoB,uBAAuB,EAAE,MAAM,SAAS,CAAC;AAEzE;;;;GAIG;AACH,MAAM,WAAW,gBAAgB;IAC/B,SAAS,EAAE,MAAM,MAAM,CAAC;IACxB,SAAS,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IACpC;;;;;;;;;;OAUG;IACH,SAAS,EAAE,MAAM,OAAO,CAAC;CAC1B;AAuCD;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,uBAAuB,GAAG,gBAAgB,CAyBhF"}
1
+ {"version":3,"file":"scrollable.web.d.ts","sourceRoot":"","sources":["../../../../src/scroll/scrollable.web.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAoB,uBAAuB,EAAE,MAAM,SAAS,CAAC;AAEzE;;;;GAIG;AACH,MAAM,WAAW,gBAAgB;IAC/B,SAAS,EAAE,MAAM,MAAM,CAAC;IACxB,SAAS,EAAE,CAAC,MAAM,EAAE,MAAM,KAAK,IAAI,CAAC;IACpC;;;;;;;;;;;;;;;;;;;;;;;OAuBG;IACH,YAAY,EAAE,MAAM,MAAM,CAAC;CAC5B;AAuCD;;;;GAIG;AACH,wBAAgB,cAAc,CAAC,MAAM,EAAE,uBAAuB,GAAG,gBAAgB,CAkChF"}
@@ -1,6 +1,46 @@
1
1
  import React from 'react';
2
2
  import type { TabsProps, TabsTriggerProps, TabsContentProps } from './types';
3
- export declare const Tabs: React.NamedExoticComponent<TabsProps>;
3
+ /**
4
+ * The imperative surface a horizontal drag gesture drives the underline through.
5
+ *
6
+ * It lives here rather than in the gesture's own code because every quantity the
7
+ * drag needs — where the neighbouring trigger is, how wide it is, whether there
8
+ * IS a neighbour — is measured layout the strip already holds and nothing
9
+ * outside it can see. The gesture supplies one number, the finger's delta; the
10
+ * strip decides what that means.
11
+ *
12
+ * Obtained via a `ref` on `Tabs`. The gesture belongs in the LAYOUT beside the
13
+ * strip, never inside a tab screen — a gesture mounted in a screen is destroyed
14
+ * by the navigation it commits, mid-release.
15
+ */
16
+ export interface TabsDragController {
17
+ /**
18
+ * Feed the raw horizontal finger delta (`translationX`).
19
+ *
20
+ * The underline travels OPPOSITE the finger, because that is where the
21
+ * incoming tab is: dragging left reveals the NEXT tab, which sits to the
22
+ * right. Travel is clamped at the neighbour — the underline stops exactly
23
+ * where it would land, so it never promises more than the release delivers —
24
+ * and at the first/last tab it rubber-bands instead.
25
+ *
26
+ * Returns the value that releasing NOW would commit, or `null` for "no
27
+ * commit": either the drag has not passed the threshold, or there is no
28
+ * neighbour in that direction.
29
+ */
30
+ drag(translationX: number): string | null;
31
+ /**
32
+ * End the drag. Pass the value `drag` last returned to keep the underline
33
+ * where the finger left it while the caller navigates; pass `null` to spring
34
+ * it home.
35
+ *
36
+ * On a commit the offset is FOLDED into the underline's base position rather
37
+ * than zeroed, so the focus change that follows springs from where the finger
38
+ * actually let go. Zeroing first would snap backwards for a frame and then
39
+ * animate forwards from the wrong place.
40
+ */
41
+ release(committed: string | null): void;
42
+ }
43
+ export declare const Tabs: React.NamedExoticComponent<TabsProps & React.RefAttributes<TabsDragController>>;
4
44
  export declare const TabsTrigger: React.NamedExoticComponent<TabsTriggerProps>;
5
45
  export declare const TabsContent: React.NamedExoticComponent<TabsContentProps>;
6
46
  //# sourceMappingURL=Tabs.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"Tabs.d.ts","sourceRoot":"","sources":["../../../../src/tabs/Tabs.tsx"],"names":[],"mappings":"AAAA,OAAO,KAQN,MAAM,OAAO,CAAC;AAiBf,OAAO,KAAK,EAAE,SAAS,EAAE,gBAAgB,EAAE,gBAAgB,EAAe,MAAM,SAAS,CAAC;AA4Q1F,eAAO,MAAM,IAAI,uCAAyB,CAAC;AAG3C,eAAO,MAAM,WAAW,8CAAqB,CAAC;AAG9C,eAAO,MAAM,WAAW,8CAA0B,CAAC"}
1
+ {"version":3,"file":"Tabs.d.ts","sourceRoot":"","sources":["../../../../src/tabs/Tabs.tsx"],"names":[],"mappings":"AAAA,OAAO,KAUN,MAAM,OAAO,CAAC;AA4Bf,OAAO,KAAK,EAAE,SAAS,EAAE,gBAAgB,EAAE,gBAAgB,EAAe,MAAM,SAAS,CAAC;AAwD1F;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,kBAAkB;IACjC;;;;;;;;;;;;OAYG;IACH,IAAI,CAAC,YAAY,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;IAC1C;;;;;;;;;OASG;IACH,OAAO,CAAC,SAAS,EAAE,MAAM,GAAG,IAAI,GAAG,IAAI,CAAC;CACzC;AAmbD,eAAO,MAAM,IAAI,iFAAyB,CAAC;AAG3C,eAAO,MAAM,WAAW,8CAAqB,CAAC;AAG9C,eAAO,MAAM,WAAW,8CAA0B,CAAC"}