@oxyhq/bloom 0.79.0 → 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 (147) hide show
  1. package/README.md +3 -3
  2. package/lib/commonjs/provider/index.js +11 -7
  3. package/lib/commonjs/provider/index.js.map +1 -1
  4. package/lib/commonjs/scroll/context.js +96 -0
  5. package/lib/commonjs/scroll/context.js.map +1 -0
  6. package/lib/commonjs/scroll/expo-router/index.js +133 -0
  7. package/lib/commonjs/scroll/expo-router/index.js.map +1 -0
  8. package/lib/commonjs/scroll/index.js +85 -21
  9. package/lib/commonjs/scroll/index.js.map +1 -1
  10. package/lib/commonjs/scroll/index.web.js +221 -121
  11. package/lib/commonjs/scroll/index.web.js.map +1 -1
  12. package/lib/commonjs/scroll/scrollable.native.js +58 -0
  13. package/lib/commonjs/scroll/scrollable.native.js.map +1 -0
  14. package/lib/commonjs/scroll/scrollable.web.js +9 -3
  15. package/lib/commonjs/scroll/scrollable.web.js.map +1 -1
  16. package/lib/commonjs/scroll/store.js +47 -25
  17. package/lib/commonjs/scroll/store.js.map +1 -1
  18. package/lib/commonjs/tabs/Tabs.js +263 -63
  19. package/lib/commonjs/tabs/Tabs.js.map +1 -1
  20. package/lib/commonjs/tabs/expo-router/RouterTabs.js +215 -0
  21. package/lib/commonjs/tabs/expo-router/RouterTabs.js.map +1 -0
  22. package/lib/commonjs/tabs/expo-router/index.js +13 -0
  23. package/lib/commonjs/tabs/expo-router/index.js.map +1 -0
  24. package/lib/commonjs/theme/color-policy.js +11 -10
  25. package/lib/commonjs/theme/color-policy.js.map +1 -1
  26. package/lib/module/provider/index.js +10 -6
  27. package/lib/module/provider/index.js.map +1 -1
  28. package/lib/module/scroll/context.js +90 -0
  29. package/lib/module/scroll/context.js.map +1 -0
  30. package/lib/module/scroll/expo-router/index.js +128 -0
  31. package/lib/module/scroll/expo-router/index.js.map +1 -0
  32. package/lib/module/scroll/index.js +80 -21
  33. package/lib/module/scroll/index.js.map +1 -1
  34. package/lib/module/scroll/index.web.js +218 -122
  35. package/lib/module/scroll/index.web.js.map +1 -1
  36. package/lib/module/scroll/scrollable.native.js +54 -0
  37. package/lib/module/scroll/scrollable.native.js.map +1 -0
  38. package/lib/module/scroll/scrollable.web.js +9 -3
  39. package/lib/module/scroll/scrollable.web.js.map +1 -1
  40. package/lib/module/scroll/store.js +47 -25
  41. package/lib/module/scroll/store.js.map +1 -1
  42. package/lib/module/tabs/Tabs.js +271 -65
  43. package/lib/module/tabs/Tabs.js.map +1 -1
  44. package/lib/module/tabs/expo-router/RouterTabs.js +210 -0
  45. package/lib/module/tabs/expo-router/RouterTabs.js.map +1 -0
  46. package/lib/module/tabs/expo-router/index.js +10 -0
  47. package/lib/module/tabs/expo-router/index.js.map +1 -0
  48. package/lib/module/theme/color-policy.js +11 -10
  49. package/lib/module/theme/color-policy.js.map +1 -1
  50. package/lib/typescript/commonjs/provider/index.d.ts +7 -5
  51. package/lib/typescript/commonjs/provider/index.d.ts.map +1 -1
  52. package/lib/typescript/commonjs/scroll/context.d.ts +14 -0
  53. package/lib/typescript/commonjs/scroll/context.d.ts.map +1 -0
  54. package/lib/typescript/commonjs/scroll/expo-router/index.d.ts +7 -0
  55. package/lib/typescript/commonjs/scroll/expo-router/index.d.ts.map +1 -0
  56. package/lib/typescript/commonjs/scroll/index.d.ts +14 -23
  57. package/lib/typescript/commonjs/scroll/index.d.ts.map +1 -1
  58. package/lib/typescript/commonjs/scroll/index.web.d.ts +17 -19
  59. package/lib/typescript/commonjs/scroll/index.web.d.ts.map +1 -1
  60. package/lib/typescript/commonjs/scroll/scrollable.native.d.ts +23 -0
  61. package/lib/typescript/commonjs/scroll/scrollable.native.d.ts.map +1 -0
  62. package/lib/typescript/commonjs/scroll/scrollable.web.d.ts +18 -5
  63. package/lib/typescript/commonjs/scroll/scrollable.web.d.ts.map +1 -1
  64. package/lib/typescript/commonjs/scroll/store.d.ts +41 -22
  65. package/lib/typescript/commonjs/scroll/store.d.ts.map +1 -1
  66. package/lib/typescript/commonjs/scroll/types.d.ts +82 -12
  67. package/lib/typescript/commonjs/scroll/types.d.ts.map +1 -1
  68. package/lib/typescript/commonjs/tabs/Tabs.d.ts +41 -1
  69. package/lib/typescript/commonjs/tabs/Tabs.d.ts.map +1 -1
  70. package/lib/typescript/commonjs/tabs/expo-router/RouterTabs.d.ts +80 -0
  71. package/lib/typescript/commonjs/tabs/expo-router/RouterTabs.d.ts.map +1 -0
  72. package/lib/typescript/commonjs/tabs/expo-router/index.d.ts +9 -0
  73. package/lib/typescript/commonjs/tabs/expo-router/index.d.ts.map +1 -0
  74. package/lib/typescript/commonjs/tabs/types.d.ts +48 -4
  75. package/lib/typescript/commonjs/tabs/types.d.ts.map +1 -1
  76. package/lib/typescript/commonjs/theme/color-policy.d.ts.map +1 -1
  77. package/lib/typescript/module/provider/index.d.ts +7 -5
  78. package/lib/typescript/module/provider/index.d.ts.map +1 -1
  79. package/lib/typescript/module/scroll/context.d.ts +14 -0
  80. package/lib/typescript/module/scroll/context.d.ts.map +1 -0
  81. package/lib/typescript/module/scroll/expo-router/index.d.ts +7 -0
  82. package/lib/typescript/module/scroll/expo-router/index.d.ts.map +1 -0
  83. package/lib/typescript/module/scroll/index.d.ts +14 -23
  84. package/lib/typescript/module/scroll/index.d.ts.map +1 -1
  85. package/lib/typescript/module/scroll/index.web.d.ts +17 -19
  86. package/lib/typescript/module/scroll/index.web.d.ts.map +1 -1
  87. package/lib/typescript/module/scroll/scrollable.native.d.ts +23 -0
  88. package/lib/typescript/module/scroll/scrollable.native.d.ts.map +1 -0
  89. package/lib/typescript/module/scroll/scrollable.web.d.ts +18 -5
  90. package/lib/typescript/module/scroll/scrollable.web.d.ts.map +1 -1
  91. package/lib/typescript/module/scroll/store.d.ts +41 -22
  92. package/lib/typescript/module/scroll/store.d.ts.map +1 -1
  93. package/lib/typescript/module/scroll/types.d.ts +82 -12
  94. package/lib/typescript/module/scroll/types.d.ts.map +1 -1
  95. package/lib/typescript/module/tabs/Tabs.d.ts +41 -1
  96. package/lib/typescript/module/tabs/Tabs.d.ts.map +1 -1
  97. package/lib/typescript/module/tabs/expo-router/RouterTabs.d.ts +80 -0
  98. package/lib/typescript/module/tabs/expo-router/RouterTabs.d.ts.map +1 -0
  99. package/lib/typescript/module/tabs/expo-router/index.d.ts +9 -0
  100. package/lib/typescript/module/tabs/expo-router/index.d.ts.map +1 -0
  101. package/lib/typescript/module/tabs/types.d.ts +48 -4
  102. package/lib/typescript/module/tabs/types.d.ts.map +1 -1
  103. package/lib/typescript/module/theme/color-policy.d.ts.map +1 -1
  104. package/package.json +29 -1
  105. package/src/__tests__/BloomProvider.web.test.tsx +8 -15
  106. package/src/__tests__/Tabs.test.tsx +187 -0
  107. package/src/__tests__/optional-peer-imports.test.ts +7 -2
  108. package/src/__tests__/scroll-expo-router-adapter.test.tsx +164 -0
  109. package/src/__tests__/scroll-native.test.tsx +465 -0
  110. package/src/__tests__/scroll-store.test.ts +41 -17
  111. package/src/__tests__/scroll-web.test.tsx +797 -108
  112. package/src/provider/index.tsx +10 -7
  113. package/src/scroll/context.tsx +107 -0
  114. package/src/scroll/expo-router/index.ts +140 -0
  115. package/src/scroll/index.ts +92 -25
  116. package/src/scroll/index.web.tsx +255 -162
  117. package/src/scroll/scrollable.native.ts +64 -0
  118. package/src/scroll/scrollable.web.ts +30 -8
  119. package/src/scroll/store.ts +47 -25
  120. package/src/scroll/types.ts +82 -12
  121. package/src/tabs/Tabs.tsx +352 -77
  122. package/src/tabs/expo-router/RouterTabs.tsx +268 -0
  123. package/src/tabs/expo-router/index.ts +8 -0
  124. package/src/tabs/types.ts +48 -4
  125. package/src/theme/__tests__/__fixtures__/golden-resolved-tokens.json +5 -5
  126. package/src/theme/__tests__/__snapshots__/visual-gallery.test.tsx.snap +11 -11
  127. package/src/theme/__tests__/policy-legibility.test.ts +10 -7
  128. package/src/theme/color-policy.ts +11 -10
  129. package/lib/commonjs/provider/scroll-provider.js +0 -13
  130. package/lib/commonjs/provider/scroll-provider.js.map +0 -1
  131. package/lib/commonjs/provider/scroll-provider.web.js +0 -13
  132. package/lib/commonjs/provider/scroll-provider.web.js.map +0 -1
  133. package/lib/module/provider/scroll-provider.js +0 -16
  134. package/lib/module/provider/scroll-provider.js.map +0 -1
  135. package/lib/module/provider/scroll-provider.web.js +0 -5
  136. package/lib/module/provider/scroll-provider.web.js.map +0 -1
  137. package/lib/typescript/commonjs/provider/scroll-provider.d.ts +0 -14
  138. package/lib/typescript/commonjs/provider/scroll-provider.d.ts.map +0 -1
  139. package/lib/typescript/commonjs/provider/scroll-provider.web.d.ts +0 -3
  140. package/lib/typescript/commonjs/provider/scroll-provider.web.d.ts.map +0 -1
  141. package/lib/typescript/module/provider/scroll-provider.d.ts +0 -14
  142. package/lib/typescript/module/provider/scroll-provider.d.ts.map +0 -1
  143. package/lib/typescript/module/provider/scroll-provider.web.d.ts +0 -3
  144. package/lib/typescript/module/provider/scroll-provider.web.d.ts.map +0 -1
  145. package/src/__tests__/scroll-native.test.ts +0 -25
  146. package/src/provider/scroll-provider.ts +0 -13
  147. package/src/provider/scroll-provider.web.ts +0 -2
