@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
package/docs/ai-chat.mdx CHANGED
@@ -66,7 +66,8 @@ Give the shell a sized parent — it fills it.
66
66
  - `navOpen` / `panelOpen` are controllable. Reduced motion snaps: the drag itself still tracks the finger 1:1 (it is direct manipulation, not an animation), and only the settle at the end of it jumps rather than travels.
67
67
  - **A collapsible rail** — `sidebarCollapsed` publishes the state while the sidebar owns its width and transition. `sidebarWidth` and `collapsedSidebarWidth` are explicit column overrides only; omit them for a Bloom `Sidebar` so its FAB, insets and collapse animation determine the actual width. The shell does not clip that sidebar into a fixed 56px lane.
68
68
  - **`background` and `surface`** — `background` is any node, drawn above the shell's own background-full and below the sidebar, workspace and drawers; it is never interactive. The workspace paints background-full too, so pass `surface={false}` with it (that drops both) or the layer is covered exactly where the chat sits. When the layer belongs to the chat rather than the page, put it on `AiChatContainer` instead, which is the nearer slot and needs no opt-out.
69
- - **What the shell publishes** — `useAiChatShell()` returns the shell's state to any descendant: `compact`, `navCollapsed`, `sidebarCollapsed`, `hasNav` / `hasPanel`, `openNav` / `closeNav` / `openPanel`, and **`navPresented`**.
69
+ - **`scroll`** — what scrolls the page, `AppShell`'s contract. `container` (the default) fills the parent and the conversation scrolls inside the card. **`document`** (web) grows the page and lets the browser scroll it — the page's scrollbar, scroll restoration, the mobile address bar collapsing and `window.scrollTo` behave as on any page. See [Document scroll](#document-scroll). Native has no document and treats it as `container`.
70
+ - **What the shell publishes** — `useAiChatShell()` returns the shell's state to any descendant: `compact`, `navCollapsed`, `sidebarCollapsed`, `hasNav` / `hasPanel`, `documentScroll`, `openNav` / `closeNav` / `openPanel`, and **`navPresented`**.
70
71
 
71
72
  `navPresented` is the one to guard a nav's accessibility with. Below `lg` a closed drawer stays **mounted** — it is translated offscreen, not removed — so everything inside `mobileSidebar` is still focusable and still read aloud unless its own tree says otherwise. The shell marks its own wrapper `aria-hidden` and `pointerEvents="none"`, which handles the pointer but not the tab order of a host's controls; read `navPresented` and skip them while it is `false`.
72
73
 
@@ -104,17 +105,42 @@ The thread scrolls with a thin scrollbar, px 16 / pt 16, turns 12 apart, and is
104
105
  | card | unchanged: radius 24, background-secondary (or `surface={false}`), clipped; `background` still fills it under everything |
105
106
  | `children` | fill the card's full height — nothing is in flow above or below them |
106
107
  | 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
+ | footer block | `AgentThinking` + `composer`, absolute at the bottom, px 10 / pt 12 / pb 10, gap 10 — **no band of its own**: the transcript passes behind the composer and stays in sight around it |
108
109
  | 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
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.
111
+ Both blocks are `pointerEvents="box-none"`, so the transcript under their empty space still scrolls and takes clicks. The fade is decorative (hidden from assistive tech) and exists **only while turns are actually under the header**. Nothing is painted behind the composer: its own card is what covers the turns under it, as the conversation slides past on either side.
112
112
 
113
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
114
 
115
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
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.
117
+ **Limits.** The fade is background-secondary. With `surface={false}` there is no known colour to fade to, so the header floats **without** it. Over an opaque `background` layer the fade is the card surface, not the layer.
118
+
119
+ ### Document scroll
120
+
121
+ Under `<AiChatShell scroll="document">` (web) the **document** is the scroller, the way every framed Oxy web page scrolls. The chat keeps every part; what moves is the page.
122
+
123
+ | Part | Geometry |
124
+ |---|---|
125
+ | shell | at least one screen (`100dvh`) and grows with the chat; `overflow: clip`, never `hidden`, so it is not a scroll container |
126
+ | sidebar, panel | sticky rails, `top: 12`, one screen tall less the gutter above and below |
127
+ | drawers, backdrop, shell `background` | `position: fixed`, so they cover the screen, not the page |
128
+ | card | grows with the conversation, one screen tall at the least; the chrome always floats (`floatingChrome` is implied) |
129
+ | header block | sticky in the flow at `top: 12`, with the same fade while turns run under it |
130
+ | footer block | sticky in the flow at `bottom: 12`, no band — the conversation passes behind the composer |
131
+ | frame | a layer the size of the visible card, sticky at the gutter: a ring of the shell's background-full over whatever scrolls past the card's edge, so the card keeps its rounded corners and its gutter at the top and bottom of the screen. Clipped sideways to 1px, so it never reaches the columns beside the card. Drawn only when the shell paints its `surface` |
132
+ | container `background` | the same screen-sized layer, under the turns: a wallpaper stays put while the conversation moves over it |
133
+ | thread | a plain view, no scroller: bottom-anchored, px 16 / pt 16, turns 12 apart |
134
+
135
+ The thread keeps its props and its handle. It follows through the window, opens at its newest turn (the page is shared with whatever route was on it before), fires `onStartReached` near the top and holds a landed page still. Offsets stay in **the thread's own frame**: `0` is the thread's top, so a row's `onLayout` y is still a `scrollToOffset` target, and `contentSize` runs to the **end of the page**, so `contentSize − offset − viewport` is still the distance left to scroll. `getScrollView()` returns `null`. The browser's scroll anchoring is off inside the thread — it would move the page for growth the thread already accounts for, and only in some browsers.
136
+
137
+ At rest the chrome is in the flow and covers nothing, so `useAiChatChromeInsets()` is `{ top: 0, bottom: 0 }`.
138
+
139
+ **The host's part.** Nothing between the shell and the document may be a scroll container:
140
+
141
+ - Expo / react-native-web inject `html, body { height: 100%; overflow: hidden }` and `#root { height: 100% }`. Override them with unlayered rules — `height: auto`, `min-height: 100%` (`100vh` on `#root`) and `overflow: visible` on **both** axes (one clipped axis promotes the other to `auto`).
142
+ - `expo-router`'s `Stack` renders each web screen absolutely positioned, which pins the page to one screen: on web, host the routes with `Slot`, or a `Navigator` that renders the focused screen in the flow.
143
+ - Every route shares the one window scroll, so a page that is not a chat should open at the top (or where the reader left it): `useScrollRestoration('window')` from [`scroll`](/docs/bloom/main/scroll).
118
144
 
119
145
  ## Turns
120
146