@oxy.so/bloom 4.12.0 → 4.13.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 (43) hide show
  1. package/docs/ai-chat.mdx +31 -5
  2. package/lib/commonjs/ai-chat/AiChatContainer.js +390 -116
  3. package/lib/commonjs/ai-chat/AiChatContainer.js.map +1 -1
  4. package/lib/commonjs/ai-chat/AiChatShell.js +211 -176
  5. package/lib/commonjs/ai-chat/AiChatShell.js.map +1 -1
  6. package/lib/commonjs/ai-chat/context.js +10 -1
  7. package/lib/commonjs/ai-chat/context.js.map +1 -1
  8. package/lib/commonjs/ai-chat/shared.js +41 -2
  9. package/lib/commonjs/ai-chat/shared.js.map +1 -1
  10. package/lib/module/ai-chat/AiChatContainer.js +394 -120
  11. package/lib/module/ai-chat/AiChatContainer.js.map +1 -1
  12. package/lib/module/ai-chat/AiChatShell.js +213 -178
  13. package/lib/module/ai-chat/AiChatShell.js.map +1 -1
  14. package/lib/module/ai-chat/context.js +9 -0
  15. package/lib/module/ai-chat/context.js.map +1 -1
  16. package/lib/module/ai-chat/shared.js +40 -1
  17. package/lib/module/ai-chat/shared.js.map +1 -1
  18. package/lib/typescript/commonjs/ai-chat/AiChatContainer.d.ts +8 -3
  19. package/lib/typescript/commonjs/ai-chat/AiChatContainer.d.ts.map +1 -1
  20. package/lib/typescript/commonjs/ai-chat/AiChatShell.d.ts +1 -1
  21. package/lib/typescript/commonjs/ai-chat/AiChatShell.d.ts.map +1 -1
  22. package/lib/typescript/commonjs/ai-chat/context.d.ts +14 -0
  23. package/lib/typescript/commonjs/ai-chat/context.d.ts.map +1 -1
  24. package/lib/typescript/commonjs/ai-chat/shared.d.ts +24 -2
  25. package/lib/typescript/commonjs/ai-chat/shared.d.ts.map +1 -1
  26. package/lib/typescript/commonjs/ai-chat/types.d.ts +19 -0
  27. package/lib/typescript/commonjs/ai-chat/types.d.ts.map +1 -1
  28. package/lib/typescript/module/ai-chat/AiChatContainer.d.ts +8 -3
  29. package/lib/typescript/module/ai-chat/AiChatContainer.d.ts.map +1 -1
  30. package/lib/typescript/module/ai-chat/AiChatShell.d.ts +1 -1
  31. package/lib/typescript/module/ai-chat/AiChatShell.d.ts.map +1 -1
  32. package/lib/typescript/module/ai-chat/context.d.ts +14 -0
  33. package/lib/typescript/module/ai-chat/context.d.ts.map +1 -1
  34. package/lib/typescript/module/ai-chat/shared.d.ts +24 -2
  35. package/lib/typescript/module/ai-chat/shared.d.ts.map +1 -1
  36. package/lib/typescript/module/ai-chat/types.d.ts +19 -0
  37. package/lib/typescript/module/ai-chat/types.d.ts.map +1 -1
  38. package/package.json +1 -1
  39. package/src/ai-chat/AiChatContainer.tsx +361 -88
  40. package/src/ai-chat/AiChatShell.tsx +39 -13
  41. package/src/ai-chat/context.ts +15 -0
  42. package/src/ai-chat/shared.ts +45 -2
  43. package/src/ai-chat/types.ts +19 -0