@@ -3,32 +3,36 @@
3
3
  Object.defineProperty(exports, "__esModule", {
4
4
  value: true
5
5
  });
6
- exports.ScrollRestorationProvider = ScrollRestorationProvider;
6
+ Object.defineProperty(exports, "ScrollRestorationProvider", {
7
+ enumerable: true,
8
+ get: function () {
9
+ return _context.ScrollRestorationProvider;
10
+ }
11
+ });
7
12
  exports.useScrollRestoration = useScrollRestoration;
8
13
  var _react = require("react");
9
- var _expoRouter = require("expo-router");
14
+ var _context = require("./context.js");
10
15
  var _scrollableWeb = require("./scrollable.web.js");
11
16
  var _store = require("./store.js");
12
- var _jsxRuntime = require("react/jsx-runtime");
13
17
  /**
14
18
  * Web variant of the scroll-restoration primitive.
15
19
  *
16
- * Mirrors the proven Bluesky pattern (`history.scrollRestoration = 'manual'`
17
- * plus an in-memory `Map<routeKey, offset>`) with two deliberate differences
18
- * forced by Oxy's layouts and the behaviour of the (expo-router-wrapped)
19
- * React Navigation web stack:
20
+ * Mirrors the proven Bluesky pattern (manual `history.scrollRestoration` plus
21
+ * an in-memory `Map<key, offset>`) with three deliberate differences forced by
22
+ * Oxy's layouts and by the behaviour of the (expo-router-wrapped) React
23
+ * Navigation web stack:
20
24
  *
21
25
  * 1. Bluesky restores the WINDOW scroller, whereas Oxy apps keep multi-column
22
26
  * layouts whose feed scrolls an INNER container. So we restore the offset
23
27
  * of a caller-registered scrollable (a ref to an element / RN scroll
24
- * component, or the `'window'` sentinel), keyed by the active route.
28
+ * component, or the `'window'` sentinel).
25
29
  *
26
- * 2. The web stack HIDES the background screen on push. While
27
- * hidden, the previous screen's scroll container collapses
28
- * (`scrollHeight === clientHeight`) and the navigator forces its
29
- * `scrollTop` to 0. The screen is NOT unmounted, so a virtualized list
30
- * (e.g. FlashList) keeps its rows but re-lays them out over SEVERAL frames
31
- * once the screen is re-shown. Two problems follow, both handled here:
30
+ * 2. The web stack HIDES the background screen on push. While hidden, the
31
+ * previous screen's scroll container collapses (`scrollHeight ===
32
+ * clientHeight`) and the navigator forces its `scrollTop` to 0. The screen
33
+ * is NOT unmounted, so a virtualized list (e.g. FlashList) keeps its rows
34
+ * but re-lays them out over SEVERAL frames once the screen is re-shown. Two
35
+ * problems follow, both handled here:
32
36
  *
33
37
  * (a) A blur-time read of `scrollTop` returns the navigator's forced 0,
34
38
  * not the user's real offset — saving it would clobber the good
@@ -42,28 +46,22 @@ var _jsxRuntime = require("react/jsx-runtime");
42
46
  * sticks (the content has grown tall enough) or a small frame cap is
43
47
  * reached.
44
48
  *
45
- * Native bundlers use `./index.ts` (a no-op); web bundlers select this file via
46
- * the `"browser"` export condition in `package.json`.
49
+ * 3. A document-scrolled app (one window scroller shared by every route) has
50
+ * no per-screen container to reset, so an unrecognised key MUST be written
51
+ * to 0 explicitly. Doing nothing leaves the previous screen's offset in
52
+ * place and the new screen opens mid-page.
47
53
  *
48
- * The navigation hooks are imported from `expo-router` (which re-exports
49
- * `useFocusEffect` and `useRoute` from its bundled React Navigation core) rather
50
- * than from `@react-navigation/native` directly. Every Oxy app uses expo-router
51
- * as its router, so it is always a DIRECT, top-level dependency that resolves
52
- * cleanly under Bun's isolated linker — whereas `@react-navigation/native` is
53
- * only a nested/transitive dependency of expo-router and would fail to resolve
54
- * when bundling those apps.
54
+ * Native bundlers use `./index.ts`; web bundlers select this file via the
55
+ * `"browser"` export condition in `package.json`.
55
56
  */
