@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.
- package/docs/ai-chat.mdx +31 -5
- package/lib/commonjs/ai-chat/AiChatContainer.js +390 -116
- package/lib/commonjs/ai-chat/AiChatContainer.js.map +1 -1
- package/lib/commonjs/ai-chat/AiChatShell.js +211 -176
- package/lib/commonjs/ai-chat/AiChatShell.js.map +1 -1
- package/lib/commonjs/ai-chat/context.js +10 -1
- package/lib/commonjs/ai-chat/context.js.map +1 -1
- package/lib/commonjs/ai-chat/shared.js +41 -2
- package/lib/commonjs/ai-chat/shared.js.map +1 -1
- package/lib/module/ai-chat/AiChatContainer.js +394 -120
- package/lib/module/ai-chat/AiChatContainer.js.map +1 -1
- package/lib/module/ai-chat/AiChatShell.js +213 -178
- package/lib/module/ai-chat/AiChatShell.js.map +1 -1
- package/lib/module/ai-chat/context.js +9 -0
- package/lib/module/ai-chat/context.js.map +1 -1
- package/lib/module/ai-chat/shared.js +40 -1
- package/lib/module/ai-chat/shared.js.map +1 -1
- package/lib/typescript/commonjs/ai-chat/AiChatContainer.d.ts +8 -3
- package/lib/typescript/commonjs/ai-chat/AiChatContainer.d.ts.map +1 -1
- package/lib/typescript/commonjs/ai-chat/AiChatShell.d.ts +1 -1
- package/lib/typescript/commonjs/ai-chat/AiChatShell.d.ts.map +1 -1
- package/lib/typescript/commonjs/ai-chat/context.d.ts +14 -0
- package/lib/typescript/commonjs/ai-chat/context.d.ts.map +1 -1
- package/lib/typescript/commonjs/ai-chat/shared.d.ts +24 -2
- package/lib/typescript/commonjs/ai-chat/shared.d.ts.map +1 -1
- package/lib/typescript/commonjs/ai-chat/types.d.ts +19 -0
- package/lib/typescript/commonjs/ai-chat/types.d.ts.map +1 -1
- package/lib/typescript/module/ai-chat/AiChatContainer.d.ts +8 -3
- package/lib/typescript/module/ai-chat/AiChatContainer.d.ts.map +1 -1
- package/lib/typescript/module/ai-chat/AiChatShell.d.ts +1 -1
- package/lib/typescript/module/ai-chat/AiChatShell.d.ts.map +1 -1
- package/lib/typescript/module/ai-chat/context.d.ts +14 -0
- package/lib/typescript/module/ai-chat/context.d.ts.map +1 -1
- package/lib/typescript/module/ai-chat/shared.d.ts +24 -2
- package/lib/typescript/module/ai-chat/shared.d.ts.map +1 -1
- package/lib/typescript/module/ai-chat/types.d.ts +19 -0
- package/lib/typescript/module/ai-chat/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/ai-chat/AiChatContainer.tsx +361 -88
- package/src/ai-chat/AiChatShell.tsx +39 -13
- package/src/ai-chat/context.ts +15 -0
- package/src/ai-chat/shared.ts +45 -2
- 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
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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:
|
|
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
|
|
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 ?
|
|
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:
|
|
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
|
}
|
package/src/ai-chat/context.ts
CHANGED
|
@@ -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
|
}
|
package/src/ai-chat/shared.ts
CHANGED
|
@@ -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; }
|
package/src/ai-chat/types.ts
CHANGED
|
@@ -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;
|