@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
@@ -22,13 +22,15 @@ import { useControllableState } from '../hooks/use-controllable-state';
22
22
  import { RiCloseLine } from '../icons/remix/RiCloseLine';
23
23
  import { RiCodeSLine } from '../icons/remix/RiCodeSLine';
24
24
  import { RiMenuLine } from '../icons/remix/RiMenuLine';
25
- import type { WebCssStyle } from '../styles/web-view-style';
25
+ import { WEB_OVERFLOW_CLIP, WEB_POSITION_FIXED, WEB_VIEWPORT_HEIGHT, type WebCssStyle } from '../styles/web-view-style';
26
26
  import { Text } from '../typography';
27
- import { AiChatShellContext, useAiChatShell, type AiChatShellState } from './context';
27
+ import { AiChatDocumentGutterContext, AiChatShellContext, useAiChatShell, type AiChatShellState } from './context';
28
28
  import {
29
29
  dataHook,
30
+ DOCUMENT_RAIL,
30
31
  IS_WEB,
31
32
  PANEL_BREAKPOINT,
33
+ SHELL_GUTTER,
32
34
  SIDEBAR_BREAKPOINT,
33
35
  useAiChatPalette,
34
36
  useAiChatWebCss,
@@ -319,6 +321,7 @@ export function AiChatShell({
319
321
  onPanelOpenChange,
320
322
  background,
321
323
  surface = true,
324
+ scroll = 'container',
322
325
  labels,
323
326
  style,
324
327
  testID,
@@ -328,6 +331,12 @@ export function AiChatShell({
328
331
  const reducedMotion = useReducedMotion();
329
332
  const l = useMemo(() => ({ ...DEFAULT_LABELS, ...labels }), [labels]);
330
333
  const { width: windowWidth } = useWindowDimensions();
334
+ // Native has no document: `document` is `container` there.
335
+ const documentScroll = IS_WEB && scroll === 'document';
336
+ // What covers the screen — the drawers, the backdrop, a shell background —
337
+ // is `fixed` over a scrolling document: `absolute` would cover the page and
338
+ // scroll away with it.
339
+ const overlayPosition = documentScroll ? WEB_POSITION_FIXED : 'absolute';
331
340
  const wide = windowWidth >= PANEL_BREAKPOINT;
332
341
  const navInFlow = windowWidth >= SIDEBAR_BREAKPOINT;
333
342
 
@@ -523,6 +532,7 @@ export function AiChatShell({
523
532
  // In flow it is always on screen; as a drawer, only while it is open.
524
533
  navPresented: navInFlow ? !!sidebar : navOpen,
525
534
  sidebarCollapsed,
535
+ documentScroll,
526
536
  openNav: () => {
527
537
  setPanelOpen(false);
528
538
  setNavOpen(true);
@@ -542,6 +552,7 @@ export function AiChatShell({
542
552
  navOpen,
543
553
  sidebar,
544
554
  sidebarCollapsed,
555
+ documentScroll,
545
556
  mobileSidebar,
546
557
  panel,
547
558
  setPanelOpen,
@@ -555,11 +566,13 @@ export function AiChatShell({
555
566
  const rootStyle: WebCssStyle = {
556
567
  position: 'relative',
557
568
  width: '100%',
558
- height: '100%',
569
+ // A document-scrolled shell is at least one screen and grows with the
570
+ // chat. `clip`, not `hidden`, so it is not a scroll container of its own
571
+ // and the sticky columns inside still stick to the screen.
572
+ ...(documentScroll ? { minHeight: WEB_VIEWPORT_HEIGHT, overflow: WEB_OVERFLOW_CLIP } : { height: '100%', overflow: 'hidden' }),
559
573
  flexDirection: 'row',
560
574
  gap: 16,
561
- overflow: 'hidden',
562
- padding: 12,
575
+ padding: SHELL_GUTTER,
563
576
  backgroundColor: surface ? palette.full : 'transparent',
564
577
  };
565
578
 
@@ -567,6 +580,7 @@ export function AiChatShell({
567
580
 
568
581
  return (
569
582
  <AiChatShellContext.Provider value={shellState}>
583
+ <AiChatDocumentGutterContext.Provider value={documentScroll && surface ? palette.full : null}>
570
584
  <View
571
585
  {...dataHook('bloomAiChatDragging', dragging ? 'on' : '')}
572
586
  {...(navSwipeArmed ? navSwipe.panHandlers : null)}
@@ -574,7 +588,7 @@ export function AiChatShell({
574
588
  onLayout={(event: LayoutChangeEvent) => setShellWidth(event.nativeEvent.layout.width)}
575
589
  style={[rootStyle, style]}>
576
590
  {background ? (
577
- <View pointerEvents="none" style={{ position: 'absolute', top: 0, left: 0, right: 0, bottom: 0 }}>
591
+ <View pointerEvents="none" style={{ position: overlayPosition, top: 0, left: 0, right: 0, bottom: 0 }}>
578
592
  {background}
579
593
  </View>
580
594
  ) : null}
@@ -582,7 +596,7 @@ export function AiChatShell({
582
596
  <View
583
597
  aria-hidden={!navOpen}
584
598
  pointerEvents={navOpen ? 'box-none' : 'none'}
585
- style={{ position: 'absolute', top: 0, bottom: 0, left: 0, zIndex: 10, width: 272, paddingTop: 12, paddingBottom: 12, paddingLeft: 6 }}>
599
+ style={{ position: overlayPosition, top: 0, bottom: 0, left: 0, zIndex: 10, width: 272, paddingTop: 12, paddingBottom: 12, paddingLeft: 6 }}>
586
600
  <Animated.View
587
601
  pointerEvents={navOpen ? 'auto' : 'none'}
588
602
  style={[{ height: '100%', width: 260, transformOrigin: 'left' }, railStyle]}>
@@ -594,9 +608,8 @@ export function AiChatShell({
594
608
  {navInFlow ? (
595
609
  <View
596
610
  style={{
597
- position: 'relative',
611
+ ...(documentScroll ? DOCUMENT_RAIL : { position: 'relative', height: '100%' }),
598
612
  zIndex: 10,
599
- height: '100%',
600
613
  // Untouched unless the host asked for a width: the sidebar has
601
614
  // always sized itself.
602
615
  ...(sidebarCollapsed
@@ -618,7 +631,7 @@ export function AiChatShell({
618
631
  flex: 1,
619
632
  flexDirection: 'column',
620
633
  gap: 8,
621
- overflow: 'hidden',
634
+ overflow: documentScroll ? WEB_OVERFLOW_CLIP : 'hidden',
622
635
  backgroundColor: surface ? palette.full : 'transparent',
623
636
  },
624
637
  navInFlow ? null : workspaceStyle,
@@ -636,7 +649,12 @@ export function AiChatShell({
636
649
  />
637
650
  </Animated.View>
638
651
  ) : null}
639
- <View style={{ minHeight: 0, minWidth: 0, flex: 1, flexDirection: 'row', gap: 12, overflow: 'hidden' }}>
652
+ <View
653
+ style={
654
+ documentScroll
655
+ ? { minWidth: 0, flexGrow: 1, flexDirection: 'row', gap: 12 }
656
+ : { minHeight: 0, minWidth: 0, flex: 1, flexDirection: 'row', gap: 12, overflow: 'hidden' }
657
+ }>
640
658
  <View style={{ position: 'relative', minWidth: 0, flex: 1, flexBasis: 0, flexDirection: 'row' }}>
641
659
  {children}
642
660
  {wide && panel ? (
@@ -649,7 +667,14 @@ export function AiChatShell({
649
667
  />
650
668
  ) : null}
651
669
  </View>
652
- {wide && panel ? panel(panelWidth) : null}
670
+ {wide && panel ? (
671
+ documentScroll ? (
672
+ // A row, so the panel stretches to the rail's height.
673
+ <View style={[DOCUMENT_RAIL, { flexDirection: 'row' }]}>{panel(panelWidth)}</View>
674
+ ) : (
675
+ panel(panelWidth)
676
+ )
677
+ ) : null}
653
678
  </View>
654
679
  </Animated.View>
655
680
 
@@ -657,7 +682,7 @@ export function AiChatShell({
657
682
  <View
658
683
  aria-hidden={!panelOpen}
659
684
  pointerEvents={panelOpen ? 'auto' : 'none'}
660
- style={{ position: 'absolute', top: 0, left: 0, right: 0, bottom: 0, zIndex: 50 }}>
685
+ style={{ position: overlayPosition, top: 0, left: 0, right: 0, bottom: 0, zIndex: 50 }}>
661
686
  <Animated.View style={[{ position: 'absolute', top: 0, left: 0, right: 0, bottom: 0 }, backdropStyle]}>
662
687
  <Pressable
663
688
  accessibilityRole="button"
@@ -707,6 +732,7 @@ export function AiChatShell({
707
732
  </View>
708
733
  ) : null}
709
734
  </View>
735
+ </AiChatDocumentGutterContext.Provider>
710
736
  </AiChatShellContext.Provider>
711
737
  );
712
738
  }
@@ -60,6 +60,12 @@ export interface AiChatShellState {
60
60
  navPresented: boolean;
61
61
  /** The in-flow sidebar is narrowed to its rail width (`sidebarCollapsed`). */
62
62
  sidebarCollapsed: boolean;
63
+ /**
64
+ * The page scrolls the DOCUMENT (`scroll="document"`, web). The container
65
+ * pins its chrome to the screen and the thread follows the window instead of
66
+ * a `ScrollView` of its own.
67
+ */
68
+ documentScroll: boolean;
63
69
  openNav: () => void;
64
70
  closeNav: () => void;
65
71
  openPanel: () => void;
@@ -76,6 +82,15 @@ export type AiChatShellIcon = BloomIconComponent;
76
82
 
77
83
  export const AiChatShellContext = createContext<AiChatShellState | null>(null);
78
84
 
85
+ /**
86
+ * Internal: what a document-scrolled shell tells the card inside it — the
87
+ * colour of the gutter around the card, which the card's pinned frame paints
88
+ * over whatever of the conversation scrolls into it. `null` when the page is
89
+ * not document-scrolled, or when the shell paints no surface of its own and
90
+ * there is no known colour to mask with.
91
+ */
92
+ export const AiChatDocumentGutterContext = createContext<string | null>(null);
93
+
79
94
  export function useAiChatShell(): AiChatShellState | null {
80
95
  return useContext(AiChatShellContext);
81
96
  }
@@ -1,5 +1,5 @@
1
1
  import { useEffect, useMemo } from 'react';
2
- import { Platform } from 'react-native';
2
+ import { Platform, type ViewStyle } from 'react-native';
3
3
 
4
4
  import {
5
5
  BUTTON_SHADOW,
@@ -7,6 +7,11 @@ import {
7
7
  type Ramp,
8
8
  } from '../button/shared';
9
9
  import { adoptStyleSheet } from '../styles/adopt-style-sheet';
10
+ import {
11
+ WEB_POSITION_STICKY,
12
+ webViewportHeightMinus,
13
+ type WebCssStyle,
14
+ } from '../styles/web-view-style';
10
15
  import type { Theme } from '../theme/types';
11
16
  import { useTheme } from '../theme/use-theme';
12
17
 
@@ -107,6 +112,39 @@ export const PANEL_BREAKPOINT = 1280;
107
112
  /** The sidebar sits in flow from `lg`; below it becomes a push drawer. */
108
113
  export const SIDEBAR_BREAKPOINT = 1024;
109
114
 
115
+ /** The shell's padding: the gutter around its columns, px. */
116
+ export const SHELL_GUTTER = 12;
117
+
118
+ /**
119
+ * `scroll="document"`: a column pinned to the viewport while the document
120
+ * scrolls under it — the sidebar and the panel. One screen tall less the
121
+ * gutter above and below, so it closes where the page does; `flex-start` so
122
+ * the row's stretch does not make it as tall as the page, which would leave the
123
+ * sticky box nowhere to move.
124
+ */
125
+ export const DOCUMENT_RAIL: WebCssStyle = {
126
+ position: WEB_POSITION_STICKY,
127
+ top: SHELL_GUTTER,
128
+ height: webViewportHeightMinus(SHELL_GUTTER * 2),
129
+ alignSelf: 'flex-start',
130
+ flexShrink: 0,
131
+ };
132
+
133
+ /**
134
+ * `scroll="document"`: a layer the size of the visible card that takes no room
135
+ * in the flow — pinned like the rail, then pulled back out of the layout by an
136
+ * equal negative margin, so what follows it starts where it does. The card's
137
+ * frame and its `background` slot are drawn on it: the card grows with the
138
+ * conversation, the layer stays the size of the screen.
139
+ */
140
+ export const DOCUMENT_LAYER: WebCssStyle = {
141
+ position: WEB_POSITION_STICKY,
142
+ top: SHELL_GUTTER,
143
+ height: webViewportHeightMinus(SHELL_GUTTER * 2),
144
+ marginBottom: `calc(-100dvh + ${SHELL_GUTTER * 2}px)` as ViewStyle['marginBottom'],
145
+ borderRadius: CONTAINER_RADIUS,
146
+ };
147
+
110
148
  // ---------------------------------------------------------------------------
111
149
  // Web
112
150
  // ---------------------------------------------------------------------------
@@ -132,7 +170,10 @@ const STYLE_ID = 'bloom-ai-chat-web-css';
132
170
  * - the generated image's feathered radial reveal, a registered `--bloom-ai-reveal`
133
171
  * percentage transitioning 0% → 132% over 1550ms;
134
172
  * - the resize grip's hover reveal;
135
- * - the composer field's placeholder colour.
173
+ * - the composer field's placeholder colour;
174
+ * - the document-scrolled card's frame, clipped to its gutter (RN-web drops
175
+ * `clip-path` from `style`), and its thread out of the browser's scroll
176
+ * anchoring.
136
177
  */
137
178
  export const AI_CHAT_WEB_CSS = `
138
179
  @property --bloom-ai-reveal {
@@ -158,6 +199,8 @@ export const AI_CHAT_WEB_CSS = `
158
199
  [data-bloom-ai-chat-zoom="in"] { cursor: zoom-in; }
159
200
  [data-bloom-ai-chat-zoom="out"] { cursor: zoom-out; }
160
201
  [data-bloom-ai-chat-zoom]:focus-visible { box-shadow: inset 0 0 0 2px var(--bloom-ai-chat-ring); }
202
+ [data-bloom-ai-chat-frame] { clip-path: inset(-${SHELL_GUTTER}px -1px); }
203
+ [data-bloom-ai-chat-thread] { overflow-anchor: none; }
161
204
  [data-bloom-ai-chat-scroll="thin"] { scrollbar-width: thin; }
162
205
  [data-bloom-ai-chat-scroll="none"] { scrollbar-width: none; }
163
206
  [data-bloom-ai-chat-scroll="none"]::-webkit-scrollbar { display: none; }
@@ -470,6 +470,25 @@ export interface AiChatShellProps {
470
470
  * Default `true`; `false` leaves both transparent.
471
471
  */
472
472
  surface?: boolean;
473
+ /**
474
+ * What scrolls the page — `AppShell`'s contract.
475
+ *
476
+ * - `container` (default) — the shell fills its parent and the conversation
477
+ * scrolls inside the card, in the thread's own `ScrollView`.
478
+ * - `document` — WEB: the chat grows the document and the browser scrolls it:
479
+ * the page's scrollbar, scroll restoration, the mobile address bar
480
+ * collapsing and `window.scrollTo` all behave as on any page. The sidebar
481
+ * and the panel stay pinned (`position: sticky`); the chat's header and
482
+ * composer float over the conversation, pinned to the top and the bottom of
483
+ * the screen, and it passes behind them. The drawers and the backdrop are
484
+ * `position: fixed`, so they cover the screen rather than the page.
485
+ *
486
+ * Nothing between the shell and the document may be a scroll container: an
487
+ * app's `html`/`body`/root reset of `height: 100%; overflow: hidden` pins
488
+ * everything to one screen, so it has to let the document grow. Native has
489
+ * no document and treats it as `container`.
490
+ */
491
+ scroll?: 'container' | 'document';
473
492
  labels?: AiChatShellLabels;
474
493
  style?: StyleProp<ViewStyle>;
475
494
  testID?: string;