56
57
 
57
- const ScrollOffsetContext = /*#__PURE__*/(0, _react.createContext)(null);
58
- ScrollOffsetContext.displayName = 'BloomScrollOffsetContext';
59
-
60
58
  /**
61
- * Maximum number of animation frames the focus restore will re-apply the saved
62
- * offset before giving up. A virtualized list re-lays out its rows over a
63
- * handful of frames after its screen is re-shown; ~30 frames (≈0.5s at 60fps)
64
- * is comfortably longer than any observed relayout while staying short enough
65
- * that the loop never lingers as a perceptible cost. The loop normally exits
66
- * far earlier — as soon as the write sticks.
59
+ * Maximum number of animation frames the restore will re-apply the saved offset
60
+ * before giving up. A virtualized list re-lays out its rows over a handful of
61
+ * frames after its screen is re-shown; ~30 frames (≈0.5s at 60fps) is
62
+ * comfortably longer than any observed relayout while staying short enough that
63
+ * the loop never lingers as a perceptible cost. The loop normally exits far
64
+ * earlier — as soon as the write sticks.
67
65
  */
68
66
  const RESTORE_FRAME_CAP = 30;
69
67
 
@@ -77,141 +75,243 @@ const RESTORE_FRAME_CAP = 30;
77
75
  const RESTORE_STICK_TOLERANCE_PX = 2;
78
76
 
79
77
  /**
80
- * Switch the browser to manual scroll restoration exactly once per document.
81
- *
82
- * The browser's default `'auto'` restoration fights our manual restore on
83
- * Back/Forward navigations. Doing this at module scope (guarded for SSR) means
84
- * it is set before any provider mounts, matching Bluesky's module-level call.
78
+ * The web binding is a constant: this platform observes offsets by subscribing
79
+ * to the resolved DOM node's own `scroll` event, so it needs nothing from the
80
+ * caller. It is still returned and still safe to spread onto a list — so a
81
+ * call site is written once and runs on both platforms.
85
82
  */
