@oxy.so/bloom 4.7.0 → 4.9.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/adoption-matrix.mdx +2 -2
- package/docs/ai-chat.mdx +21 -0
- package/docs/settings-modal.mdx +1 -1
- package/lib/commonjs/ai-chat/AiChatContainer.js +384 -77
- package/lib/commonjs/ai-chat/AiChatContainer.js.map +1 -1
- package/lib/commonjs/ai-chat/context.js +28 -1
- package/lib/commonjs/ai-chat/context.js.map +1 -1
- package/lib/commonjs/ai-chat/index.js +6 -0
- package/lib/commonjs/ai-chat/index.js.map +1 -1
- package/lib/commonjs/ai-chat/index.web.js +6 -0
- package/lib/commonjs/ai-chat/index.web.js.map +1 -1
- package/lib/commonjs/settings-modal/SettingsStorage.js +1 -1
- package/lib/commonjs/settings-modal/SettingsStorage.js.map +1 -1
- package/lib/module/ai-chat/AiChatContainer.js +386 -79
- package/lib/module/ai-chat/AiChatContainer.js.map +1 -1
- package/lib/module/ai-chat/context.js +26 -0
- package/lib/module/ai-chat/context.js.map +1 -1
- package/lib/module/ai-chat/index.js +1 -1
- package/lib/module/ai-chat/index.js.map +1 -1
- package/lib/module/ai-chat/index.web.js +1 -1
- package/lib/module/ai-chat/index.web.js.map +1 -1
- package/lib/module/settings-modal/SettingsStorage.js +1 -1
- package/lib/module/settings-modal/SettingsStorage.js.map +1 -1
- package/lib/typescript/commonjs/ai-chat/AiChatContainer.d.ts +24 -1
- package/lib/typescript/commonjs/ai-chat/AiChatContainer.d.ts.map +1 -1
- package/lib/typescript/commonjs/ai-chat/context.d.ts +32 -0
- package/lib/typescript/commonjs/ai-chat/context.d.ts.map +1 -1
- package/lib/typescript/commonjs/ai-chat/index.d.ts +2 -2
- package/lib/typescript/commonjs/ai-chat/index.d.ts.map +1 -1
- package/lib/typescript/commonjs/ai-chat/index.web.d.ts +2 -2
- package/lib/typescript/commonjs/ai-chat/index.web.d.ts.map +1 -1
- package/lib/typescript/commonjs/ai-chat/types.d.ts +17 -0
- package/lib/typescript/commonjs/ai-chat/types.d.ts.map +1 -1
- package/lib/typescript/commonjs/settings-modal/SettingsStorage.d.ts.map +1 -1
- package/lib/typescript/commonjs/settings-modal/types.d.ts +2 -1
- package/lib/typescript/commonjs/settings-modal/types.d.ts.map +1 -1
- package/lib/typescript/module/ai-chat/AiChatContainer.d.ts +24 -1
- package/lib/typescript/module/ai-chat/AiChatContainer.d.ts.map +1 -1
- package/lib/typescript/module/ai-chat/context.d.ts +32 -0
- package/lib/typescript/module/ai-chat/context.d.ts.map +1 -1
- package/lib/typescript/module/ai-chat/index.d.ts +2 -2
- package/lib/typescript/module/ai-chat/index.d.ts.map +1 -1
- package/lib/typescript/module/ai-chat/index.web.d.ts +2 -2
- package/lib/typescript/module/ai-chat/index.web.d.ts.map +1 -1
- package/lib/typescript/module/ai-chat/types.d.ts +17 -0
- package/lib/typescript/module/ai-chat/types.d.ts.map +1 -1
- package/lib/typescript/module/settings-modal/SettingsStorage.d.ts.map +1 -1
- package/lib/typescript/module/settings-modal/types.d.ts +2 -1
- package/lib/typescript/module/settings-modal/types.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/ai-chat/AiChatContainer.tsx +343 -59
- package/src/ai-chat/context.ts +39 -0
- package/src/ai-chat/index.ts +2 -2
- package/src/ai-chat/index.web.ts +2 -2
- package/src/ai-chat/types.ts +17 -0
- package/src/settings-modal/SettingsStorage.tsx +6 -4
- package/src/settings-modal/types.ts +2 -1
package/docs/adoption-matrix.mdx
CHANGED
|
@@ -278,7 +278,7 @@ The classification is an EQUALITY in `src/__tests__/adoption-matrix.test.ts`, no
|
|
|
278
278
|
| `segmented-control` | `@oxy.so/bloom/segmented-control` + barrel | universal | `segmented-control.mdx` | yes | 4 | reads | | | |
|
|
279
279
|
| `select` | `@oxy.so/bloom/select` + barrel | web fork | `select.mdx` | yes | 7 | reads | | reads | |
|
|
280
280
|
| `settings-list` | `@oxy.so/bloom/settings-list` + barrel | universal | `settings-list.mdx` | yes | 5 | | | | |
|
|
281
|
-
| `settings-modal` | `@oxy.so/bloom/settings-modal` + barrel | universal | `settings-modal.mdx` | yes |
|
|
281
|
+
| `settings-modal` | `@oxy.so/bloom/settings-modal` + barrel | universal | `settings-modal.mdx` | yes | 3 | | | | reads |
|
|
282
282
|
| `shipment-request` | `@oxy.so/bloom/shipment-request` + barrel | universal | `shipment-request.mdx` | yes | 1 | **adaptation** | | | |
|
|
283
283
|
| `sidebar` | `@oxy.so/bloom/sidebar` + barrel | universal | `sidebar.mdx` | yes | 6 | n/a¹ | | | reads |
|
|
284
284
|
| `skeleton` | `@oxy.so/bloom/skeleton` + barrel | universal | `skeleton.mdx` | yes | 1 | | | | |
|
|
@@ -304,7 +304,7 @@ The classification is an EQUALITY in `src/__tests__/adoption-matrix.test.ts`, no
|
|
|
304
304
|
| `tenancy` | `@oxy.so/bloom/tenancy` + barrel | universal | `tenancy.mdx` | yes | 3 | | n/a¹ | | |
|
|
305
305
|
| `text-field` | `@oxy.so/bloom/text-field` + barrel | universal | `text-field.mdx` | yes | 13 | reads | reads | | |
|
|
306
306
|
| `textarea` | `@oxy.so/bloom/textarea` + barrel | universal | `textarea.mdx` | yes | 4 | reads | reads | | n/a¹ |
|
|
307
|
-
| `theme` | `@oxy.so/bloom/theme` + barrel | web fork | `theme.mdx` | yes |
|
|
307
|
+
| `theme` | `@oxy.so/bloom/theme` + barrel | web fork | `theme.mdx` | yes | 295 | | | n/a¹ | reads |
|
|
308
308
|
| `theme-toggle` | `@oxy.so/bloom/theme-toggle` + barrel | universal | `theme-toggle.mdx` | yes | 1 | n/a¹ | | | |
|
|
309
309
|
| `toast` | `@oxy.so/bloom/toast` + barrel | universal | `toast.mdx` | yes | 17 | | | | |
|
|
310
310
|
| `tooltip` | `@oxy.so/bloom/tooltip` + barrel | web fork | `tooltip.mdx` | yes | 1 | | | | |
|
package/docs/ai-chat.mdx
CHANGED
|
@@ -95,6 +95,27 @@ The thread scrolls with a thin scrollbar, px 16 / pt 16, turns 12 apart, and is
|
|
|
95
95
|
|
|
96
96
|
**Hazard (web):** react-native-web gives every view `min-height: 0`, so the usual `flexGrow: 1` + `justifyContent: 'flex-end'` content container stays the viewport's height and its children overflow it instead of scrolling — and `onContentSizeChange` never sees the growth. The thread floors its container at `minHeight: '100%'` on web instead.
|
|
97
97
|
|
|
98
|
+
### Floating chrome
|
|
99
|
+
|
|
100
|
+
`<AiChatContainer floatingChrome>` keeps every part and changes the geometry: the conversation fills the **whole card** and scrolls **behind** the header and **behind** the composer. Use it when the transcript is the screen — a full-height chat where two opaque bands would each take a strip of it, or where a `background` layer should run edge to edge under everything. Leave it off (the default, and what the AI Chat template uses) for the stacked layout above.
|
|
101
|
+
|
|
102
|
+
| Layer | Geometry |
|
|
103
|
+
|---|---|
|
|
104
|
+
| card | unchanged: radius 24, background-secondary (or `surface={false}`), clipped; `background` still fills it under everything |
|
|
105
|
+
| `children` | fill the card's full height — nothing is in flow above or below them |
|
|
106
|
+
| header block | the `header` slot (`AiChatMobileHeader`) + the breadcrumb row, absolute at the top, transparent — no band of its own |
|
|
107
|
+
| footer block | `AgentThinking` + `composer`, absolute at the bottom; pt **24** (instead of 12), px 10 / pb 10, gap 10 |
|
|
108
|
+
| top fade | the page-header edge scrim in the card surface, opaque at the top edge, ending 55% of the header block's height below it; fades in over the first **16px** scrolled (the gap it closes) |
|
|
109
|
+
| bottom fade | a 24px scrim ramp across the footer's top padding, then solid surface behind the composer; fades in over the first **24px** of transcript below the fold |
|
|
110
|
+
|
|
111
|
+
Both blocks are `pointerEvents="box-none"`, so the transcript under their empty space still scrolls and takes clicks. Both fades are decorative (hidden from assistive tech) and exist **only while turns are actually under that edge** — at rest, with the thread followed to the end, the bottom is clear and a `background` layer shows untinted.
|
|
112
|
+
|
|
113
|
+
**Insets.** The container measures both blocks (`onLayout`) and hands the heights to its children through context. An `AiChatThread` pads its **content** by them — pt = header block + 16, pb = footer block — and sets matching iOS `scrollIndicatorInsets`, so at rest the first and last turns sit exactly where the stacked layout puts them relative to the chrome, and in motion pass under it. The `ScrollView` stays the only scroller; `autoFollow`, `followThreshold`, `onStartReached`, `maintainStartPosition` and the ref work unchanged. A resize that is **exactly** the chrome's (the composer growing, `AgentThinking` appearing, the header measuring in on mount) is not a new turn: a reader at the end stays at the end without an animated follow, and a reader mid-thread keeps their turn in place. Any other child reads the same numbers from `useAiChatChromeInsets()` (`{ top, bottom }`, or `null` outside a floating container) to keep an empty state or a scroller of its own clear of the chrome.
|
|
114
|
+
|
|
115
|
+
**Why a fade, not a frost.** `AgentChat` frosts its overlaid header (white/black 20%, a 20px backdrop blur on web) once the transcript reaches it. Here the chrome has no band, and a frosted rectangle is one: it ends at its own bottom edge, and that edge is a line across the transcript. A one-colour opacity ramp is identical on native and web and needs no blur.
|
|
116
|
+
|
|
117
|
+
**Limits.** The fades are background-secondary. With `surface={false}` there is no known colour to fade to, so the chrome floats **without** fades — the composer's own surface is then what covers the text under it. Over an opaque `background` layer the fades are the card surface, not the layer.
|
|
118
|
+
|
|
98
119
|
## Turns
|
|
99
120
|
|
|
100
121
|
- **User** — pushed right with a 6px bleed past the column, as wide as its text up to half the column + 6; radius 16, background-primary, px 12 / py 11, shadow-card.
|
package/docs/settings-modal.mdx
CHANGED
|
@@ -108,7 +108,7 @@ Menus, selects, tooltips and the date popover opened inside a page stack above i
|
|
|
108
108
|
- **`SettingsGeneralPage`** — `plan` (`SettingsPlanCard`) then `sections`. The plan card's artwork is `SettingsPlanArt`: on web, a WebGL "burning flag" shader over `artworkSource` (or Bloom's generated accent/success/warning discs), eased toward the pointer; native, reduced motion and a failed WebGL context get the static discs. A radial fade melts it into the card.
|
|
109
109
|
- **`SettingsProfilePage`** — cards of rows.
|
|
110
110
|
- **`SettingsToolsPage`** — scope pills (sliding thumb), the authentication switch, the scope's servers (`SettingsServerList`), Team servers (empty state when none) and plugin servers. Server tiles take a theme `tone`; the "…" menu's actions and callbacks are props.
|
|
111
|
-
- **`SettingsStoragePage`** — Bloom's `FileUpload` dropzone (`upload` props) over the file table: type/order selects, search, sortable columns, row selection, delete (rows collapse out before `onDeleteFile`), the "⋮" menu and pagination. Files that appear in `files` grow in from under the header.
|
|
111
|
+
- **`SettingsStoragePage`** — Bloom's `FileUpload` dropzone (`upload` props; `upload={false}` for a read-only store) over the file table: type/order selects, search, sortable columns, row selection, delete (rows collapse out before `onDeleteFile`), the "⋮" menu and pagination. Files that appear in `files` grow in from under the header.
|
|
112
112
|
|
|
113
113
|
## Colours
|
|
114
114
|
|
|
@@ -7,12 +7,17 @@ exports.AiChatContainer = AiChatContainer;
|
|
|
7
7
|
exports.AiChatThread = void 0;
|
|
8
8
|
var _react = _interopRequireWildcard(require("react"));
|
|
9
9
|
var _reactNative = require("react-native");
|
|
10
|
+
var _reactNativeReanimated = _interopRequireWildcard(require("react-native-reanimated"));
|
|
10
11
|
var _index = require("../agent-thinking/index.js");
|
|
11
12
|
var _index2 = require("../breadcrumb/index.js");
|
|
12
13
|
var _RiFolderLine = require("../icons/remix/RiFolderLine.js");
|
|
13
14
|
var _RiMoreFill = require("../icons/remix/RiMoreFill.js");
|
|
14
15
|
var _RiShare2Line = require("../icons/remix/RiShare2Line.js");
|
|
16
|
+
var _EdgeScrim = require("../page-header/EdgeScrim.js");
|
|
17
|
+
var _colorUtils = require("../theme/color-utils.js");
|
|
18
|
+
var _useIsomorphicLayoutEffect = require("../theme/use-isomorphic-layout-effect.js");
|
|
15
19
|
var _AiChatControls = require("./AiChatControls.js");
|
|
20
|
+
var _context = require("./context.js");
|
|
16
21
|
var _shared = require("./shared.js");
|
|
17
22
|
var _jsxRuntime = require("react/jsx-runtime");
|
|
18
23
|
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); }
|
|
@@ -22,6 +27,22 @@ const DEFAULT_LABELS = {
|
|
|
22
27
|
more: 'More options'
|
|
23
28
|
};
|
|
24
29
|
|
|
30
|
+
/** The thread's own top padding: the gap between the header and the first turn. */
|
|
31
|
+
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;
|
|
38
|
+
/** Below this many px of overlap an edge counts as clear (sub-pixel scroll noise). */
|
|
39
|
+
const EDGE_THRESHOLD = 1;
|
|
40
|
+
const HIDDEN_FROM_A11Y = {
|
|
41
|
+
'aria-hidden': true,
|
|
42
|
+
accessibilityElementsHidden: true,
|
|
43
|
+
importantForAccessibility: 'no-hide-descendants'
|
|
44
|
+
};
|
|
45
|
+
|
|
25
46
|
/**
|
|
26
47
|
* The AI chat's centre column: the centre column of the AI chat template.
|
|
27
48
|
*
|
|
@@ -39,6 +60,22 @@ const DEFAULT_LABELS = {
|
|
|
39
60
|
*
|
|
40
61
|
* `project` is optional: without one the breadcrumb is the chat's own crumb
|
|
41
62
|
* alone, since a chat that belongs to nothing must not have to invent a folder.
|
|
63
|
+
*
|
|
64
|
+
* `floatingChrome` changes the geometry, not the parts:
|
|
65
|
+
*
|
|
66
|
+
* thread the `children` fill the whole card, under everything below
|
|
67
|
+
* header the `header` slot + breadcrumb row, absolute at the top, no band;
|
|
68
|
+
* an edge scrim of the surface (opaque at the top, gone
|
|
69
|
+
* `SCRIM_TAIL_RATIO` below the block) fades in over the first 16px
|
|
70
|
+
* 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
|
|
74
|
+
*
|
|
75
|
+
* Both blocks are `pointerEvents="box-none"`, so the transcript under their
|
|
76
|
+
* empty space still scrolls and takes clicks. The measured heights reach an
|
|
77
|
+
* `AiChatThread` child through context (its content padding) and any other
|
|
78
|
+
* child through `useAiChatChromeInsets()`.
|
|
42
79
|
*/
|
|
43
80
|
function AiChatContainer({
|
|
44
81
|
project,
|
|
@@ -55,6 +92,7 @@ function AiChatContainer({
|
|
|
55
92
|
workingLabel,
|
|
56
93
|
background,
|
|
57
94
|
surface = true,
|
|
95
|
+
floatingChrome = false,
|
|
58
96
|
labels,
|
|
59
97
|
style,
|
|
60
98
|
testID
|
|
@@ -65,76 +103,104 @@ function AiChatContainer({
|
|
|
65
103
|
...DEFAULT_LABELS,
|
|
66
104
|
...labels
|
|
67
105
|
}), [labels]);
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
106
|
+
const hasCrumbRow = title != null || project != null || onShare || onMore || actions != null;
|
|
107
|
+
const crumbRow = hasCrumbRow ? /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
|
|
108
|
+
pointerEvents: "box-none",
|
|
109
|
+
style: {
|
|
110
|
+
width: '100%',
|
|
111
|
+
flexDirection: 'row',
|
|
112
|
+
alignItems: 'center',
|
|
113
|
+
justifyContent: 'space-between',
|
|
114
|
+
gap: 8,
|
|
115
|
+
paddingLeft: 16,
|
|
116
|
+
paddingRight: 16,
|
|
117
|
+
paddingTop: 16
|
|
118
|
+
},
|
|
119
|
+
children: [/*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
|
|
120
|
+
pointerEvents: "box-none",
|
|
81
121
|
style: {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
left: 0,
|
|
85
|
-
right: 0,
|
|
86
|
-
bottom: 0
|
|
122
|
+
minWidth: 0,
|
|
123
|
+
flex: 1
|
|
87
124
|
},
|
|
88
|
-
children:
|
|
89
|
-
|
|
125
|
+
children: title != null || project != null ? /*#__PURE__*/(0, _jsxRuntime.jsxs)(_index2.Breadcrumb, {
|
|
126
|
+
accessibilityLabel: l.breadcrumb,
|
|
127
|
+
children: [project === undefined ? null : /*#__PURE__*/(0, _jsxRuntime.jsx)(_index2.BreadcrumbItem, {
|
|
128
|
+
icon: projectIcon,
|
|
129
|
+
onPress: onProjectPress,
|
|
130
|
+
children: project
|
|
131
|
+
}), title == null ? null : /*#__PURE__*/(0, _jsxRuntime.jsx)(_index2.BreadcrumbItem, {
|
|
132
|
+
current: true,
|
|
133
|
+
children: title
|
|
134
|
+
})]
|
|
135
|
+
}) : null
|
|
136
|
+
}), /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
|
|
90
137
|
style: {
|
|
91
|
-
|
|
138
|
+
flexShrink: 0,
|
|
92
139
|
flexDirection: 'row',
|
|
93
140
|
alignItems: 'center',
|
|
94
|
-
|
|
95
|
-
gap: 8,
|
|
96
|
-
paddingLeft: 16,
|
|
97
|
-
paddingRight: 16,
|
|
98
|
-
paddingTop: 16
|
|
141
|
+
gap: 8
|
|
99
142
|
},
|
|
100
|
-
children:
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
143
|
+
children: actions ?? /*#__PURE__*/(0, _jsxRuntime.jsxs)(_jsxRuntime.Fragment, {
|
|
144
|
+
children: [onShare ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_AiChatControls.GlyphAction, {
|
|
145
|
+
icon: _RiShare2Line.RiShare2Line,
|
|
146
|
+
label: l.share,
|
|
147
|
+
onPress: onShare,
|
|
148
|
+
palette: palette
|
|
149
|
+
}) : null, onMore ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_AiChatControls.GlyphAction, {
|
|
150
|
+
icon: _RiMoreFill.RiMoreFill,
|
|
151
|
+
label: l.more,
|
|
152
|
+
onPress: onMore,
|
|
153
|
+
palette: palette
|
|
154
|
+
}) : null]
|
|
155
|
+
})
|
|
156
|
+
})]
|
|
157
|
+
}) : null;
|
|
158
|
+
const footerBody = /*#__PURE__*/(0, _jsxRuntime.jsxs)(_jsxRuntime.Fragment, {
|
|
159
|
+
children: [working ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_index.AgentThinking, {
|
|
160
|
+
variant: "infinity",
|
|
161
|
+
label: workingLabel,
|
|
162
|
+
style: {
|
|
163
|
+
paddingLeft: 6,
|
|
164
|
+
paddingRight: 6
|
|
165
|
+
}
|
|
166
|
+
}) : null, composer]
|
|
167
|
+
});
|
|
168
|
+
const backgroundLayer = background ? /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
|
|
169
|
+
pointerEvents: "none",
|
|
170
|
+
style: {
|
|
171
|
+
position: 'absolute',
|
|
172
|
+
top: 0,
|
|
173
|
+
left: 0,
|
|
174
|
+
right: 0,
|
|
175
|
+
bottom: 0
|
|
176
|
+
},
|
|
177
|
+
children: background
|
|
178
|
+
}) : null;
|
|
179
|
+
const rootStyle = [{
|
|
180
|
+
height: '100%',
|
|
181
|
+
minWidth: 0,
|
|
182
|
+
flex: 1,
|
|
183
|
+
flexDirection: 'column',
|
|
184
|
+
overflow: 'hidden',
|
|
185
|
+
borderRadius: _shared.CONTAINER_RADIUS,
|
|
186
|
+
backgroundColor: surface ? palette.secondary : 'transparent'
|
|
187
|
+
}, style];
|
|
188
|
+
if (floatingChrome) {
|
|
189
|
+
return /*#__PURE__*/(0, _jsxRuntime.jsx)(FloatingChrome, {
|
|
190
|
+
testID: testID,
|
|
191
|
+
rootStyle: rootStyle,
|
|
192
|
+
backgroundLayer: backgroundLayer,
|
|
193
|
+
header: header,
|
|
194
|
+
crumbRow: crumbRow,
|
|
195
|
+
footerBody: footerBody,
|
|
196
|
+
fadeColor: surface ? palette.secondary : null,
|
|
197
|
+
children: children
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
return /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNative.View, {
|
|
201
|
+
testID: testID,
|
|
202
|
+
style: rootStyle,
|
|
203
|
+
children: [backgroundLayer, header, crumbRow, children, /*#__PURE__*/(0, _jsxRuntime.jsx)(_reactNative.View, {
|
|
138
204
|
style: {
|
|
139
205
|
width: '100%',
|
|
140
206
|
flexDirection: 'column',
|
|
@@ -144,14 +210,171 @@ function AiChatContainer({
|
|
|
144
210
|
paddingTop: 12,
|
|
145
211
|
paddingBottom: 10
|
|
146
212
|
},
|
|
147
|
-
children:
|
|
148
|
-
|
|
149
|
-
|
|
213
|
+
children: footerBody
|
|
214
|
+
})]
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
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.
|
|
221
|
+
*
|
|
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
|
|
225
|
+
* sidebar's `SidebarScrollArea`, whose scrim this reuses.
|
|
226
|
+
*/
|
|
227
|
+
function FloatingChrome({
|
|
228
|
+
testID,
|
|
229
|
+
rootStyle,
|
|
230
|
+
backgroundLayer,
|
|
231
|
+
header,
|
|
232
|
+
crumbRow,
|
|
233
|
+
footerBody,
|
|
234
|
+
fadeColor,
|
|
235
|
+
children
|
|
236
|
+
}) {
|
|
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
|
+
});
|
|
245
|
+
const topFade = (0, _reactNativeReanimated.useSharedValue)(0);
|
|
246
|
+
const bottomFade = (0, _reactNativeReanimated.useSharedValue)(0);
|
|
247
|
+
const onHeaderLayout = (0, _react.useCallback)(event => {
|
|
248
|
+
const top = Math.round(event.nativeEvent.layout.height);
|
|
249
|
+
setInsets(previous => previous.top === top ? previous : {
|
|
250
|
+
...previous,
|
|
251
|
+
top
|
|
252
|
+
});
|
|
253
|
+
}, []);
|
|
254
|
+
const onFooterLayout = (0, _react.useCallback)(event => {
|
|
255
|
+
const bottom = Math.round(event.nativeEvent.layout.height);
|
|
256
|
+
setInsets(previous => previous.bottom === bottom ? previous : {
|
|
257
|
+
...previous,
|
|
258
|
+
bottom
|
|
259
|
+
});
|
|
260
|
+
}, []);
|
|
261
|
+
const reportScroll = (0, _react.useCallback)(({
|
|
262
|
+
offset,
|
|
263
|
+
viewport,
|
|
264
|
+
content
|
|
265
|
+
}) => {
|
|
266
|
+
const range = Math.max(0, content - viewport);
|
|
267
|
+
const y = Math.max(0, Math.min(offset, range));
|
|
268
|
+
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;
|
|
274
|
+
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]);
|
|
281
|
+
const chrome = (0, _react.useMemo)(() => ({
|
|
282
|
+
insets,
|
|
283
|
+
reportScroll
|
|
284
|
+
}), [insets, reportScroll]);
|
|
285
|
+
|
|
286
|
+
// SVG never receives alpha inside stopColor; it travels on the containing view.
|
|
287
|
+
const parsed = fadeColor ? (0, _colorUtils.parseRgba)(fadeColor) : null;
|
|
288
|
+
const solid = parsed ? `rgb(${parsed.r}, ${parsed.g}, ${parsed.b})` : fadeColor;
|
|
289
|
+
const alpha = parsed?.a ?? 1;
|
|
290
|
+
const topStyle = (0, _reactNativeReanimated.useAnimatedStyle)(() => ({
|
|
291
|
+
opacity: topFade.value * alpha
|
|
292
|
+
}), [topFade, alpha]);
|
|
293
|
+
const bottomStyle = (0, _reactNativeReanimated.useAnimatedStyle)(() => ({
|
|
294
|
+
opacity: bottomFade.value * alpha
|
|
295
|
+
}), [bottomFade, alpha]);
|
|
296
|
+
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: {
|
|
310
|
+
position: 'absolute',
|
|
311
|
+
top: 0,
|
|
312
|
+
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`,
|
|
334
|
+
pointerEvents: "box-none",
|
|
335
|
+
onLayout: onFooterLayout,
|
|
336
|
+
style: {
|
|
337
|
+
position: 'absolute',
|
|
338
|
+
left: 0,
|
|
339
|
+
right: 0,
|
|
340
|
+
bottom: 0
|
|
341
|
+
},
|
|
342
|
+
children: [solid && edges.bottom ? /*#__PURE__*/(0, _jsxRuntime.jsxs)(_reactNativeReanimated.default.View, {
|
|
343
|
+
testID: `${prefix}-fade-bottom`,
|
|
344
|
+
pointerEvents: "none",
|
|
345
|
+
...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",
|
|
150
367
|
style: {
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
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
|
+
})]
|
|
155
378
|
})]
|
|
156
379
|
});
|
|
157
380
|
}
|
|
@@ -177,6 +400,13 @@ function AiChatContainer({
|
|
|
177
400
|
*
|
|
178
401
|
* There is no virtualization and no `scrollToIndex`: a host jumping to a turn
|
|
179
402
|
* measures its row (`onLayout`) and calls `scrollToOffset`.
|
|
403
|
+
*
|
|
404
|
+
* Inside `AiChatContainer floatingChrome` the thread still fills its parent —
|
|
405
|
+
* the whole card — and pads its CONTENT by the chrome the container measured:
|
|
406
|
+
* pt = header block + 16, pb = footer block. At rest the first and last turns
|
|
407
|
+
* sit exactly where the stacked layout puts them relative to the chrome; in
|
|
408
|
+
* motion they pass under it. It also reports its position so the container
|
|
409
|
+
* knows when to fade each edge. Anywhere else nothing of this exists.
|
|
180
410
|
*/
|
|
181
411
|
const AiChatThread = exports.AiChatThread = /*#__PURE__*/(0, _react.forwardRef)(function AiChatThread({
|
|
182
412
|
children,
|
|
@@ -191,14 +421,45 @@ const AiChatThread = exports.AiChatThread = /*#__PURE__*/(0, _react.forwardRef)(
|
|
|
191
421
|
style,
|
|
192
422
|
testID
|
|
193
423
|
}, ref) {
|
|
424
|
+
const chrome = (0, _react.useContext)(_context.AiChatFloatingChromeContext);
|
|
194
425
|
const scrollRef = (0, _react.useRef)(null);
|
|
195
426
|
const lastHeight = (0, _react.useRef)(0);
|
|
427
|
+
/**
|
|
428
|
+
* The latest content height from ANY source, for the chrome report only.
|
|
429
|
+
* `lastHeight` must change solely in `onContentSizeChange`: a scroll event
|
|
430
|
+
* carrying the new size first would otherwise hide the growth from the follow.
|
|
431
|
+
*/
|
|
432
|
+
const contentHeight = (0, _react.useRef)(0);
|
|
196
433
|
const offset = (0, _react.useRef)(0);
|
|
197
434
|
const viewport = (0, _react.useRef)(0);
|
|
198
435
|
/** An `onStartReached` is out and its page has not landed yet. */
|
|
199
436
|
const pageOut = (0, _react.useRef)(false);
|
|
200
437
|
/** The reader has left the top zone, so the next approach may fire again. */
|
|
201
438
|
const startArmed = (0, _react.useRef)(true);
|
|
439
|
+
|
|
440
|
+
// The chrome's padding changing is not a new turn: it is recorded here,
|
|
441
|
+
// before the resize it causes, so `onContentSizeChange` can tell the two apart.
|
|
442
|
+
const insetTop = chrome?.insets.top ?? 0;
|
|
443
|
+
const insetBottom = chrome?.insets.bottom ?? 0;
|
|
444
|
+
const appliedInsets = (0, _react.useRef)({
|
|
445
|
+
top: insetTop,
|
|
446
|
+
bottom: insetBottom
|
|
447
|
+
});
|
|
448
|
+
const insetShift = (0, _react.useRef)({
|
|
449
|
+
top: 0,
|
|
450
|
+
bottom: 0
|
|
451
|
+
});
|
|
452
|
+
(0, _useIsomorphicLayoutEffect.useIsomorphicLayoutEffect)(() => {
|
|
453
|
+
const applied = appliedInsets.current;
|
|
454
|
+
insetShift.current = {
|
|
455
|
+
top: insetShift.current.top + insetTop - applied.top,
|
|
456
|
+
bottom: insetShift.current.bottom + insetBottom - applied.bottom
|
|
457
|
+
};
|
|
458
|
+
appliedInsets.current = {
|
|
459
|
+
top: insetTop,
|
|
460
|
+
bottom: insetBottom
|
|
461
|
+
};
|
|
462
|
+
}, [insetTop, insetBottom]);
|
|
202
463
|
(0, _react.useImperativeHandle)(ref, () => ({
|
|
203
464
|
scrollToEnd: options => scrollRef.current?.scrollToEnd({
|
|
204
465
|
animated: options?.animated ?? true
|
|
@@ -212,14 +473,24 @@ const AiChatThread = exports.AiChatThread = /*#__PURE__*/(0, _react.forwardRef)(
|
|
|
212
473
|
}),
|
|
213
474
|
getScrollView: () => scrollRef.current
|
|
214
475
|
}), []);
|
|
215
|
-
const listening = !!onScroll || !!onStartReached || followThreshold !== undefined || maintainStartPosition;
|
|
476
|
+
const listening = !!chrome || !!onScroll || !!onStartReached || followThreshold !== undefined || maintainStartPosition;
|
|
477
|
+
const report = (0, _react.useCallback)(() => {
|
|
478
|
+
chrome?.reportScroll({
|
|
479
|
+
offset: offset.current,
|
|
480
|
+
viewport: viewport.current,
|
|
481
|
+
content: contentHeight.current
|
|
482
|
+
});
|
|
483
|
+
}, [chrome]);
|
|
216
484
|
const handleScroll = (0, _react.useCallback)(event => {
|
|
217
485
|
const {
|
|
218
486
|
contentOffset,
|
|
219
|
-
layoutMeasurement
|
|
487
|
+
layoutMeasurement,
|
|
488
|
+
contentSize
|
|
220
489
|
} = event.nativeEvent;
|
|
221
490
|
offset.current = contentOffset.y;
|
|
222
491
|
viewport.current = layoutMeasurement.height;
|
|
492
|
+
if (contentSize?.height) contentHeight.current = contentSize.height;
|
|
493
|
+
report();
|
|
223
494
|
if (onStartReached) {
|
|
224
495
|
if (contentOffset.y <= onStartReachedThreshold) {
|
|
225
496
|
if (startArmed.current) {
|
|
@@ -232,16 +503,45 @@ const AiChatThread = exports.AiChatThread = /*#__PURE__*/(0, _react.forwardRef)(
|
|
|
232
503
|
}
|
|
233
504
|
}
|
|
234
505
|
onScroll?.(event);
|
|
235
|
-
}, [onScroll, onStartReached, onStartReachedThreshold]);
|
|
506
|
+
}, [onScroll, onStartReached, onStartReachedThreshold, report]);
|
|
236
507
|
const onLayout = (0, _react.useCallback)(event => {
|
|
237
508
|
viewport.current = event.nativeEvent.layout.height;
|
|
238
|
-
|
|
509
|
+
report();
|
|
510
|
+
}, [report]);
|
|
239
511
|
const onContentSizeChange = (0, _react.useCallback)((_width, height) => {
|
|
240
512
|
const previous = lastHeight.current;
|
|
241
513
|
const grew = height > previous + 0.5;
|
|
242
514
|
const first = previous === 0;
|
|
243
515
|
lastHeight.current = height;
|
|
244
|
-
|
|
516
|
+
contentHeight.current = height;
|
|
517
|
+
const shift = insetShift.current;
|
|
518
|
+
insetShift.current = {
|
|
519
|
+
top: 0,
|
|
520
|
+
bottom: 0
|
|
521
|
+
};
|
|
522
|
+
report();
|
|
523
|
+
if (first) return;
|
|
524
|
+
|
|
525
|
+
// Exactly the chrome's padding changed (a composer growing, the header
|
|
526
|
+
// measuring in): hold the reader still instead of following. At the end
|
|
527
|
+
// they stay at the end, unanimated; mid-thread, a top shift is absorbed
|
|
528
|
+
// into the offset so the turn they are on does not move.
|
|
529
|
+
const chromeShift = shift.top + shift.bottom;
|
|
530
|
+
if (chromeShift !== 0 && Math.abs(height - previous - chromeShift) < 1) {
|
|
531
|
+
const fromBottom = previous - offset.current - viewport.current;
|
|
532
|
+
if (fromBottom <= EDGE_THRESHOLD) {
|
|
533
|
+
scrollRef.current?.scrollToEnd({
|
|
534
|
+
animated: false
|
|
535
|
+
});
|
|
536
|
+
} else if (offset.current > 0 && shift.top !== 0) {
|
|
537
|
+
scrollRef.current?.scrollTo({
|
|
538
|
+
y: offset.current + shift.top,
|
|
539
|
+
animated: false
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
return;
|
|
543
|
+
}
|
|
544
|
+
if (!grew) return;
|
|
245
545
|
|
|
246
546
|
// A page landing above the reader: keep the turn they are on where it is.
|
|
247
547
|
if (maintainStartPosition && pageOut.current) {
|
|
@@ -292,8 +592,15 @@ const AiChatThread = exports.AiChatThread = /*#__PURE__*/(0, _react.forwardRef)(
|
|
|
292
592
|
gap: 12,
|
|
293
593
|
paddingLeft: 16,
|
|
294
594
|
paddingRight: 16,
|
|
295
|
-
paddingTop:
|
|
595
|
+
paddingTop: THREAD_PAD_TOP + (chrome ? chrome.insets.top : 0),
|
|
596
|
+
...(chrome ? {
|
|
597
|
+
paddingBottom: chrome.insets.bottom
|
|
598
|
+
} : null)
|
|
296
599
|
},
|
|
600
|
+
scrollIndicatorInsets: chrome ? {
|
|
601
|
+
top: chrome.insets.top,
|
|
602
|
+
bottom: chrome.insets.bottom
|
|
603
|
+
} : undefined,
|
|
297
604
|
children: children
|
|
298
605
|
});
|
|
299
606
|
});
|