@oxyhq/bloom 0.6.21 → 0.6.23

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 (49) hide show
  1. package/lib/commonjs/scroll/index.js +39 -0
  2. package/lib/commonjs/scroll/index.js.map +1 -0
  3. package/lib/commonjs/scroll/index.web.js +226 -0
  4. package/lib/commonjs/scroll/index.web.js.map +1 -0
  5. package/lib/commonjs/scroll/scrollable.web.js +69 -0
  6. package/lib/commonjs/scroll/scrollable.web.js.map +1 -0
  7. package/lib/commonjs/scroll/store.js +90 -0
  8. package/lib/commonjs/scroll/store.js.map +1 -0
  9. package/lib/commonjs/scroll/types.js +6 -0
  10. package/lib/commonjs/scroll/types.js.map +1 -0
  11. package/lib/module/scroll/index.js +34 -0
  12. package/lib/module/scroll/index.js.map +1 -0
  13. package/lib/module/scroll/index.web.js +220 -0
  14. package/lib/module/scroll/index.web.js.map +1 -0
  15. package/lib/module/scroll/scrollable.web.js +65 -0
  16. package/lib/module/scroll/scrollable.web.js.map +1 -0
  17. package/lib/module/scroll/store.js +84 -0
  18. package/lib/module/scroll/store.js.map +1 -0
  19. package/lib/module/scroll/types.js +4 -0
  20. package/lib/module/scroll/types.js.map +1 -0
  21. package/lib/typescript/commonjs/scroll/index.d.ts +27 -0
  22. package/lib/typescript/commonjs/scroll/index.d.ts.map +1 -0
  23. package/lib/typescript/commonjs/scroll/index.web.d.ts +26 -0
  24. package/lib/typescript/commonjs/scroll/index.web.d.ts.map +1 -0
  25. package/lib/typescript/commonjs/scroll/scrollable.web.d.ts +29 -0
  26. package/lib/typescript/commonjs/scroll/scrollable.web.d.ts.map +1 -0
  27. package/lib/typescript/commonjs/scroll/store.d.ts +46 -0
  28. package/lib/typescript/commonjs/scroll/store.d.ts.map +1 -0
  29. package/lib/typescript/commonjs/scroll/types.d.ts +46 -0
  30. package/lib/typescript/commonjs/scroll/types.d.ts.map +1 -0
  31. package/lib/typescript/module/scroll/index.d.ts +27 -0
  32. package/lib/typescript/module/scroll/index.d.ts.map +1 -0
  33. package/lib/typescript/module/scroll/index.web.d.ts +26 -0
  34. package/lib/typescript/module/scroll/index.web.d.ts.map +1 -0
  35. package/lib/typescript/module/scroll/scrollable.web.d.ts +29 -0
  36. package/lib/typescript/module/scroll/scrollable.web.d.ts.map +1 -0
  37. package/lib/typescript/module/scroll/store.d.ts +46 -0
  38. package/lib/typescript/module/scroll/store.d.ts.map +1 -0
  39. package/lib/typescript/module/scroll/types.d.ts +46 -0
  40. package/lib/typescript/module/scroll/types.d.ts.map +1 -0
  41. package/package.json +22 -1
  42. package/src/__tests__/scroll-native.test.ts +25 -0
  43. package/src/__tests__/scroll-store.test.ts +85 -0
  44. package/src/__tests__/scroll-web.test.tsx +325 -0
  45. package/src/scroll/index.ts +47 -0
  46. package/src/scroll/index.web.tsx +253 -0
  47. package/src/scroll/scrollable.web.ts +92 -0
  48. package/src/scroll/store.ts +84 -0
  49. package/src/scroll/types.ts +48 -0