86
- if (typeof history !== 'undefined' && 'scrollRestoration' in history) {
87
- history.scrollRestoration = 'manual';
88
- }
83
+ const WEB_BINDING = {
84
+ onScroll: () => undefined
85
+ };
89
86
 
90
87
  /**
91
- * Holds the per-route offset map for the subtree. One provider near the app
92
- * root is enough; the store lives for the document's lifetime so offsets
93
- * survive navigating away and back (including browser Back/Forward).
88
+ * Events that mean the USER has taken over the scroller, and the restore must
89
+ * stop trying to put them back.
90
+ *
91
+ * The browser's own restoration is specified the same way: HTML §7.4.6.5 has
92
+ * the UA re-attempt the saved position and "may continue to attempt to do so
93
+ * periodically, until document's has been scrolled by the user becomes true".
94
+ * Without this the loop fights anyone who starts scrolling inside the frame
95
+ * budget, yanking them back once per frame for up to {@link RESTORE_FRAME_CAP}
96
+ * frames.
97
+ *
98
+ * Two designs were rejected, and the reasons are the point:
99
+ *
100
+ * - **"Any `scroll` event aborts"** — the obvious version, and it aborts on the
101
+ * FIRST frame every time: `setOffset` is itself a scroll, so the loop would
102
+ * cancel on the event its own write produced and no restore would ever
103
+ * complete.
104
+ * - **"Abort on an offset we did not write"** — tempting because it needs no
105
+ * listeners, but a virtualized list inserting rows ABOVE the viewport makes
106
+ * the browser's scroll anchoring move `scrollTop` on its own. That is
107
+ * precisely the situation the loop exists for, so the unexplained-delta test
108
+ * would abort exactly when it must not.
109
+ *
110
+ * What is being detected is user INPUT, so this detects user input. `pointerdown`
111
+ * covers a scrollbar drag and a touch-less press; `keydown` covers arrow/page
112
+ * keys, and reaches us whenever focus is inside the scroller it would scroll.
94
113
  */
95
- function ScrollRestorationProvider({
96
- children
97
- }) {
98
- const store = (0, _react.useMemo)(() => new _store.ScrollOffsetStore(), []);
99
- return /*#__PURE__*/(0, _jsxRuntime.jsx)(ScrollOffsetContext.Provider, {
100
- value: store,
101
- children: children
102
- });
103
- }
104
- function useScrollOffsetStore() {
105
- const store = (0, _react.useContext)(ScrollOffsetContext);
106
- if (store === null) {
107
- throw new Error('useScrollRestoration must be used within a <ScrollRestorationProvider>.');
108
- }
109
- return store;
110
- }
114
+ const USER_TAKEOVER_EVENTS = ['wheel', 'touchstart', 'pointerdown', 'keydown'];
111
115
 
112
116
  /**
113
117
  * Preserve and restore the scroll offset of `target` across navigation, keyed
114
- * by the active route (plus an optional `options.key` for routes that host
115
- * multiple scrollables).
118
+ * by the content its screen is showing plus an optional `options.key`.
116
119
  *
117
120
  * Behaviour (web):
118
121
  * - On every scroll while the screen is focused, the current offset is recorded
119
- * in memory and persisted. This live stream of saves is the source of truth.
120
- * - On focus, the saved offset is re-applied across a bounded run of animation
121
- * frames, stopping as soon as the write sticks (the list has re-rendered its
122
- * rows and grown tall enough) or {@link RESTORE_FRAME_CAP} is reached. A
123
- * saved offset of 0 is a no-op (nothing to restore).
124
- * - On blur, the LAST OBSERVED offset is persisted as a final safety net — not
125
- * a fresh `scrollTop` read, which the navigator may already have forced to 0
126
- * while collapsing the hidden screen.
122
+ * under the key that was live when the listener was attached. This stream of
123
+ * saves is the source of truth.
124
+ * - On focus and again whenever the key changes while the screen STAYS
125
+ * focused, which is how an in-screen tab or folder swap is seen at all — the
126
+ * offset saved for the new key is re-applied across a bounded run of
127
+ * animation frames, stopping as soon as the write sticks or
128
+ * {@link RESTORE_FRAME_CAP} is reached. Content never seen this session is
129
+ * written to 0 once instead; a write to 0 always sticks, so it needs no loop.
130
+ * - On blur (and on a key change), the LAST OBSERVED offset is persisted under
131
+ * the OUTGOING key as a final safety net — not a fresh `scrollTop` read,
132
+ * which the navigator may already have forced to 0 while collapsing the
133
+ * hidden screen.
127
134
  */