@@ -19,6 +19,7 @@ var _useIsomorphicLayoutEffect = require("../theme/use-isomorphic-layout-effect.
19
19
  var _AiChatControls = require("./AiChatControls.js");
20
20
  var _context = require("./context.js");
21
21
  var _shared = require("./shared.js");
22
+ var _webViewStyle = require("../styles/web-view-style.js");
22
23
  var _jsxRuntime = require("react/jsx-runtime");
23
24
  function _interopRequireWildcard(e, t) { if ("function" == typeof WeakMap) var r = new WeakMap(), n = new WeakMap(); return (_interopRequireWildcard = function (e, t) { if (!t && e && e.__esModule) return e; var o, i, f = { __proto__: null, default: e }; if (null === e || "object" != typeof e && "function" != typeof e) return f; if (o = t ? n : r) { if (o.has(e)) return o.get(e); o.set(e, f); } for (const t in e) "default" !== t && {}.hasOwnProperty.call(e, t) && ((i = (o = Object.defineProperty) && Object.getOwnPropertyDescriptor(e, t)) && (i.get || i.set) ? o(f, t, i) : f[t] = e[t]); return f; })(e, t); }
24
25
  const DEFAULT_LABELS = {
@@ -29,12 +30,8 @@ const DEFAULT_LABELS = {
29
30
 
30
31
  /** The thread's own top padding: the gap between the header and the first turn. */
31
32
  const THREAD_PAD_TOP = 16;
32
- /**
33
- * `floatingChrome`: the footer's fade strip, px — the footer's top padding, so
34
- * the last turn at rest ends where the fade begins, and the distance over which
35
- * the bottom fade ramps in as the transcript slides under it.
36
- */
37
- const FOOTER_FADE = 24;
33
+ /** The footer's top padding: the gap between the last turn at rest and the composer. */
34
+ const FOOTER_GAP = 12;
38
35
  /** Below this many px of overlap an edge counts as clear (sub-pixel scroll noise). */
39
36
  const EDGE_THRESHOLD = 1;
40
37
  const HIDDEN_FROM_A11Y = {
@@ -68,14 +65,19 @@ const HIDDEN_FROM_A11Y = {
68
65
  * an edge scrim of the surface (opaque at the top, gone
69
66
  * `SCRIM_TAIL_RATIO` below the block) fades in over the first 16px
70
67
  * of transcript scrolled under it
71
- * footer absolute at the bottom, pt 24 instead of 12: the 24px strip is a
72
- * scrim ramp and the rest is solid surface, faded in over the first
73
- * 24px of transcript below the fold
68
+ * footer absolute at the bottom, with no band of its own: the transcript
69
+ * passes behind the composer and stays in sight around it
74
70
  *
75
71
  * Both blocks are `pointerEvents="box-none"`, so the transcript under their
76
72
  * empty space still scrolls and takes clicks. The measured heights reach an
77
73
  * `AiChatThread` child through context (its content padding) and any other
78
74
  * child through `useAiChatChromeInsets()`.
75
+ *
76
+ * Under a shell with `scroll="document"` the chrome always floats, since the
77
+ * document is what moves: the header and the footer are `sticky` in the flow,
78
+ * pinned to the screen's top and bottom gutter, the `background` slot and the
79
+ * card's frame are layers the size of the screen, and the card grows with the
80
+ * conversation. At rest nothing overlaps, so the insets are zero.
79
81
  */
80
82
  function AiChatContainer({
81
83
  project,
@@ -103,6 +105,8 @@ function AiChatContainer({
103
105
  ...DEFAULT_LABELS,
104
106
  ...labels
105
107
  }), [labels]);
108
+ const documentScroll = (0, _context.useAiChatShell)()?.documentScroll ?? false;
109
+ const gutterColor = (0, _react.useContext)(_context.AiChatDocumentGutterContext);
106
110
  const hasCrumbRow = title != null || project != null || onShare || onMore || actions != null;
107
111
  const crumbRow = hasCrumbRow ? /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
108
112
  pointerEvents: "box-none",
@@ -185,6 +189,35 @@ function AiChatContainer({
185
189
  borderRadius: _shared.CONTAINER_RADIUS,
186
190
  backgroundColor: surface ? palette.secondary : 'transparent'
187
191
  }, style];
192
+ if (documentScroll) {
193
+ return /*#__PURE__*/(0, _jsxRuntime.jsx)(FloatingChrome, {
194
+ documentScroll: true,
195
+ testID: testID,
196
+ rootStyle: [{
197
+ // Grows with the conversation, one screen at the least; nothing
198
+ // clips it, so the header and composer inside can stick.
199
+ minHeight: (0, _webViewStyle.webViewportHeightMinus)(_shared.SHELL_GUTTER * 2),
200
+ minWidth: 0,
201
+ flex: 1,
202
+ flexDirection: 'column',
203
+ borderRadius: _shared.CONTAINER_RADIUS,
204
+ backgroundColor: surface ? palette.secondary : 'transparent'
205
+ }, style],
206
+ backgroundLayer: background ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
207
+ pointerEvents: "none",
208
+ style: [_shared.DOCUMENT_LAYER, {
209
+ overflow: 'hidden'
210
+ }],
211
+ children: background
212
+ }) : null,
213
+ header: header,
214
+ crumbRow: crumbRow,
215
+ footerBody: footerBody,
216
+ fadeColor: surface ? palette.secondary : null,
217
+ gutterColor: gutterColor,
218
+ children: children
219
+ });
220
+ }
188
221
  if (floatingChrome) {
189
222
  return /*#__PURE__*/(0, _jsxRuntime.jsx)(FloatingChrome, {
190
223
  testID: testID,
@@ -214,17 +247,24 @@ function AiChatContainer({
214
247
  })]
215
248
  });
216
249
  }
250
+ const NO_INSETS = {
251
+ top: 0,
252
+ bottom: 0
253
+ };
254
+
217
255
  /**
218
- * `AiChatContainer floatingChrome`: the same parts, the chrome floated over the
219
- * transcript. Everything it measures lives here, so the stacked path above
220
- * carries none of the state.
256
+ * `AiChatContainer floatingChrome` (and every container under a document-
257
+ * scrolled shell): the same parts, the chrome floated over the transcript.
258
+ * Everything it measures lives here, so the stacked path above carries none of
259
+ * the state.
221
260
  *
222
- * The edge opacities are shared values written from the thread's scroll
223
- * reports, so a scroll frame re-renders nothing; the booleans only mount and
224
- * unmount the scrims, and change once per crossing. Same model as the
261
+ * The header's edge opacity is a shared value written from the thread's scroll
262
+ * reports, so a scroll frame re-renders nothing; the boolean only mounts and
263
+ * unmounts the scrim, and changes once per crossing. Same model as the
225
264
  * sidebar's `SidebarScrollArea`, whose scrim this reuses.
226
265
  */
227
266
  function FloatingChrome({
267
+ documentScroll = false,
228
268
  testID,
229
269
  rootStyle,
230
270
  backgroundLayer,
@@ -232,32 +272,30 @@ function FloatingChrome({
232
272
  crumbRow,
233
273
  footerBody,
234
274
  fadeColor,
275
+ gutterColor = null,
235
276
  children
236
277
  }) {
237
- const [insets, setInsets] = (0, _react.useState)({
238
- top: 0,
239
- bottom: 0
240
- });
241
- const [edges, setEdges] = (0, _react.useState)({
242
- top: false,
243
- bottom: false
244
- });
278
+ const [insets, setInsets] = (0, _react.useState)(NO_INSETS);
279
+ const [headerHeight, setHeaderHeight] = (0, _react.useState)(0);
280
+ const [scrolled, setScrolled] = (0, _react.useState)(false);
245
281
  const topFade = (0, _reactNativeReanimated.useSharedValue)(0);
246
- const bottomFade = (0, _reactNativeReanimated.useSharedValue)(0);
247
282
  const onHeaderLayout = (0, _react.useCallback)(event => {
248
283
  const top = Math.round(event.nativeEvent.layout.height);
249
- setInsets(previous => previous.top === top ? previous : {
284
+ setHeaderHeight(top);
285
+ // In the flow the header covers nothing at rest: no inset to publish.
286
+ if (!documentScroll) setInsets(previous => previous.top === top ? previous : {
250
287
  ...previous,
251
288
  top
252
289
  });
253
- }, []);
290
+ }, [documentScroll]);
254
291
  const onFooterLayout = (0, _react.useCallback)(event => {
292
+ if (documentScroll) return;
255
293
  const bottom = Math.round(event.nativeEvent.layout.height);
256
294
  setInsets(previous => previous.bottom === bottom ? previous : {
257
295
  ...previous,
258
296
  bottom
259
297
  });
260
- }, []);
298
+ }, [documentScroll]);
261
299
  const reportScroll = (0, _react.useCallback)(({
262
300
  offset,
263
301
  viewport,
@@ -266,18 +304,12 @@ function FloatingChrome({
266
304
  const range = Math.max(0, content - viewport);
267
305
  const y = Math.max(0, Math.min(offset, range));
268
306
  const overflow = viewport > 0 && range > EDGE_THRESHOLD;
269
- // The first turn rests THREAD_PAD_TOP below the header and the last one
270
- // right on the footer's fade strip, so the overlap starts at the first px
271
- // scrolled and each ramp spans the gap it is closing.
272
- const top = overflow && y > EDGE_THRESHOLD;
273
- const bottom = overflow && range - y > EDGE_THRESHOLD;
307
+ // The first turn rests THREAD_PAD_TOP below the header, so the overlap
308
+ // starts at the first px scrolled and the ramp spans the gap it closes.
274
309
  topFade.value = overflow ? Math.min(1, y / THREAD_PAD_TOP) : 0;
275
- bottomFade.value = overflow ? Math.min(1, (range - y) / FOOTER_FADE) : 0;
276
- setEdges(previous => previous.top === top && previous.bottom === bottom ? previous : {
277
- top,
278
- bottom
279
- });
280
- }, [topFade, bottomFade]);
310
+ const next = overflow && y > EDGE_THRESHOLD;
311
+ setScrolled(previous => previous === next ? previous : next);
312
+ }, [topFade]);
281
313
  const chrome = (0, _react.useMemo)(() => ({
282
314
  insets,
283
315
  reportScroll
@@ -290,92 +322,110 @@ function FloatingChrome({
290
322
  const topStyle = (0, _reactNativeReanimated.useAnimatedStyle)(() => ({
291
323
  opacity: topFade.value * alpha
292
324
  }), [topFade, alpha]);
293
- const bottomStyle = (0, _reactNativeReanimated.useAnimatedStyle)(() => ({
294
- opacity: bottomFade.value * alpha
295
- }), [bottomFade, alpha]);
296
325
  const prefix = testID ?? 'ai-chat';
297
- return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
298
- testID: testID,
299
- style: [rootStyle, {
300
- position: 'relative'
301
- }],
302
- children: [backgroundLayer, /*#__PURE__*/(0, _jsxRuntime.jsx)(_context.AiChatFloatingChromeContext.Provider, {
303
- value: chrome,
304
- children: children
305
- }), /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
306
- testID: `${prefix}-chrome-top`,
307
- pointerEvents: "box-none",
308
- onLayout: onHeaderLayout,
309
- style: {
326
+
327
+ // Absolute over a card of fixed height; sticky in the flow of a card that
328
+ // grows with the document, pinned to the screen's gutter.
329
+ const headerPosition = documentScroll ? {
330
+ position: _webViewStyle.WEB_POSITION_STICKY,
331
+ top: _shared.SHELL_GUTTER,
332
+ zIndex: 2
333
+ } : {
334
+ position: 'absolute',
335
+ top: 0,
336
+ left: 0,
337
+ right: 0
338
+ };
339
+ const footerPosition = documentScroll ? {
340
+ position: _webViewStyle.WEB_POSITION_STICKY,
341
+ bottom: _shared.SHELL_GUTTER,
342
+ zIndex: 2
343
+ } : {
344
+ position: 'absolute',
345
+ left: 0,
346
+ right: 0,
347
+ bottom: 0
348
+ };
349
+ const top = /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
350
+ testID: `${prefix}-chrome-top`,
351
+ pointerEvents: "box-none",
352
+ onLayout: onHeaderLayout,
353
+ style: headerPosition,
354
+ children: [solid && scrolled && headerHeight > 0 ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNativeReanimated.default.View, {
355
+ testID: `${prefix}-fade-top`,
356
+ pointerEvents: "none",
357
+ ...HIDDEN_FROM_A11Y,
358
+ style: [{
310
359
  position: 'absolute',
311
360
  top: 0,
312
361
  left: 0,
313
- right: 0
314
- },
315
- children: [solid && edges.top && insets.top > 0 ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNativeReanimated.default.View, {
316
- testID: `${prefix}-fade-top`,
317
- pointerEvents: "none",
318
- ...HIDDEN_FROM_A11Y,
319
- style: [{
320
- position: 'absolute',
321
- top: 0,
322
- left: 0,
323
- right: 0,
324
- // Past the block's own bottom: a ramp that ends where the layout
325
- // does is a line across the transcript again.
326
- height: Math.round(insets.top * (1 + _EdgeScrim.SCRIM_TAIL_RATIO))
327
- }, topStyle],
328
- children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_EdgeScrim.EdgeScrim, {
329
- color: solid
330
- })
331
- }) : null, header, crumbRow]
332
- }), /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
333
- testID: `${prefix}-chrome-bottom`,
362
+ right: 0,
363
+ // Past the block's own bottom: a ramp that ends where the layout
364
+ // does is a line across the transcript again.
365
+ height: Math.round(headerHeight * (1 + _EdgeScrim.SCRIM_TAIL_RATIO)),
366
+ // The card's own corners, since nothing clips a document card.
367
+ borderTopLeftRadius: _shared.CONTAINER_RADIUS,
368
+ borderTopRightRadius: _shared.CONTAINER_RADIUS,
369
+ overflow: 'hidden'
370
+ }, topStyle],
371
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_EdgeScrim.EdgeScrim, {
372
+ color: solid
373
+ })
374
+ }) : null, header, crumbRow]
375
+ });
376
+ const bottom = /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
377
+ testID: `${prefix}-chrome-bottom`,
378
+ pointerEvents: "box-none",
379
+ onLayout: onFooterLayout,
380
+ style: footerPosition,
381
+ children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
334
382
  pointerEvents: "box-none",
335
- onLayout: onFooterLayout,
336
383
  style: {
337
- position: 'absolute',
338
- left: 0,
339
- right: 0,
340
- bottom: 0
384
+ width: '100%',
385
+ flexDirection: 'column',
386
+ gap: 10,
387
+ paddingLeft: 10,
388
+ paddingRight: 10,
389
+ paddingTop: FOOTER_GAP,
390
+ paddingBottom: 10
341
391
  },
342
- children: [solid && edges.bottom ? /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNativeReanimated.default.View, {
343
- testID: `${prefix}-fade-bottom`,
392
+ children: footerBody
393
+ })
394
+ });
395
+ const thread = /*#__PURE__*/(0, _jsxRuntime.jsx)(_context.AiChatFloatingChromeContext.Provider, {
396
+ value: chrome,
397
+ children: children
398
+ });
399
+ if (documentScroll) {
400
+ return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
401
+ testID: testID,
402
+ style: rootStyle,
403
+ children: [backgroundLayer, gutterColor ?
404
+ /*#__PURE__*/
405
+ // The card's frame, pinned to the screen: a ring of the gutter's
406
+ // colour painted over whatever of the conversation scrolls past the
407
+ // card's visible edge, so the card keeps its rounded corners and its
408
+ // gutter at the top and the bottom of the screen. Clipped to the
409
+ // gutter sideways (the family sheet), so it never reaches the columns
410
+ // beside the card.
411
+ (0, _jsxRuntime.jsx)(_reactNative.View, {
412
+ testID: `${prefix}-frame`,
344
413
  pointerEvents: "none",
345
414
  ...HIDDEN_FROM_A11Y,
346
- style: [_reactNative.StyleSheet.absoluteFill, {
347
- flexDirection: 'column'
348
- }, bottomStyle],
349
- children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
350
- style: {
351
- height: FOOTER_FADE,
352
- transform: [{
353
- rotate: '180deg'
354
- }]
355
- },
356
- children: /*#__PURE__*/(0, _jsxRuntime.jsx)(_EdgeScrim.EdgeScrim, {
357
- color: solid
358
- })
359
- }), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
360
- style: {
361
- flex: 1,
362
- backgroundColor: solid
363
- }
364
- })]
365
- }) : null, /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
366
- pointerEvents: "box-none",
367
- style: {
368
- width: '100%',
369
- flexDirection: 'column',
370
- gap: 10,
371
- paddingLeft: 10,
372
- paddingRight: 10,
373
- paddingTop: FOOTER_FADE,
374
- paddingBottom: 10
375
- },
376
- children: footerBody
377
- })]
378
- })]
415
+ ...(0, _shared.dataHook)('bloomAiChatFrame'),
416
+ style: [_shared.DOCUMENT_LAYER, {
417
+ zIndex: 1,
418
+ boxShadow: `0 0 0 ${_shared.SHELL_GUTTER}px ${gutterColor}`
419
+ }]
420
+ }) : null, top, thread, bottom]
421
+ });
422
+ }
423
+ return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
424
+ testID: testID,
425
+ style: [rootStyle, {
426
+ position: 'relative'
427
+ }],
428
+ children: [backgroundLayer, thread, top, bottom]
379
429
  });