@@ -0,0 +1,220 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Web variant of the scroll-restoration primitive.
5
+ *
6
+ * Mirrors the proven Bluesky pattern (`history.scrollRestoration = 'manual'`
7
+ * plus an in-memory `Map<routeKey, offset>`) with two deliberate differences
8
+ * forced by Oxy's layouts and the behaviour of React Navigation's web stack:
9
+ *
10
+ * 1. Bluesky restores the WINDOW scroller, whereas Oxy apps keep multi-column
11
+ * layouts whose feed scrolls an INNER container. So we restore the offset
12
+ * of a caller-registered scrollable (a ref to an element / RN scroll
13
+ * component, or the `'window'` sentinel), keyed by the active route.
14
+ *
15
+ * 2. React Navigation's web stack HIDES the background screen on push. While
16
+ * hidden, the previous screen's scroll container collapses
17
+ * (`scrollHeight === clientHeight`) and the navigator forces its
18
+ * `scrollTop` to 0. The screen is NOT unmounted, so a virtualized list
19
+ * (e.g. FlashList) keeps its rows but re-lays them out over SEVERAL frames
20
+ * once the screen is re-shown. Two problems follow, both handled here:
21
+ *
22
+ * (a) A blur-time read of `scrollTop` returns the navigator's forced 0,
23
+ * not the user's real offset — saving it would clobber the good
24
+ * value. We therefore persist the last offset OBSERVED by the live
25
+ * scroll listener, never a fresh read taken at blur time.
26
+ *
27
+ * (b) A single-frame restore writes `scrollTop` while the list is still
28
+ * collapsed; the write is clamped to 0 and never re-applied once the
29
+ * content grows. We therefore re-apply the target offset across a
30
+ * bounded run of animation frames, stopping as soon as the write
31
+ * sticks (the content has grown tall enough) or a small frame cap is
32
+ * reached.
33
+ *
34
+ * Native bundlers use `./index.ts` (a no-op); web bundlers select this file via
35
+ * the `"browser"` export condition in `package.json`.
36
+ */
37
+ import { createContext, useCallback, useContext, useMemo, useRef } from 'react';
38
+ import { useFocusEffect, useRoute } from '@react-navigation/native';
39
+ import { createScroller } from "./scrollable.web.js";
40
+ import { ScrollOffsetStore, deriveScrollKey } from "./store.js";
41
+ import { jsx as _jsx } from "react/jsx-runtime";
42
+ const ScrollOffsetContext = /*#__PURE__*/createContext(null);
43
+ ScrollOffsetContext.displayName = 'BloomScrollOffsetContext';
44
+
45
+ /**
46
+ * Maximum number of animation frames the focus restore will re-apply the saved
47
+ * offset before giving up. A virtualized list re-lays out its rows over a
48
+ * handful of frames after its screen is re-shown; ~30 frames (≈0.5s at 60fps)
49
+ * is comfortably longer than any observed relayout while staying short enough
50
+ * that the loop never lingers as a perceptible cost. The loop normally exits
51
+ * far earlier — as soon as the write sticks.
52
+ */
53
+ const RESTORE_FRAME_CAP = 30;
54
+
55
+ /**
56
+ * Tolerance (in CSS pixels) for considering a restore "stuck". After writing
57
+ * `element.scrollTop = target`, the browser may clamp it to the current
58
+ * `scrollHeight - clientHeight`; if the resulting offset is within this many
59
+ * pixels of the target we treat the restore as complete. Sub-pixel rounding and
60
+ * fractional device-pixel ratios make an exact equality check unreliable.
61
+ */
62
+ const RESTORE_STICK_TOLERANCE_PX = 2;
63
+
64
+ /**
65
+ * Switch the browser to manual scroll restoration exactly once per document.
66
+ *
67
+ * The browser's default `'auto'` restoration fights our manual restore on
68
+ * Back/Forward navigations. Doing this at module scope (guarded for SSR) means
69
+ * it is set before any provider mounts, matching Bluesky's module-level call.
70
+ */
71
+ if (typeof history !== 'undefined' && 'scrollRestoration' in history) {
72
+ history.scrollRestoration = 'manual';
73
+ }
74
+
75
+ /**
76
+ * Holds the per-route offset map for the subtree. One provider near the app
77
+ * root is enough; the store lives for the document's lifetime so offsets
78
+ * survive navigating away and back (including browser Back/Forward).
79
+ */
80
+ export function ScrollRestorationProvider({
81
+ children
82
+ }) {
83
+ const store = useMemo(() => new ScrollOffsetStore(), []);
84
+ return /*#__PURE__*/_jsx(ScrollOffsetContext.Provider, {
85
+ value: store,
86
+ children: children
87
+ });
88
+ }
89
+ function useScrollOffsetStore() {
90
+ const store = useContext(ScrollOffsetContext);
91
+ if (store === null) {
92
+ throw new Error('useScrollRestoration must be used within a <ScrollRestorationProvider>.');
93
+ }
94
+ return store;
95
+ }
96
+
97
+ /**
98
+ * Preserve and restore the scroll offset of `target` across navigation, keyed
99
+ * by the active route (plus an optional `options.key` for routes that host
100
+ * multiple scrollables).
101
+ *
102
+ * Behaviour (web):
103
+ * - On every scroll while the screen is focused, the current offset is recorded
104
+ * in memory and persisted. This live stream of saves is the source of truth.
105
+ * - On focus, the saved offset is re-applied across a bounded run of animation
106
+ * frames, stopping as soon as the write sticks (the list has re-rendered its
107
+ * rows and grown tall enough) or {@link RESTORE_FRAME_CAP} is reached. A
108
+ * saved offset of 0 is a no-op (nothing to restore).
109
+ * - On blur, the LAST OBSERVED offset is persisted as a final safety net — not
110
+ * a fresh `scrollTop` read, which the navigator may already have forced to 0
111
+ * while collapsing the hidden screen.
112
+ */
113
+ export function useScrollRestoration(target, options) {
114
+ const store = useScrollOffsetStore();
115
+ const route = useRoute();
116
+ const subKey = options?.key;
117
+ const enabled = options?.enabled ?? true;
118
+ const scrollKey = deriveScrollKey(route.key, subKey);
119
+
120
+ // Keep the latest target/enabled/key in refs so the focus effect can read
121
+ // them without being re-subscribed on every render.
122
+ const targetRef = useRef(target);
123
+ targetRef.current = target;
124
+ const enabledRef = useRef(enabled);
125
+ enabledRef.current = enabled;
126
+ const scrollKeyRef = useRef(scrollKey);
127
+ scrollKeyRef.current = scrollKey;
128
+ useFocusEffect(
129
+ // The effect identity is intentionally stable across renders: it reads all
130
+ // varying inputs from refs. React Navigation re-runs it on each focus.
131
+ useCallback(() => {
132
+ const key = scrollKeyRef.current;
133
+ if (!enabledRef.current || key === null) return undefined;
134
+ const scroller = createScroller(targetRef.current);
135
+ const element = targetRef.current === 'window' ? typeof window !== 'undefined' ? window : null : resolveScrollEventTarget(targetRef.current);
136
+
137
+ // The last offset the live scroll listener observed for this focus
138
+ // session. This — not a blur-time `getOffset()` — is what we persist on
139
+ // blur, because by blur time the navigator may have collapsed the
140
+ // hidden screen and forced its `scrollTop` to 0 (bug A). `null` means
141
+ // the user never scrolled this session, so there is nothing newer to
142
+ // persist than what the scroll listener already saved live.
143
+ let lastObservedOffset = null;
144
+ const save = () => {
145
+ const currentKey = scrollKeyRef.current;
146
+ if (!enabledRef.current || currentKey === null) return;
147
+ const offset = scroller.getOffset();
148
+ // Ignore a spurious 0 produced by the navigator collapsing a hidden
149
+ // background screen: while collapsed the container cannot scroll, so
150
+ // its `scrollTop` is forced to 0. Persisting it would clobber the
151
+ // good offset recorded by earlier live saves (bug A). A genuine
152
+ // scroll-to-top keeps the container scrollable and is saved normally.
153
+ if (offset === 0 && !scroller.canScroll()) return;
154
+ lastObservedOffset = offset;
155
+ store.save(currentKey, offset);
156
+ };
157
+
158
+ // Restore across a bounded run of frames. A freshly re-shown
159
+ // virtualized list re-lays out its rows over several frames, so a
160
+ // single write while it is still collapsed would be clamped to 0 and
161
+ // never re-applied (bug B). We re-apply each frame until the write
162
+ // sticks or the frame cap is hit.
163
+ const targetOffset = store.read(key);
164
+ let rafId = null;
165
+ if (targetOffset > 0 && typeof requestAnimationFrame !== 'undefined') {
166
+ let framesLeft = RESTORE_FRAME_CAP;
167
+ const applyOffset = () => {
168
+ rafId = null;
169
+ scroller.setOffset(targetOffset);
170
+ framesLeft -= 1;
171
+ // Stop once the write took effect (content grew tall enough) or we
172
+ // exhaust the frame budget. `getOffset` re-reads the clamped value.
173
+ const reached = Math.abs(scroller.getOffset() - targetOffset) <= RESTORE_STICK_TOLERANCE_PX;
174
+ if (!reached && framesLeft > 0) {
175
+ rafId = requestAnimationFrame(applyOffset);
176
+ }
177
+ };
178
+ rafId = requestAnimationFrame(applyOffset);
179
+ }
180
+ element?.addEventListener('scroll', save, {
181
+ passive: true
182
+ });
183
+ return () => {
184
+ if (rafId !== null) cancelAnimationFrame(rafId);
185
+ element?.removeEventListener('scroll', save);
186
+ // Final capture on blur: persist the last offset the scroll listener
187
+ // OBSERVED, never a fresh read (which the navigator may have forced
188
+ // to 0 while collapsing the hidden screen). When the user never
189
+ // scrolled this session there is nothing newer to persist than the
190
+ // live saves already recorded.
191
+ if (lastObservedOffset !== null) {
192
+ const currentKey = scrollKeyRef.current;
193
+ if (enabledRef.current && currentKey !== null) {
194
+ store.save(currentKey, lastObservedOffset);
195
+ }
196
+ }
197
+ };
198
+ }, [store]));
199
+ }
200
+
201
+ /**
202
+ * Resolve the EventTarget to listen for `scroll` on. RNW scroll components
203
+ * expose `getScrollableNode()`; raw refs may hold the DOM node directly.
204
+ */
205
+ function resolveScrollEventTarget(target) {
206
+ const current = target.current;
207
+ if (current == null) return null;
208
+ if (typeof EventTarget !== 'undefined' && current instanceof EventTarget) {
209
+ return current;
210
+ }
211
+ const handle = current;
212
+ if (typeof handle.getScrollableNode === 'function') {
213
+ const node = handle.getScrollableNode();
214
+ if (typeof EventTarget !== 'undefined' && node instanceof EventTarget) {
215
+ return node;
216
+ }
217
+ }
218
+ return null;
219
+ }
220
+ //# sourceMappingURL=index.web.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["createContext","useCallback","useContext","useMemo","useRef","useFocusEffect","useRoute","createScroller","ScrollOffsetStore","deriveScrollKey","jsx","_jsx","ScrollOffsetContext","displayName","RESTORE_FRAME_CAP","RESTORE_STICK_TOLERANCE_PX","history","scrollRestoration","ScrollRestorationProvider","children","store","Provider","value","useScrollOffsetStore","Error","useScrollRestoration","target","options","route","subKey","key","enabled","scrollKey","targetRef","current","enabledRef","scrollKeyRef","undefined","scroller","element","window","resolveScrollEventTarget","lastObservedOffset","save","currentKey","offset","getOffset","canScroll","targetOffset","read","rafId","requestAnimationFrame","framesLeft","applyOffset","setOffset","reached","Math","abs","addEventListener","passive","cancelAnimationFrame","removeEventListener","EventTarget","handle","getScrollableNode","node"],"sourceRoot":"../../../src","sources":["scroll/index.web.tsx"],"mappings":"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","ignoreList":[]}
@@ -0,0 +1,65 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * A normalized read/write interface over whatever the caller registered, so
5
+ * the hook does not branch on target shape. All methods are safe no-ops when
6
+ * the underlying element is not yet (or no longer) attached.
7
+ */
8
+
9
+ function isElement(value) {
10
+ return typeof HTMLElement !== 'undefined' && value instanceof HTMLElement;
11
+ }
12
+ function hasGetScrollableNode(value) {
13
+ return typeof value === 'object' && value !== null && typeof value.getScrollableNode === 'function';
14
+ }
15
+
16
+ /**
17
+ * Resolve the live DOM element a target currently points at, or `null`.
18
+ *
19
+ * RNW scroll components expose `getScrollableNode()`; plain refs may hold the
20
+ * DOM node directly. We never cache the node because RNW can swap it across
21
+ * renders — we re-resolve on every read/write.
22
+ */
23
+ function resolveElement(target) {
24
+ if (target === 'window') return null;
25
+ const current = target.current;
26
+ if (current == null) return null;
27
+ if (isElement(current)) return current;
28
+ if (hasGetScrollableNode(current)) {
29
+ const node = current.getScrollableNode();
30
+ if (isElement(node)) return node;
31
+ }
32
+ return null;
33
+ }
34
+
35
+ /**
36
+ * Build a {@link ResolvedScroller} for a target. The window sentinel reads and
37
+ * writes the document scroller; everything else operates on the resolved
38
+ * element's `scrollTop`.
39
+ */
40
+ export function createScroller(target) {
41
+ if (target === 'window') {
42
+ return {
43
+ getOffset: () => typeof window === 'undefined' ? 0 : window.scrollY,
44
+ setOffset: offset => {
45
+ if (typeof window !== 'undefined') window.scrollTo(0, offset);
46
+ },
47
+ canScroll: () => true
48
+ };
49
+ }
50
+ return {
51
+ getOffset: () => {
52
+ const element = resolveElement(target);
53
+ return element ? element.scrollTop : 0;
54
+ },
55
+ setOffset: offset => {
56
+ const element = resolveElement(target);
57
+ if (element) element.scrollTop = offset;
58
+ },
59
+ canScroll: () => {
60
+ const element = resolveElement(target);
61
+ return element ? element.scrollHeight > element.clientHeight : false;
62
+ }
63
+ };
64
+ }
65
+ //# sourceMappingURL=scrollable.web.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["isElement","value","HTMLElement","hasGetScrollableNode","getScrollableNode","resolveElement","target","current","node","createScroller","getOffset","window","scrollY","setOffset","offset","scrollTo","canScroll","element","scrollTop","scrollHeight","clientHeight"],"sourceRoot":"../../../src","sources":["scroll/scrollable.web.ts"],"mappings":";;AAEA;AACA;AACA;AACA;AACA;;AAkBA,SAASA,SAASA,CAACC,KAAc,EAAwB;EACvD,OAAO,OAAOC,WAAW,KAAK,WAAW,IAAID,KAAK,YAAYC,WAAW;AAC3E;AAEA,SAASC,oBAAoBA,CAC3BF,KAAc,EACkD;EAChE,OACE,OAAOA,KAAK,KAAK,QAAQ,IACzBA,KAAK,KAAK,IAAI,IACd,OAAQA,KAAK,CAAsBG,iBAAiB,KAAK,UAAU;AAEvE;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,SAASC,cAAcA,CAACC,MAA+B,EAAsB;EAC3E,IAAIA,MAAM,KAAK,QAAQ,EAAE,OAAO,IAAI;EAEpC,MAAMC,OAAO,GAAID,MAAM,CAA0BC,OAAO;EACxD,IAAIA,OAAO,IAAI,IAAI,EAAE,OAAO,IAAI;EAEhC,IAAIP,SAAS,CAACO,OAAO,CAAC,EAAE,OAAOA,OAAO;EAEtC,IAAIJ,oBAAoB,CAACI,OAAO,CAAC,EAAE;IACjC,MAAMC,IAAI,GAAGD,OAAO,CAACH,iBAAiB,CAAC,CAAC;IACxC,IAAIJ,SAAS,CAACQ,IAAI,CAAC,EAAE,OAAOA,IAAI;EAClC;EAEA,OAAO,IAAI;AACb;;AAEA;AACA;AACA;AACA;AACA;AACA,OAAO,SAASC,cAAcA,CAACH,MAA+B,EAAoB;EAChF,IAAIA,MAAM,KAAK,QAAQ,EAAE;IACvB,OAAO;MACLI,SAAS,EAAEA,CAAA,KAAO,OAAOC,MAAM,KAAK,WAAW,GAAG,CAAC,GAAGA,MAAM,CAACC,OAAQ;MACrEC,SAAS,EAAGC,MAAM,IAAK;QACrB,IAAI,OAAOH,MAAM,KAAK,WAAW,EAAEA,MAAM,CAACI,QAAQ,CAAC,CAAC,EAAED,MAAM,CAAC;MAC/D,CAAC;MACDE,SAAS,EAAEA,CAAA,KAAM;IACnB,CAAC;EACH;EAEA,OAAO;IACLN,SAAS,EAAEA,CAAA,KAAM;MACf,MAAMO,OAAO,GAAGZ,cAAc,CAACC,MAAM,CAAC;MACtC,OAAOW,OAAO,GAAGA,OAAO,CAACC,SAAS,GAAG,CAAC;IACxC,CAAC;IACDL,SAAS,EAAGC,MAAM,IAAK;MACrB,MAAMG,OAAO,GAAGZ,cAAc,CAACC,MAAM,CAAC;MACtC,IAAIW,OAAO,EAAEA,OAAO,CAACC,SAAS,GAAGJ,MAAM;IACzC,CAAC;IACDE,SAAS,EAAEA,CAAA,KAAM;MACf,MAAMC,OAAO,GAAGZ,cAAc,CAACC,MAAM,CAAC;MACtC,OAAOW,OAAO,GAAGA,OAAO,CAACE,YAAY,GAAGF,OAAO,CAACG,YAAY,GAAG,KAAK;IACtE;EACF,CAAC;AACH","ignoreList":[]}
@@ -0,0 +1,84 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Pure, platform-agnostic core of the scroll-restoration primitive.
5
+ *
6
+ * This file deliberately contains NO React and NO DOM/native imports so the
7
+ * logic can be unit-tested in isolation and shared verbatim by the web and
8
+ * native barrels. The web barrel drives it with real scroll offsets; the
9
+ * native barrel never instantiates it (native-stack already restores scroll).
10
+ */
11
+
12
+ /**
13
+ * Separator between the navigation route key and an optional caller-supplied
14
+ * sub-key. A route may host more than one independently-scrolling list (e.g.
15
+ * a tabbed profile screen), so each list contributes its own sub-key.
16
+ *
17
+ * `\0` (NUL) can never appear in a React Navigation route key or in a
18
+ * developer-authored sub-key, so it is collision-free as a delimiter. It is
19
+ * written as an escape sequence (not a literal byte) so the source stays
20
+ * text-diffable.
21
+ */
22
+ const COMPOSITE_KEY_SEPARATOR = '\0';
23
+
24
+ /**
25
+ * Derive the storage key for a scrollable from its owning route key and an
26
+ * optional caller sub-key.
27
+ *
28
+ * - `routeKey` is React Navigation's stable per-route `route.key`.
29
+ * - `subKey` distinguishes multiple scrollables that share a single route.
30
+ *
31
+ * Returns `null` when there is no route key to anchor against (the scrollable
32
+ * is not inside a navigator), which the caller treats as "do not persist".
33
+ */
34
+ export function deriveScrollKey(routeKey, subKey) {
35
+ if (!routeKey) return null;
36
+ if (subKey === undefined || subKey === '') return routeKey;
37
+ return `${routeKey}${COMPOSITE_KEY_SEPARATOR}${subKey}`;
38
+ }
39
+
40
+ /**
41
+ * In-memory map of `scrollKey -> last-known scroll offset`.
42
+ *
43
+ * Mirrors the semantics of Bluesky's `Map<screenKey, scrollY>`: offsets live
44
+ * only for the lifetime of the document/session. We never persist them — a
45
+ * full reload should start at the top — and we never auto-evict, because a
46
+ * route can be revisited via browser Forward/Back long after it blurred.
47
+ */
48
+ export class ScrollOffsetStore {
49
+ offsets = new Map();
50
+
51
+ /** Persist the offset for a key. A negative offset is clamped to 0. */
52
+ save(key, offset) {
53
+ this.offsets.set(key, offset > 0 ? offset : 0);
54
+ }
55
+
56
+ /**
57
+ * Read the saved offset for a key. Returns `0` when nothing was saved, so
58
+ * callers can restore unconditionally (an unseen list restores to the top).
59
+ */
60
+ read(key) {
61
+ return this.offsets.get(key) ?? 0;
62
+ }
63
+
64
+ /** Whether an offset was ever saved for this key. */
65
+ has(key) {
66
+ return this.offsets.has(key);
67
+ }
68
+
69
+ /** Drop a saved offset (e.g. when a route is permanently removed). */
70
+ forget(key) {
71
+ this.offsets.delete(key);
72
+ }
73
+
74
+ /** Clear every saved offset. Primarily for tests and full resets. */
75
+ clear() {
76
+ this.offsets.clear();
77
+ }
78
+
79
+ /** Number of distinct keys currently tracked. */
80
+ get size() {
81
+ return this.offsets.size;
82
+ }
83
+ }
84
+ //# sourceMappingURL=store.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["COMPOSITE_KEY_SEPARATOR","deriveScrollKey","routeKey","subKey","undefined","ScrollOffsetStore","offsets","Map","save","key","offset","set","read","get","has","forget","delete","clear","size"],"sourceRoot":"../../../src","sources":["scroll/store.ts"],"mappings":";;AAAA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,MAAMA,uBAAuB,GAAG,IAAI;;AAEpC;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,OAAO,SAASC,eAAeA,CAC7BC,QAA4B,EAC5BC,MAAe,EACA;EACf,IAAI,CAACD,QAAQ,EAAE,OAAO,IAAI;EAC1B,IAAIC,MAAM,KAAKC,SAAS,IAAID,MAAM,KAAK,EAAE,EAAE,OAAOD,QAAQ;EAC1D,OAAO,GAAGA,QAAQ,GAAGF,uBAAuB,GAAGG,MAAM,EAAE;AACzD;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,OAAO,MAAME,iBAAiB,CAAC;EACZC,OAAO,GAAG,IAAIC,GAAG,CAAiB,CAAC;;EAEpD;EACAC,IAAIA,CAACC,GAAW,EAAEC,MAAc,EAAQ;IACtC,IAAI,CAACJ,OAAO,CAACK,GAAG,CAACF,GAAG,EAAEC,MAAM,GAAG,CAAC,GAAGA,MAAM,GAAG,CAAC,CAAC;EAChD;;EAEA;AACF;AACA;AACA;EACEE,IAAIA,CAACH,GAAW,EAAU;IACxB,OAAO,IAAI,CAACH,OAAO,CAACO,GAAG,CAACJ,GAAG,CAAC,IAAI,CAAC;EACnC;;EAEA;EACAK,GAAGA,CAACL,GAAW,EAAW;IACxB,OAAO,IAAI,CAACH,OAAO,CAACQ,GAAG,CAACL,GAAG,CAAC;EAC9B;;EAEA;EACAM,MAAMA,CAACN,GAAW,EAAQ;IACxB,IAAI,CAACH,OAAO,CAACU,MAAM,CAACP,GAAG,CAAC;EAC1B;;EAEA;EACAQ,KAAKA,CAAA,EAAS;IACZ,IAAI,CAACX,OAAO,CAACW,KAAK,CAAC,CAAC;EACtB;;EAEA;EACA,IAAIC,IAAIA,CAAA,EAAW;IACjB,OAAO,IAAI,CAACZ,OAAO,CAACY,IAAI;EAC1B;AACF","ignoreList":[]}
@@ -0,0 +1,4 @@
1
+ "use strict";
2
+
3
+ export {};
4
+ //# sourceMappingURL=types.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":[],"sourceRoot":"../../../src","sources":["scroll/types.ts"],"mappings":"","ignoreList":[]}
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Native variant of the scroll-restoration primitive — a deliberate no-op.
3
+ *
4
+ * React Navigation's native-stack keeps every screen mounted while it is in the
5
+ * stack, so a list's scroll position survives a push/pop for free. There is
6
+ * nothing to save or restore. We still ship the full API surface (provider +
7
+ * hook) so consumers write one set of call sites that compile and run on every
8
+ * platform; on native the provider just renders its children and the hook does
9
+ * nothing.
10
+ *
11
+ * Web bundlers select `./index.web` via the `"browser"` export condition in
12
+ * `package.json`; native bundlers fall through to this file.
13
+ */
14
+ import type { ReactElement } from 'react';
15
+ import type { ScrollRestorationProviderProps, ScrollRestorationTarget, UseScrollRestorationOptions } from './types';
16
+ export type { ScrollableHandle, ScrollRestorationProviderProps, ScrollRestorationTarget, UseScrollRestorationOptions, } from './types';
17
+ /**
18
+ * No-op provider. Renders children unchanged — native scroll persistence is
19
+ * handled by the navigator, so no per-route state is kept.
20
+ */
21
+ export declare function ScrollRestorationProvider({ children, }: ScrollRestorationProviderProps): ReactElement;
22
+ /**
23
+ * No-op hook. Accepts the same arguments as the web implementation so call
24
+ * sites are identical across platforms.
25
+ */
26
+ export declare function useScrollRestoration(_target: ScrollRestorationTarget, _options?: UseScrollRestorationOptions): void;
27
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/scroll/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,OAAO,CAAC;AAC1C,OAAO,KAAK,EACV,8BAA8B,EAC9B,uBAAuB,EACvB,2BAA2B,EAC5B,MAAM,SAAS,CAAC;AAEjB,YAAY,EACV,gBAAgB,EAChB,8BAA8B,EAC9B,uBAAuB,EACvB,2BAA2B,GAC5B,MAAM,SAAS,CAAC;AAEjB;;;GAGG;AACH,wBAAgB,yBAAyB,CAAC,EACxC,QAAQ,GACT,EAAE,8BAA8B,GAAG,YAAY,CAE/C;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,uBAAuB,EAChC,QAAQ,CAAC,EAAE,2BAA2B,GACrC,IAAI,CAEN"}
@@ -0,0 +1,26 @@
1
+ import type { ScrollRestorationProviderProps, ScrollRestorationTarget, UseScrollRestorationOptions } from './types';
2
+ export type { ScrollableHandle, ScrollRestorationProviderProps, ScrollRestorationTarget, UseScrollRestorationOptions, } from './types';
3
+ /**
4
+ * Holds the per-route offset map for the subtree. One provider near the app
5
+ * root is enough; the store lives for the document's lifetime so offsets
6
+ * survive navigating away and back (including browser Back/Forward).
7
+ */
8
+ export declare function ScrollRestorationProvider({ children, }: ScrollRestorationProviderProps): import("react/jsx-runtime").JSX.Element;
9
+ /**
10
+ * Preserve and restore the scroll offset of `target` across navigation, keyed
11
+ * by the active route (plus an optional `options.key` for routes that host
12
+ * multiple scrollables).
13
+ *
14
+ * Behaviour (web):
15
+ * - On every scroll while the screen is focused, the current offset is recorded
16
+ * in memory and persisted. This live stream of saves is the source of truth.
17
+ * - On focus, the saved offset is re-applied across a bounded run of animation
18
+ * frames, stopping as soon as the write sticks (the list has re-rendered its
19
+ * rows and grown tall enough) or {@link RESTORE_FRAME_CAP} is reached. A
20
+ * saved offset of 0 is a no-op (nothing to restore).
21
+ * - On blur, the LAST OBSERVED offset is persisted as a final safety net — not
22
+ * a fresh `scrollTop` read, which the navigator may already have forced to 0
23
+ * while collapsing the hidden screen.
24
+ */
25
+ export declare function useScrollRestoration(target: ScrollRestorationTarget, options?: UseScrollRestorationOptions): void;
26
+ //# sourceMappingURL=index.web.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.web.d.ts","sourceRoot":"","sources":["../../../../src/scroll/index.web.tsx"],"names":[],"mappings":"AAuCA,OAAO,KAAK,EACV,8BAA8B,EAC9B,uBAAuB,EACvB,2BAA2B,EAC5B,MAAM,SAAS,CAAC;AAEjB,YAAY,EACV,gBAAgB,EAChB,8BAA8B,EAC9B,uBAAuB,EACvB,2BAA2B,GAC5B,MAAM,SAAS,CAAC;AAmCjB;;;;GAIG;AACH,wBAAgB,yBAAyB,CAAC,EACxC,QAAQ,GACT,EAAE,8BAA8B,2CAOhC;AAYD;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,oBAAoB,CAClC,MAAM,EAAE,uBAAuB,EAC/B,OAAO,CAAC,EAAE,2BAA2B,GACpC,IAAI,CAoGN"}
@@ -0,0 +1,29 @@
1
+ import type { ScrollRestorationTarget } from './types';
2
+ /**
3
+ * A normalized read/write interface over whatever the caller registered, so
4
+ * the hook does not branch on target shape. All methods are safe no-ops when
5
+ * the underlying element is not yet (or no longer) attached.
6
+ */
7
+ export interface ResolvedScroller {
8
+ getOffset: () => number;
9
+ setOffset: (offset: number) => void;
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`).
13
+ *
14
+ * React Navigation's web stack collapses a hidden background screen so its
15
+ * content height drops to the viewport height; while collapsed the container
16
+ * cannot be scrolled and its `scrollTop` is forced to 0. The hook uses this
17
+ * 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
+ */
21
+ canScroll: () => boolean;
22
+ }
23
+ /**
24
+ * Build a {@link ResolvedScroller} for a target. The window sentinel reads and
25
+ * writes the document scroller; everything else operates on the resolved
26
+ * element's `scrollTop`.
27
+ */
28
+ export declare function createScroller(target: ScrollRestorationTarget): ResolvedScroller;
29
+ //# sourceMappingURL=scrollable.web.d.ts.map
@@ -0,0 +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"}
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Pure, platform-agnostic core of the scroll-restoration primitive.
3
+ *
4
+ * This file deliberately contains NO React and NO DOM/native imports so the
5
+ * logic can be unit-tested in isolation and shared verbatim by the web and
6
+ * native barrels. The web barrel drives it with real scroll offsets; the
7
+ * native barrel never instantiates it (native-stack already restores scroll).
8
+ */
9
+ /**
10
+ * Derive the storage key for a scrollable from its owning route key and an
11
+ * optional caller sub-key.
12
+ *
13
+ * - `routeKey` is React Navigation's stable per-route `route.key`.
14
+ * - `subKey` distinguishes multiple scrollables that share a single route.
15
+ *
16
+ * Returns `null` when there is no route key to anchor against (the scrollable
17
+ * is not inside a navigator), which the caller treats as "do not persist".
18
+ */
19
+ export declare function deriveScrollKey(routeKey: string | undefined, subKey?: string): string | null;
20
+ /**
21
+ * In-memory map of `scrollKey -> last-known scroll offset`.
22
+ *
23
+ * Mirrors the semantics of Bluesky's `Map<screenKey, scrollY>`: offsets live
24
+ * only for the lifetime of the document/session. We never persist them — a
25
+ * full reload should start at the top — and we never auto-evict, because a
26
+ * route can be revisited via browser Forward/Back long after it blurred.
27
+ */
28
+ export declare class ScrollOffsetStore {
29
+ private readonly offsets;
30
+ /** Persist the offset for a key. A negative offset is clamped to 0. */
31
+ save(key: string, offset: number): void;
32
+ /**
33
+ * Read the saved offset for a key. Returns `0` when nothing was saved, so
34
+ * callers can restore unconditionally (an unseen list restores to the top).
35
+ */
36
+ read(key: string): number;
37
+ /** Whether an offset was ever saved for this key. */
38
+ has(key: string): boolean;
39
+ /** Drop a saved offset (e.g. when a route is permanently removed). */
40
+ forget(key: string): void;
41
+ /** Clear every saved offset. Primarily for tests and full resets. */
42
+ clear(): void;
43
+ /** Number of distinct keys currently tracked. */
44
+ get size(): number;
45
+ }
46
+ //# sourceMappingURL=store.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"store.d.ts","sourceRoot":"","sources":["../../../../src/scroll/store.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAcH;;;;;;;;;GASG;AACH,wBAAgB,eAAe,CAC7B,QAAQ,EAAE,MAAM,GAAG,SAAS,EAC5B,MAAM,CAAC,EAAE,MAAM,GACd,MAAM,GAAG,IAAI,CAIf;AAED;;;;;;;GAOG;AACH,qBAAa,iBAAiB;IAC5B,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA6B;IAErD,uEAAuE;IACvE,IAAI,CAAC,GAAG,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,IAAI;IAIvC;;;OAGG;IACH,IAAI,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM;IAIzB,qDAAqD;IACrD,GAAG,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO;IAIzB,sEAAsE;IACtE,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI;IAIzB,qEAAqE;IACrE,KAAK,IAAI,IAAI;IAIb,iDAAiD;IACjD,IAAI,IAAI,IAAI,MAAM,CAEjB;CACF"}
@@ -0,0 +1,46 @@
1
+ import type { ReactNode, RefObject } from 'react';
2
+ /**
3
+ * Anything `useScrollRestoration` knows how to read/write a vertical scroll
4
+ * offset from. The hook accepts a ref to one of these:
5
+ *
6
+ * - a React Native `ScrollView` / `FlatList` ref (which on web is backed by a
7
+ * DOM node and exposes `getScrollableNode()`),
8
+ * - a raw DOM element ref (when a component renders its own scroll container),
9
+ * - the literal `'window'` sentinel, for the rare layout where the list IS the
10
+ * window scroller (matches Bluesky's default).
11
+ *
12
+ * Native never reads any of these — the native hook is a no-op — so the type is
13
+ * intentionally permissive rather than coupled to a specific RN class.
14
+ */
15
+ export interface ScrollableHandle {
16
+ /** Imperative scroll API exposed by RN scrollables. */
17
+ scrollTo?: (options: {
18
+ x?: number;
19
+ y?: number;
20
+ animated?: boolean;
21
+ }) => void;
22
+ /** Web/RNW path: returns the underlying DOM node. */
23
+ getScrollableNode?: () => unknown;
24
+ }
25
+ /**
26
+ * The accepted ref shapes. `'window'` is a sentinel for the document scroller.
27
+ */
28
+ export type ScrollRestorationTarget = RefObject<ScrollableHandle | null> | RefObject<unknown> | 'window';
29
+ export interface UseScrollRestorationOptions {
30
+ /**
31
+ * Sub-key to disambiguate multiple scrollables that live on the same route
32
+ * (e.g. the tabs of a profile screen). Combined with the active route key to
33
+ * form the storage key. Omit when a route has a single scrollable.
34
+ */
35
+ key?: string;
36
+ /**
37
+ * When `false`, the hook is inert (saves and restores are skipped). Useful to
38
+ * gate restoration behind a feature flag without changing call sites.
39
+ * Defaults to `true`.
40
+ */
41
+ enabled?: boolean;
42
+ }
43
+ export interface ScrollRestorationProviderProps {
44
+ children: ReactNode;
45
+ }
46
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../../../src/scroll/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,SAAS,EAAE,SAAS,EAAE,MAAM,OAAO,CAAC;AAElD;;;;;;;;;;;;GAYG;AACH,MAAM,WAAW,gBAAgB;IAC/B,uDAAuD;IACvD,QAAQ,CAAC,EAAE,CAAC,OAAO,EAAE;QAAE,CAAC,CAAC,EAAE,MAAM,CAAC;QAAC,CAAC,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,OAAO,CAAA;KAAE,KAAK,IAAI,CAAC;IAC7E,qDAAqD;IACrD,iBAAiB,CAAC,EAAE,MAAM,OAAO,CAAC;CACnC;AAED;;GAEG;AACH,MAAM,MAAM,uBAAuB,GAC/B,SAAS,CAAC,gBAAgB,GAAG,IAAI,CAAC,GAClC,SAAS,CAAC,OAAO,CAAC,GAClB,QAAQ,CAAC;AAEb,MAAM,WAAW,2BAA2B;IAC1C;;;;OAIG;IACH,GAAG,CAAC,EAAE,MAAM,CAAC;IACb;;;;OAIG;IACH,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,WAAW,8BAA8B;IAC7C,QAAQ,EAAE,SAAS,CAAC;CACrB"}
@@ -0,0 +1,27 @@
1
+ /**
2
+ * Native variant of the scroll-restoration primitive — a deliberate no-op.
3
+ *
4
+ * React Navigation's native-stack keeps every screen mounted while it is in the
5
+ * stack, so a list's scroll position survives a push/pop for free. There is
6
+ * nothing to save or restore. We still ship the full API surface (provider +
7
+ * hook) so consumers write one set of call sites that compile and run on every
8
+ * platform; on native the provider just renders its children and the hook does
9
+ * nothing.
10
+ *
11
+ * Web bundlers select `./index.web` via the `"browser"` export condition in
12
+ * `package.json`; native bundlers fall through to this file.
13
+ */
14
+ import type { ReactElement } from 'react';
15
+ import type { ScrollRestorationProviderProps, ScrollRestorationTarget, UseScrollRestorationOptions } from './types';
16
+ export type { ScrollableHandle, ScrollRestorationProviderProps, ScrollRestorationTarget, UseScrollRestorationOptions, } from './types';
17
+ /**
18
+ * No-op provider. Renders children unchanged — native scroll persistence is
19
+ * handled by the navigator, so no per-route state is kept.
20
+ */
21
+ export declare function ScrollRestorationProvider({ children, }: ScrollRestorationProviderProps): ReactElement;
22
+ /**
23
+ * No-op hook. Accepts the same arguments as the web implementation so call
24
+ * sites are identical across platforms.
25
+ */
26
+ export declare function useScrollRestoration(_target: ScrollRestorationTarget, _options?: UseScrollRestorationOptions): void;
27
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/scroll/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;GAYG;AACH,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,OAAO,CAAC;AAC1C,OAAO,KAAK,EACV,8BAA8B,EAC9B,uBAAuB,EACvB,2BAA2B,EAC5B,MAAM,SAAS,CAAC;AAEjB,YAAY,EACV,gBAAgB,EAChB,8BAA8B,EAC9B,uBAAuB,EACvB,2BAA2B,GAC5B,MAAM,SAAS,CAAC;AAEjB;;;GAGG;AACH,wBAAgB,yBAAyB,CAAC,EACxC,QAAQ,GACT,EAAE,8BAA8B,GAAG,YAAY,CAE/C;AAED;;;GAGG;AACH,wBAAgB,oBAAoB,CAClC,OAAO,EAAE,uBAAuB,EAChC,QAAQ,CAAC,EAAE,2BAA2B,GACrC,IAAI,CAEN"}