128
135
  function useScrollRestoration(target, options) {
129
- const store = useScrollOffsetStore();
130
- const route = (0, _expoRouter.useRoute)();
131
- const subKey = options?.key;
136
+ const {
137
+ store,
138
+ adapter
139
+ } = (0, _context.useScrollRestorationContext)();
140
+ const contentId = adapter.useScreenContentId();
132
141
  const enabled = options?.enabled ?? true;
133
- const scrollKey = (0, _store.deriveScrollKey)(route.key, subKey);
142
+ const scrollKey = (0, _store.deriveScrollKey)(contentId, options?.key);
134
143
 
135
- // Keep the latest target/enabled/key in refs so the focus effect can read
136
- // them without being re-subscribed on every render.
137
- const targetRef = (0, _react.useRef)(target);
138
- targetRef.current = target;
139
- const enabledRef = (0, _react.useRef)(enabled);
140
- enabledRef.current = enabled;
141
- const scrollKeyRef = (0, _react.useRef)(scrollKey);
142
- scrollKeyRef.current = scrollKey;
143
- (0, _expoRouter.useFocusEffect)(
144
- // The effect identity is intentionally stable across renders: it reads all
145
- // varying inputs from refs. expo-router's `useFocusEffect` re-runs it on
146
- // each focus.
144
+ // The key this hook instance has already sent to the top. Writing 0 is a
145
+ // one-time ARRIVAL action: a later re-run for the SAME key (`enabled` turning
146
+ // on once the rows exist, a screen regaining focus) must not repeat it,
147
+ // because by then the user may have scrolled somewhere the hook never got to
148
+ // record. Restoring twice is harmless; resetting twice is data loss.
149
+ const resetKeyRef = (0, _react.useRef)(null);
150
+ adapter.useScreenFocusEffect(
151
+ // Every varying input is a dependency rather than a ref read, so the
152
+ // session below CLOSES OVER the key it belongs to. That is what makes a
153
+ // key change mid-focus correct in both directions: the outgoing session's
154
+ // cleanup persists to the old key, and the incoming one restores the new.
147
155
  (0, _react.useCallback)(() => {
148
- const key = scrollKeyRef.current;
149
- if (!enabledRef.current || key === null) return undefined;
150
- const scroller = (0, _scrollableWeb.createScroller)(targetRef.current);
151
- const element = targetRef.current === 'window' ? typeof window !== 'undefined' ? window : null : resolveScrollEventTarget(targetRef.current);
156
+ if (!enabled || scrollKey === null) return undefined;
157
+ const scroller = (0, _scrollableWeb.createScroller)(target);
158
+ const element = target === 'window' ? typeof window === 'undefined' ? null : window : resolveScrollEventTarget(target);
152
159
 
153
- // The last offset the live scroll listener observed for this focus
154
- // session. This — not a blur-time `getOffset()` — is what we persist on
155
- // blur, because by blur time the navigator may have collapsed the
156
- // hidden screen and forced its `scrollTop` to 0 (bug A). `null` means
157
- // the user never scrolled this session, so there is nothing newer to
158
- // persist than what the scroll listener already saved live.
160
+ // The last offset the live scroll listener observed for this session.
161
+ // This — not a cleanup-time `getOffset()` — is what we persist at the
162
+ // end, because by then the navigator may have collapsed the hidden
163
+ // screen and forced its `scrollTop` to 0 (problem A). `null` means the
164
+ // user never scrolled this session, so there is nothing newer to persist
165
+ // than what the scroll listener already saved live.
159
166
  let lastObservedOffset = null;
167
+
168
+ // The offset WE last wrote, which a real browser dispatches back as a
169
+ // `scroll` event (jsdom does not, so the tests for this emit it
170
+ // explicitly). Both writers arm it: the reset, and every frame of the
171
+ // restore loop. Persisting either records an offset the user never
172
+ // chose — the reset's 0 under a key another LIVE screen may share (the
173
+ // two-live-entries trade content keying makes, see `deriveScrollKey`),
174
+ // and the loop's intermediate value as a PARTIAL that outlives an
175
+ // interrupted restore.
176
+ //
177
+ // A `restoring` FLAG covering the loop's duration is the obvious
178
+ // alternative and is wrong. It could only be cleared by a takeover
179
+ // event, so a session whose scroll came from a source that fires none —
180
+ // `scrollIntoView` from another component, a focus-driven scroll — would
181
+ // suppress saves indefinitely and never record the user's position at
182
+ // all: an unbounded corruption traded for a bounded one. Arming a VALUE
183
+ // is bounded to one event per write and covers every way the loop can
184
+ // end (stuck, capped, aborted), because each frame re-arms.
185
+ let echoOffset = null;
186
+
187
+ // The largest offset reachable when we last PERSISTED one. A browser
188
+ // clamp is recognised against this rather than against the previous
189
+ // OBSERVATION, and that distinction is the whole mechanism: Chrome
190
+ // dispatches TWO `scroll` events for one clamp, so an
191
+ // observation-relative test sees the shrink on the first and nothing on
192
+ // the second, which then saves the clamped value anyway. Measured.
193
+ let maxOffsetAtLastSave = scroller.getMaxOffset();
160
194
  const save = () => {
161
- const currentKey = scrollKeyRef.current;
162
- if (!enabledRef.current || currentKey === null) return;
163
195
  const offset = scroller.getOffset();
196
+ const maxOffset = scroller.getMaxOffset();
164
197
  // Ignore a spurious 0 produced by the navigator collapsing a hidden
165
198
  // background screen: while collapsed the container cannot scroll, so
166
- // its `scrollTop` is forced to 0. Persisting it would clobber the
167
- // good offset recorded by earlier live saves (bug A). A genuine
199
+ // its `scrollTop` is forced to 0. Persisting it would clobber the good
200
+ // offset recorded by earlier live saves (problem A). A genuine
168
201
  // scroll-to-top keeps the container scrollable and is saved normally.
169
- if (offset === 0 && !scroller.canScroll()) return;
202
+ if (offset === 0 && maxOffset <= 0) return;
203
+ // A partial clamp: the reachable range SHRANK and the offset is sitting
204
+ // exactly at the new bottom. That is what the browser does to a tab
205
+ // whose document has been taken over by a shorter sibling — the two
206
+ // together are a signature a genuine scroll does not have, because
207
+ // reaching the bottom on your own does not shrink the page in the same
208
+ // breath. Suppressing keeps the pre-clamp offset, which on return
209
+ // simply clamps to the same place if the content really did shrink.
210
+ if (maxOffset < maxOffsetAtLastSave && Math.abs(offset - maxOffset) <= RESTORE_STICK_TOLERANCE_PX) {
211
+ return;
212
+ }
213
+ if (offset === echoOffset) {
214
+ // Our own write coming back. Consume the arming so a real scroll to
215
+ // the same offset later is saved normally.
216
+ echoOffset = null;
217
+ return;
218
+ }
219
+ echoOffset = null;
170
220
  lastObservedOffset = offset;
171
- store.save(currentKey, offset);
221
+ maxOffsetAtLastSave = maxOffset;
222
+ store.save(scrollKey, offset);
172
223
  };
173
224
 
174
- // Restore across a bounded run of frames. A freshly re-shown
175
- // virtualized list re-lays out its rows over several frames, so a
176
- // single write while it is still collapsed would be clamped to 0 and
177
- // never re-applied (bug B). We re-apply each frame until the write
178
- // sticks or the frame cap is hit.
179
- const targetOffset = store.read(key);
225
+ // Restore and reset are ONE write. A miss reads 0, and 0 is exactly what
226
+ // an unseen screen must be written to left alone it would show the
227
+ // previous screen's offset, since a document-scrolled app shares one
228
+ // scroller across every route.
229
+ const targetOffset = store.read(scrollKey);
230
+
231
+ // They remain two DECISIONS, though, and `read` cannot tell them apart:
232
+ // a key never seen and a key deliberately saved at the top both read 0.
233
+ // Only the first is a RESET, and a reset is a one-time ARRIVAL action
234
+ // gated on `resetKeyRef` — repeating it on a later re-run for the same
235
+ // key would send a user who has since scrolled back to the top, and the
236
+ // hook was inert while they did it so there is nothing to restore them
237
+ // to afterwards. A 0 the user deliberately saved is theirs, and always
238
+ // re-applies. `has` is the only thing that answers this.
239
+ const isReset = !store.has(scrollKey);
240
+ const alreadyReset = isReset && resetKeyRef.current === scrollKey;
241
+ if (isReset) resetKeyRef.current = scrollKey;
180
242
  let rafId = null;
181
- if (targetOffset > 0 && typeof requestAnimationFrame !== 'undefined') {
243
+ const stopRestore = () => {
244
+ if (rafId === null) return;
245
+ cancelAnimationFrame(rafId);
246
+ rafId = null;
247
+ };
248
+ if (alreadyReset) {
249
+ // Nothing to write: this content has already been sent to the top once
250
+ // and the user is wherever they have moved to since.
251
+ } else if (targetOffset === 0) {
252
+ // A write to 0 is never clamped away, so it needs no re-apply loop.
253
+ echoOffset = 0;
254
+ scroller.setOffset(0);
255
+ } else if (typeof requestAnimationFrame !== 'undefined') {
256
+ // Re-apply across a bounded run of frames. A freshly re-shown
257
+ // virtualized list re-lays out its rows over several frames, so a
258
+ // single write while it is still collapsed would be clamped to 0 and
259
+ // never re-applied (problem B).
260
+ //
261
+ // Every write the loop makes is suppressed from the save path (see
262
+ // `echoOffset` below), so an interrupted restore leaves the ORIGINAL
263
+ // target stored rather than a partial. Nothing is lost on the success
264
+ // path: `targetOffset` came out of the store, so persisting it again
265
+ // would write back the same number.
182
266
  let framesLeft = RESTORE_FRAME_CAP;
183
267
  const applyOffset = () => {
184
268
  rafId = null;
185
269
  scroller.setOffset(targetOffset);
186
270
  framesLeft -= 1;
271
+ // `getOffset` re-reads the value the browser actually took, which is
272
+ // clamped to the content height it has reached so far. Arm it as the
273
+ // echo to swallow: this write is about to come back as a `scroll`
274
+ // event, and persisting a PARTIAL offset we wrote ourselves is how a
275
+ // restore that is aborted or capped corrupts the stored position.
276
+ const landed = scroller.getOffset();
277
+ echoOffset = landed;
187
278
  // Stop once the write took effect (content grew tall enough) or we
188
- // exhaust the frame budget. `getOffset` re-reads the clamped value.
189
- const reached = Math.abs(scroller.getOffset() - targetOffset) <= RESTORE_STICK_TOLERANCE_PX;
279
+ // exhaust the frame budget.
280
+ const reached = Math.abs(landed - targetOffset) <= RESTORE_STICK_TOLERANCE_PX;
190
281
  if (!reached && framesLeft > 0) {
191
282
  rafId = requestAnimationFrame(applyOffset);
192
283
  }
193
284
  };
194
285
  rafId = requestAnimationFrame(applyOffset);
286
+ // Only while a loop is actually in flight — the reset path has nothing
287
+ // to abort, and the listeners would be idle on every screen.
288
+ for (const type of USER_TAKEOVER_EVENTS) {
289
+ element?.addEventListener(type, stopRestore, {
290
+ passive: true
291
+ });
292
+ }
195
293
  }
196
294
  element?.addEventListener('scroll', save, {
197
295
  passive: true
198
296
  });
199
297
  return () => {
200
- if (rafId !== null) cancelAnimationFrame(rafId);
298
+ stopRestore();
201
299
  element?.removeEventListener('scroll', save);
202
- // Final capture on blur: persist the last offset the scroll listener
203
- // OBSERVED, never a fresh read (which the navigator may have forced
204
- // to 0 while collapsing the hidden screen). When the user never
205
- // scrolled this session there is nothing newer to persist than the
206
- // live saves already recorded.
300
+ // Removing a listener that was never added is a no-op, so this needs no
301
+ // flag tracking whether the loop branch ran.
302
+ for (const type of USER_TAKEOVER_EVENTS) {
303
+ element?.removeEventListener(type, stopRestore);
304
+ }
305
+ // Final capture: persist the last offset the scroll listener OBSERVED,
306
+ // never a fresh read (which the navigator may have forced to 0 while
307
+ // collapsing the hidden screen). When the user never scrolled this
308
+ // session there is nothing newer than the live saves already recorded.
207
309
  if (lastObservedOffset !== null) {
208
- const currentKey = scrollKeyRef.current;
209
- if (enabledRef.current && currentKey !== null) {
210
- store.save(currentKey, lastObservedOffset);
211
- }
310
+ store.save(scrollKey, lastObservedOffset);
212
311
  }
213
312
  };
214
- }, [store]));
313
+ }, [store, scrollKey, enabled, target]));
314
+ return WEB_BINDING;
215
315
  }