380
430
  }
381
431
 
@@ -408,7 +458,19 @@ function FloatingChrome({
408
458
  * motion they pass under it. It also reports its position so the container
409
459
  * knows when to fade each edge. Anywhere else nothing of this exists.
410
460
  */
411
- const AiChatThread = exports.AiChatThread = /*#__PURE__*/(0, _react.forwardRef)(function AiChatThread({
461
+ const AiChatThread = exports.AiChatThread = /*#__PURE__*/(0, _react.forwardRef)(function AiChatThread(props, ref) {
462
+ const documentScroll = (0, _context.useAiChatShell)()?.documentScroll ?? false;
463
+ return documentScroll ? /*#__PURE__*/(0, _jsxRuntime.jsx)(DocumentThread, {
464
+ ref: ref,
465
+ ...props
466
+ }) : /*#__PURE__*/(0, _jsxRuntime.jsx)(ScrollThread, {
467
+ ref: ref,
468
+ ...props
469
+ });
470
+ });
471
+
472
+ /** The thread inside a card of fixed height: a `ScrollView` of its own. */
473
+ const ScrollThread = /*#__PURE__*/(0, _react.forwardRef)(function ScrollThread({
412
474
  children,
413
475
  autoFollow = true,
414
476
  followAnimated = true,
@@ -604,4 +666,216 @@ const AiChatThread = exports.AiChatThread = /*#__PURE__*/(0, _react.forwardRef)(
604
666
  children: children
605
667
  });
606
668
  });
669
+
670
+ /** The window's scroll position and extent, px. */
671
+ function readWindow() {
672
+ const page = document.scrollingElement ?? document.documentElement;
673
+ return {
674
+ y: window.scrollY,
675
+ viewport: window.innerHeight,
676
+ end: page.scrollHeight
677
+ };
678
+ }
679
+ function scrollWindowTo(top, animated) {
680
+ window.scrollTo({
681
+ top,
682
+ behavior: animated ? 'smooth' : 'auto'
683
+ });
684
+ }
685
+
686
+ /**
687
+ * The thread under a document-scrolled shell (web): no scroller of its own —
688
+ * the turns grow the page and the window scrolls them. Same props, same handle,
689
+ * same behaviour: it follows the newest turn, fires `onStartReached` near the
690
+ * top, holds the reader still while a page lands above, and reports its
691
+ * position to the container.
692
+ *
693
+ * Offsets keep the thread's own frame: `0` is the thread's top, so a row's
694
+ * `onLayout` y is still a `scrollToOffset` target, and `contentSize` runs to the
695
+ * END OF THE PAGE, so `contentSize − offset − viewport` is still the distance
696
+ * left to scroll. The browser's scroll anchoring is off inside the thread (the
697
+ * family sheet): it would move the page for growth the thread already accounts
698
+ * for, and only in some browsers.
699
+ */
700
+ const DocumentThread = /*#__PURE__*/(0, _react.forwardRef)(function DocumentThread({
701
+ children,
702
+ autoFollow = true,
703
+ followAnimated = true,
704
+ followThreshold,
705
+ onStartReached,
706
+ onStartReachedThreshold = 300,
707
+ maintainStartPosition = false,
708
+ onScroll,
709
+ style,
710
+ testID
711
+ }, ref) {
712
+ const chrome = (0, _react.useContext)(_context.AiChatFloatingChromeContext);
713
+ const nodeRef = (0, _react.useRef)(null);
714
+ const lastHeight = (0, _react.useRef)(0);
715
+ /** An `onStartReached` is out and its page has not landed yet. */
716
+ const pageOut = (0, _react.useRef)(false);
717
+ /** The reader has left the top zone, so the next approach may fire again. */
718
+ const startArmed = (0, _react.useRef)(true);
719
+
720
+ /** The thread's top, px from the top of the page. */
721
+ const threadTop = (0, _react.useCallback)(() => {
722
+ const node = nodeRef.current;
723
+ return node ? node.getBoundingClientRect().top + window.scrollY : 0;
724
+ }, []);
725
+ (0, _react.useImperativeHandle)(ref, () => ({
726
+ scrollToEnd: options => scrollWindowTo(readWindow().end, options?.animated ?? true),
727
+ scrollToOffset: ({
728
+ offset: y,
729
+ animated = false
730
+ }) => scrollWindowTo(threadTop() + y, animated),
731
+ getScrollView: () => null
732
+ }), [threadTop]);
733
+
734
+ // The latest handlers, read by one window listener that is never rebound.
735
+ const handlers = (0, _react.useRef)({
736
+ chrome,
737
+ onScroll,
738
+ onStartReached,
739
+ onStartReachedThreshold
740
+ });
741
+ (0, _useIsomorphicLayoutEffect.useIsomorphicLayoutEffect)(() => {
742
+ handlers.current = {
743
+ chrome,
744
+ onScroll,
745
+ onStartReached,
746
+ onStartReachedThreshold
747
+ };
748
+ });
749
+ const report = (0, _react.useCallback)(() => {
750
+ const {
751
+ chrome: frame,
752
+ onScroll: listener,
753
+ onStartReached: startReached,
754
+ onStartReachedThreshold: zone
755
+ } = handlers.current;
756
+ const {
757
+ y,
758
+ viewport,
759
+ end
760
+ } = readWindow();
761
+ frame?.reportScroll({
762
+ offset: y,
763
+ viewport,
764
+ content: end
765
+ });
766
+ const top = threadTop();
767
+ const offset = y - top;
768
+ if (startReached) {
769
+ if (offset <= zone) {
770
+ if (startArmed.current) {
771
+ startArmed.current = false;
772
+ pageOut.current = true;
773
+ startReached();
774
+ }
775
+ } else {
776
+ startArmed.current = true;
777
+ }
778
+ }
779
+ listener?.({
780
+ nativeEvent: {
781
+ contentOffset: {
782
+ x: 0,
783
+ y: offset
784
+ },
785
+ layoutMeasurement: {
786
+ width: window.innerWidth,
787
+ height: viewport
788
+ },
789
+ contentSize: {
790
+ width: window.innerWidth,
791
+ height: end - top
792
+ },
793
+ contentInset: {
794
+ top: 0,
795
+ left: 0,
796
+ bottom: 0,
797
+ right: 0
798
+ },
799
+ zoomScale: 1
800
+ }
801
+ });
802
+ }, [threadTop]);
803
+ (0, _react.useEffect)(() => {
804
+ let frame = 0;
805
+ const onWindowChange = () => {
806
+ if (frame !== 0) return;
807
+ frame = requestAnimationFrame(() => {
808
+ frame = 0;
809
+ report();
810
+ });
811
+ };
812
+ window.addEventListener('scroll', onWindowChange, {
813
+ passive: true
814
+ });
815
+ window.addEventListener('resize', onWindowChange);
816
+ report();
817
+ return () => {
818
+ cancelAnimationFrame(frame);
819
+ window.removeEventListener('scroll', onWindowChange);
820
+ window.removeEventListener('resize', onWindowChange);
821
+ };
822
+ }, [report]);
823
+ const onLayout = (0, _react.useCallback)(event => {
824
+ const height = event.nativeEvent.layout.height;
825
+ const previous = lastHeight.current;
826
+ lastHeight.current = height;
827
+ report();
828
+ // The page is shared with whatever was on it before — another route
829
+ // left it scrolled wherever it was — so a following thread opens at its
830
+ // newest turn rather than at that offset.
831
+ if (previous === 0) {
832
+ if (autoFollow) scrollWindowTo(readWindow().end, false);
833
+ return;
834
+ }
835
+ if (height <= previous + 0.5) return;
836
+ const growth = height - previous;
837
+
838
+ // A page landing above the reader: keep the turn they are on where it is.
839
+ if (maintainStartPosition && pageOut.current) {
840
+ pageOut.current = false;
841
+ window.scrollBy({
842
+ top: growth,
843
+ behavior: 'auto'
844
+ });
845
+ return;
846
+ }
847
+ pageOut.current = false;
848
+ if (!autoFollow) return;
849
+ if (followThreshold !== undefined) {
850
+ // Measured against the page BEFORE the growth: was the reader near the
851
+ // end when this arrived?
852
+ const {
853
+ y,
854
+ viewport,
855
+ end
856
+ } = readWindow();
857
+ if (end - growth - y - viewport > followThreshold) return;
858
+ }
859
+ scrollWindowTo(readWindow().end, followAnimated);
860
+ }, [autoFollow, followAnimated, followThreshold, maintainStartPosition, report]);
861
+ return /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
862
+ ref: nodeRef,
863
+ testID: testID,
864
+ ...(0, _shared.dataHook)('bloomAiChatThread'),
865
+ onLayout: onLayout,
866
+ style: [{
867
+ // Fills the card between the header and the composer, and anchors a
868
+ // short exchange just above the composer, as the scroller does.
869
+ width: '100%',
870
+ flexGrow: 1,
871
+ flexDirection: 'column',
872
+ justifyContent: 'flex-end',
873
+ gap: 12,
874
+ paddingLeft: 16,
875
+ paddingRight: 16,
876
+ paddingTop: THREAD_PAD_TOP
877
+ }, style],
878
+ children: children
879
+ });
880
+ });
607
881
  //# sourceMappingURL=AiChatContainer.js.map