216
316
 
217
317
  /**
@@ -1 +1 @@
1
- {"version":3,"names":["_react","require","_expoRouter","_scrollableWeb","_store","_jsxRuntime","ScrollOffsetContext","createContext","displayName","RESTORE_FRAME_CAP","RESTORE_STICK_TOLERANCE_PX","history","scrollRestoration","ScrollRestorationProvider","children","store","useMemo","ScrollOffsetStore","jsx","Provider","value","useScrollOffsetStore","useContext","Error","useScrollRestoration","target","options","route","useRoute","subKey","key","enabled","scrollKey","deriveScrollKey","targetRef","useRef","current","enabledRef","scrollKeyRef","useFocusEffect","useCallback","undefined","scroller","createScroller","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":[]}
1
+ {"version":3,"names":["_react","require","_context","_scrollableWeb","_store","RESTORE_FRAME_CAP","RESTORE_STICK_TOLERANCE_PX","WEB_BINDING","onScroll","undefined","USER_TAKEOVER_EVENTS","useScrollRestoration","target","options","store","adapter","useScrollRestorationContext","contentId","useScreenContentId","enabled","scrollKey","deriveScrollKey","key","resetKeyRef","useRef","useScreenFocusEffect","useCallback","scroller","createScroller","element","window","resolveScrollEventTarget","lastObservedOffset","echoOffset","maxOffsetAtLastSave","getMaxOffset","save","offset","getOffset","maxOffset","Math","abs","targetOffset","read","isReset","has","alreadyReset","current","rafId","stopRestore","cancelAnimationFrame","setOffset","requestAnimationFrame","framesLeft","applyOffset","landed","reached","type","addEventListener","passive","removeEventListener","EventTarget","handle","getScrollableNode","node"],"sourceRoot":"../../../src","sources":["scroll/index.web.tsx"],"mappings":";;;;;;;;;;;;AAwCA,IAAAA,MAAA,GAAAC,OAAA;AAEA,IAAAC,QAAA,GAAAD,OAAA;AACA,IAAAE,cAAA,GAAAF,OAAA;AACA,IAAAG,MAAA,GAAAH,OAAA;AA5CA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;;AAwBA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,MAAMI,iBAAiB,GAAG,EAAE;;AAE5B;AACA;AACA;AACA;AACA;AACA;AACA;AACA,MAAMC,0BAA0B,GAAG,CAAC;;AAEpC;AACA;AACA;AACA;AACA;AACA;AACA,MAAMC,WAAqC,GAAG;EAAEC,QAAQ,EAAEA,CAAA,KAAMC;AAAU,CAAC;;AAE3E;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA,MAAMC,oBAAoB,GAAG,CAAC,OAAO,EAAE,YAAY,EAAE,aAAa,EAAE,SAAS,CAAC;;AAE9E;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,SAASC,oBAAoBA,CAClCC,MAA+B,EAC/BC,OAAqC,EACX;EAC1B,MAAM;IAAEC,KAAK;IAAEC;EAAQ,CAAC,GAAG,IAAAC,oCAA2B,EAAC,CAAC;EACxD,MAAMC,SAAS,GAAGF,OAAO,CAACG,kBAAkB,CAAC,CAAC;EAC9C,MAAMC,OAAO,GAAGN,OAAO,EAAEM,OAAO,IAAI,IAAI;EACxC,MAAMC,SAAS,GAAG,IAAAC,sBAAe,EAACJ,SAAS,EAAEJ,OAAO,EAAES,GAAG,CAAC;;EAE1D;EACA;EACA;EACA;EACA;EACA,MAAMC,WAAW,GAAG,IAAAC,aAAM,EA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","ignoreList":[]}
@@ -0,0 +1,58 @@
1
+ "use strict";
2
+
3
+ Object.defineProperty(exports, "__esModule", {
4
+ value: true
5
+ });
6
+ exports.createScroller = createScroller;
7
+ /**
8
+ * The write half of a scrollable, on native. There is no read half: native
9
+ * offers no way to sample a list's offset from outside it, so offsets arrive
10
+ * through the `onScroll` binding the hook returns instead.
11
+ */
12
+
13
+ function hasScrollToOffset(value) {
14
+ return typeof value === 'object' && value !== null && typeof value.scrollToOffset === 'function';
15
+ }
16
+ function hasScrollTo(value) {
17
+ return typeof value === 'object' && value !== null && typeof value.scrollTo === 'function';
18
+ }
19
+
20
+ /**
21
+ * Build a {@link NativeScroller} for a target.
22
+ *
23
+ * `scrollToOffset` (`FlatList`, `FlashList`) is preferred over `scrollTo`
24
+ * (`ScrollView`) because a list exposes both shapes on some versions and only
25
+ * the former accounts for the list's own header. The `'window'` sentinel has no
26
+ * native meaning — there is no document scroller — so it resolves to a no-op
27
+ * rather than an error: a call site shared with web legitimately passes it.
28
+ *
29
+ * The handle is re-resolved on every write; it is never cached, because a ref
30
+ * can be swapped or detached between the schedule and the write.
31
+ */
32
+ function createScroller(target) {
33
+ if (target === 'window') {
34
+ return {
35
+ setOffset: () => undefined
36
+ };
37
+ }
38
+ return {
39
+ setOffset: offset => {
40
+ const current = target.current;
41
+ if (current == null) return;
42
+ if (hasScrollToOffset(current)) {
43
+ current.scrollToOffset({
44
+ offset,
45
+ animated: false
46
+ });
47
+ return;
48
+ }
49
+ if (hasScrollTo(current)) {
50
+ current.scrollTo({
51
+ y: offset,
52
+ animated: false
53
+ });
54
+ }
55
+ }
56
+ };
57
+ }
58
+ //# sourceMappingURL=scrollable.native.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"names":["hasScrollToOffset","value","scrollToOffset","hasScrollTo","scrollTo","createScroller","target","setOffset","undefined","offset","current","animated","y"],"sourceRoot":"../../../src","sources":["scroll/scrollable.native.ts"],"mappings":";;;;;;AAEA;AACA;AACA;AACA;AACA;;AAKA,SAASA,iBAAiBA,CACxBC,KAAc,EAC+C;EAC7D,OACE,OAAOA,KAAK,KAAK,QAAQ,IACzBA,KAAK,KAAK,IAAI,IACd,OAAQA,KAAK,CAAsBC,cAAc,KAAK,UAAU;AAEpE;AAEA,SAASC,WAAWA,CAClBF,KAAc,EACyC;EACvD,OACE,OAAOA,KAAK,KAAK,QAAQ,IACzBA,KAAK,KAAK,IAAI,IACd,OAAQA,KAAK,CAAsBG,QAAQ,KAAK,UAAU;AAE9D;;AAEA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACA;AACO,SAASC,cAAcA,CAC5BC,MAA+B,EACf;EAChB,IAAIA,MAAM,KAAK,QAAQ,EAAE;IACvB,OAAO;MAAEC,SAAS,EAAEA,CAAA,KAAMC;IAAU,CAAC;EACvC;EAEA,OAAO;IACLD,SAAS,EAAGE,MAAM,IAAK;MACrB,MAAMC,OAAO,GAAGJ,MAAM,CAACI,OAAO;MAC9B,IAAIA,OAAO,IAAI,IAAI,EAAE;MACrB,IAAIV,iBAAiB,CAACU,OAAO,CAAC,EAAE;QAC9BA,OAAO,CAACR,cAAc,CAAC;UAAEO,MAAM;UAAEE,QAAQ,EAAE;QAAM,CAAC,CAAC;QACnD;MACF;MACA,IAAIR,WAAW,CAACO,OAAO,CAAC,EAAE;QACxBA,OAAO,CAACN,QAAQ,CAAC;UAAEQ,CAAC,EAAEH,MAAM;UAAEE,QAAQ,EAAE;QAAM,CAAC,CAAC;MAClD;IACF;EACF,CAAC;AACH","ignoreList":[]}
@@ -48,7 +48,12 @@ function createScroller(target) {
48
48
  setOffset: offset => {
49
49
  if (typeof window !== 'undefined') window.scrollTo(0, offset);
50
50
  },
51
- canScroll: () => true
51
+ getMaxOffset: () => {
52
+ if (typeof window === 'undefined' || typeof document === 'undefined') {
53
+ return 0;
54
+ }
55
+ return Math.max(0, document.documentElement.scrollHeight - window.innerHeight);
56
+ }
52
57
  };
53
58
  }
54
59
  return {
@@ -60,9 +65,10 @@ function createScroller(target) {
60
65
  const element = resolveElement(target);
61
66
  if (element) element.scrollTop = offset;
62
67
  },
63
- canScroll: () => {
68
+ getMaxOffset: () => {
64
69
  const element = resolveElement(target);
65
- return element ? element.scrollHeight > element.clientHeight : false;
70
+ if (!element) return 0;
71
+ return Math.max(0, element.scrollHeight - element.clientHeight);
66
72
  }
67
73
  };
68
74
  }
@@ -1 +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;AACO,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":[]}
1
+ {"version":3,"names":["isElement","value","HTMLElement","hasGetScrollableNode","getScrollableNode","resolveElement","target","current","node","createScroller","getOffset","window","scrollY","setOffset","offset","scrollTo","getMaxOffset","document","Math","max","documentElement","scrollHeight","innerHeight","element","scrollTop","clientHeight"],"sourceRoot":"../../../src","sources":["scroll/scrollable.web.ts"],"mappings":";;;;;;AAEA;AACA;AACA;AACA;AACA;;AA+BA,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;AACO,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,YAAY,EAAEA,CAAA,KAAM;QAClB,IAAI,OAAOL,MAAM,KAAK,WAAW,IAAI,OAAOM,QAAQ,KAAK,WAAW,EAAE;UACpE,OAAO,CAAC;QACV;QACA,OAAOC,IAAI,CAACC,GAAG,CACb,CAAC,EACDF,QAAQ,CAACG,eAAe,CAACC,YAAY,GAAGV,MAAM,CAACW,WACjD,CAAC;MACH;IACF,CAAC;EACH;EAEA,OAAO;IACLZ,SAAS,EAAEA,CAAA,KAAM;MACf,MAAMa,OAAO,GAAGlB,cAAc,CAACC,MAAM,CAAC;MACtC,OAAOiB,OAAO,GAAGA,OAAO,CAACC,SAAS,GAAG,CAAC;IACxC,CAAC;IACDX,SAAS,EAAGC,MAAM,IAAK;MACrB,MAAMS,OAAO,GAAGlB,cAAc,CAACC,MAAM,CAAC;MACtC,IAAIiB,OAAO,EAAEA,OAAO,CAACC,SAAS,GAAGV,MAAM;IACzC,CAAC;IACDE,YAAY,EAAEA,CAAA,KAAM;MAClB,MAAMO,OAAO,GAAGlB,cAAc,CAACC,MAAM,CAAC;MACtC,IAAI,CAACiB,OAAO,EAAE,OAAO,CAAC;MACtB,OAAOL,IAAI,CAACC,GAAG,CAAC,CAAC,EAAEI,OAAO,CAACF,YAAY,GAAGE,OAAO,CAACE,YAAY,CAAC;IACjE;EACF,CAAC;AACH","ignoreList":[]}