@agentforge-io/chat-sdk 2.3.1 → 2.4.0-dev.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.
@@ -1,60 +1,40 @@
1
1
  /**
2
- * `<ChatDrawer>` — standard bottom-sheet wrapper around `<ChatWidget>`.
2
+ * `<ChatDrawer>` — standard mobile chat shell.
3
3
  *
4
- * The host gives us:
5
- * - `open` / `onOpenChange`: controlled visibility (host owns the route /
6
- * URL sync, the SDK doesn't touch the URL).
7
- * - All the `<ChatWidget>` props (token, apiBaseUrl, etc.): forwarded
8
- * verbatim. The widget mounts INSIDE the drawer.
4
+ * Mobile-first fullscreen modal that wraps `<ChatWidget>` and gets the
5
+ * three pieces of mobile UX every embed needs right:
9
6
  *
10
- * What the drawer adds on top:
11
- * - Mounts via React portal (`document.body`) so it overlays the page
12
- * regardless of the host's stacking context. Lazy-mounted: the portal
13
- * target is computed at first open so SSR stays clean.
14
- * - Visually pinned to the bottom of the *visual viewport* (not the
15
- * layout viewport). On iOS Safari and Android Chrome the on-screen
16
- * keyboard shrinks `window.visualViewport.height`; we listen to
17
- * `resize` / `scroll` on visualViewport and reflow the drawer's
18
- * `height` + `bottom` so the composer never gets clipped by the
19
- * keyboard.
20
- * - Opens at a configurable snap fraction (default 0.98 = 98% of the
21
- * visible viewport). The visitor sees a thin sliver of the page
22
- * underneath, which keeps context and lets a tap-outside dismiss.
23
- * - Drag-to-dismiss with vanilla touch events: drag the handle down
24
- * past 30% of the panel height and the drawer closes. No external
25
- * deps — the SDK stays portable.
26
- * - Survives close+reopen: the widget inside is rendered once and kept
27
- * alive (CSS `display:none` toggle when closed, NOT unmounted), so
28
- * the chat session, transcript, and any in-flight tool calls stay
29
- * intact when the visitor closes and reopens the drawer.
7
+ * 1. **Sticky header** at the top (back button + agent identity).
8
+ * 2. **Scrollable transcript** in the middle (the SDK panel +
9
+ * its own scroller).
10
+ * 3. **Sticky composer** at the bottom that NEVER hides under
11
+ * the on-screen keyboard.
30
12
  *
31
- * What the drawer does NOT do (deliberately):
32
- * - It doesn't sync with the URL. The host decides whether `?view=chat`,
33
- * `/chat`, or any other route shape opens it.
34
- * - It doesn't manage the chat session lifecycle. That's `<ChatWidget>`'s
35
- * job. We just provide presentation.
36
- * - It doesn't render a "fake composer" to trigger opening. The host
37
- * decides what trigger UX makes sense (button, input pill, FAB, etc.)
38
- * and calls `onOpenChange(true)`.
13
+ * Why Vaul: implementing the keyboard-aware sticky composer with
14
+ * vanilla `visualViewport` listeners is doable in 50 lines but the
15
+ * real-world iOS Safari / Chrome Android edge cases (rubber-band
16
+ * scroll, address-bar transitions, autocorrect bar over the keyboard,
17
+ * focus loss on send button disable) are not. Vaul handles all of
18
+ * them (used by Linear, Vercel, etc.). It's declared as an OPTIONAL
19
+ * peerDependency so the SDK stays portable if a host doesn't install
20
+ * Vaul, `<ChatDrawer>` falls back to a render that asks the host to
21
+ * install it. Most hosts already have Vaul (Radix design system).
22
+ *
23
+ * Why fullscreen (not snap points): the operator decision was that a
24
+ * conversation should occupy the whole device. No drag-to-dismiss,
25
+ * no peek of the page behind. Close via the explicit back button in
26
+ * the header. This is the "modal" feel — predictable, no accidental
27
+ * dismissals from a fast scroll.
28
+ *
29
+ * Why we still don't manage the chat session: the SDK's `ChatWidget`
30
+ * owns that, this component is purely the surround.
39
31
  */
40
32
  import { type CSSProperties, type ReactNode } from 'react';
41
33
  import { type ChatWidgetProps } from './react';
42
34
  /**
43
- * Drawer accepts the chat surface in two shapes:
44
- *
45
- * - `widgetProps`: pass the ChatWidget configuration and the drawer
46
- * creates the widget internally. The standard / forward-looking
47
- * API — most consumers should use this.
48
- *
49
- * - `chatSlot`: pass a pre-rendered React node (typically a
50
- * `<ChatWidget>` instance the host wired up itself). Useful when
51
- * the host already orchestrates the widget (multiple chat slots,
52
- * custom decorators, legacy code) and just wants the drawer's
53
- * positioning + drag UX on top.
54
- *
55
- * Exactly one of the two MUST be passed. Mutual exclusivity isn't
56
- * encoded at the type level (TS unions with optional props get noisy)
57
- * — the runtime asserts gracefully if neither is present.
35
+ * Two intake shapes either drop in a pre-built `<ChatWidget>` as
36
+ * `chatSlot` (most hosts) or hand us the widget props and the drawer
37
+ * mounts the widget itself.
58
38
  */
59
39
  type ChatSurface = {
60
40
  widgetProps: ChatWidgetProps;
@@ -64,39 +44,24 @@ type ChatSurface = {
64
44
  widgetProps?: never;
65
45
  };
66
46
  export type ChatDrawerProps = ChatSurface & {
67
- /** Whether the drawer is visible. Controlled. */
47
+ /** Controlled visibility. */
68
48
  open: boolean;
69
- /** Fired when the drawer wants to close (drag-to-dismiss, tap on the
70
- * backdrop, close button). Host is responsible for setting `open=false`. */
49
+ /** Fired when the drawer wants to close (back button, ESC). The
50
+ * host is responsible for setting `open=false`. */
71
51
  onOpenChange: (open: boolean) => void;
72
- /** Snap fraction of the visible viewport, 0–1. Defaults to 0.98 (98% =
73
- * the standard "near-fullscreen drawer that still hints at the page
74
- * below" pattern). Lower numbers leave more of the page visible. */
75
- snap?: number;
76
- /** Display in the drawer header above the chat. Templates pass the
77
- * agent / team identity here. When omitted, the drawer renders a
78
- * bare drag handle only — useful for hosts that want a borderless
79
- * panel. */
52
+ /** Sticky header above the chat panel. Templates pass the agent /
53
+ * team identity card here. Optional when omitted the drawer
54
+ * renders just the close button. */
80
55
  header?: ReactNode;
81
- /** Backdrop click closes the drawer. Default true set false if the
82
- * host wants a "modal" feel where the only escape is the close button
83
- * or the drag-down gesture. */
84
- closeOnBackdropClick?: boolean;
85
- /** Extra class on the drawer's root surface (the white card). Use it to
86
- * add a custom shadow / border colour. The CSS vars on `--af-*` already
87
- * let you re-theme the chat widget itself; this is for the SURROUND. */
56
+ /** Custom close button. Defaults to a chevron-left back button at
57
+ * the left edge of the header. */
58
+ closeButton?: ReactNode;
59
+ /** Extra class on the drawer surface (the full-height card). */
88
60
  drawerClassName?: string;
89
- /** Inline style applied to the drawer surface — typically the host's
90
- * bundle of `--af-bg`, `--af-fg`, `--af-bubble-*`, etc. CSS vars.
91
- * Because the drawer renders into a portal (`document.body`), any
92
- * vars declared on the host's page wrapper DON'T cascade in. The
93
- * host re-declares them here so the chat surface inside the portal
94
- * picks up the same theme.
95
- *
96
- * We default the SURFACE BG to `var(--af-bg, …)` so passing
97
- * `--af-bg` in `surfaceStyle` themes the drawer's background.
98
- * When omitted the drawer falls back to white in light mode and
99
- * the system default elsewhere — passable but not theme-perfect. */
61
+ /** CSS variables (`--af-bg`, `--af-fg`, `--af-bubble-*`, etc.) for
62
+ * the drawer surface. Because the drawer renders into a portal,
63
+ * the host's page-wrapper vars don't cascade in — re-declare them
64
+ * here so the chat surface theme matches. */
100
65
  surfaceStyle?: CSSProperties & Record<string, string>;
101
66
  };
102
67
  export declare function ChatDrawer(props: ChatDrawerProps): JSX.Element | null;
@@ -3,136 +3,59 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.ChatDrawer = ChatDrawer;
4
4
  const jsx_runtime_1 = require("react/jsx-runtime");
5
5
  /**
6
- * `<ChatDrawer>` — standard bottom-sheet wrapper around `<ChatWidget>`.
6
+ * `<ChatDrawer>` — standard mobile chat shell.
7
7
  *
8
- * The host gives us:
9
- * - `open` / `onOpenChange`: controlled visibility (host owns the route /
10
- * URL sync, the SDK doesn't touch the URL).
11
- * - All the `<ChatWidget>` props (token, apiBaseUrl, etc.): forwarded
12
- * verbatim. The widget mounts INSIDE the drawer.
8
+ * Mobile-first fullscreen modal that wraps `<ChatWidget>` and gets the
9
+ * three pieces of mobile UX every embed needs right:
13
10
  *
14
- * What the drawer adds on top:
15
- * - Mounts via React portal (`document.body`) so it overlays the page
16
- * regardless of the host's stacking context. Lazy-mounted: the portal
17
- * target is computed at first open so SSR stays clean.
18
- * - Visually pinned to the bottom of the *visual viewport* (not the
19
- * layout viewport). On iOS Safari and Android Chrome the on-screen
20
- * keyboard shrinks `window.visualViewport.height`; we listen to
21
- * `resize` / `scroll` on visualViewport and reflow the drawer's
22
- * `height` + `bottom` so the composer never gets clipped by the
23
- * keyboard.
24
- * - Opens at a configurable snap fraction (default 0.98 = 98% of the
25
- * visible viewport). The visitor sees a thin sliver of the page
26
- * underneath, which keeps context and lets a tap-outside dismiss.
27
- * - Drag-to-dismiss with vanilla touch events: drag the handle down
28
- * past 30% of the panel height and the drawer closes. No external
29
- * deps — the SDK stays portable.
30
- * - Survives close+reopen: the widget inside is rendered once and kept
31
- * alive (CSS `display:none` toggle when closed, NOT unmounted), so
32
- * the chat session, transcript, and any in-flight tool calls stay
33
- * intact when the visitor closes and reopens the drawer.
11
+ * 1. **Sticky header** at the top (back button + agent identity).
12
+ * 2. **Scrollable transcript** in the middle (the SDK panel +
13
+ * its own scroller).
14
+ * 3. **Sticky composer** at the bottom that NEVER hides under
15
+ * the on-screen keyboard.
34
16
  *
35
- * What the drawer does NOT do (deliberately):
36
- * - It doesn't sync with the URL. The host decides whether `?view=chat`,
37
- * `/chat`, or any other route shape opens it.
38
- * - It doesn't manage the chat session lifecycle. That's `<ChatWidget>`'s
39
- * job. We just provide presentation.
40
- * - It doesn't render a "fake composer" to trigger opening. The host
41
- * decides what trigger UX makes sense (button, input pill, FAB, etc.)
42
- * and calls `onOpenChange(true)`.
17
+ * Why Vaul: implementing the keyboard-aware sticky composer with
18
+ * vanilla `visualViewport` listeners is doable in 50 lines but the
19
+ * real-world iOS Safari / Chrome Android edge cases (rubber-band
20
+ * scroll, address-bar transitions, autocorrect bar over the keyboard,
21
+ * focus loss on send button disable) are not. Vaul handles all of
22
+ * them (used by Linear, Vercel, etc.). It's declared as an OPTIONAL
23
+ * peerDependency so the SDK stays portable if a host doesn't install
24
+ * Vaul, `<ChatDrawer>` falls back to a render that asks the host to
25
+ * install it. Most hosts already have Vaul (Radix design system).
26
+ *
27
+ * Why fullscreen (not snap points): the operator decision was that a
28
+ * conversation should occupy the whole device. No drag-to-dismiss,
29
+ * no peek of the page behind. Close via the explicit back button in
30
+ * the header. This is the "modal" feel — predictable, no accidental
31
+ * dismissals from a fast scroll.
32
+ *
33
+ * Why we still don't manage the chat session: the SDK's `ChatWidget`
34
+ * owns that, this component is purely the surround.
43
35
  */
44
36
  const react_1 = require("react");
45
- const react_dom_1 = require("react-dom");
46
37
  const react_2 = require("./react");
38
+ // Lazy import of Vaul. We can't `import { Drawer } from 'vaul'` at
39
+ // the top of the file because vaul is an OPTIONAL peerDependency
40
+ // and the package shouldn't crash at import-time when the host
41
+ // hasn't installed it. We resolve at module evaluation but inside a
42
+ // try/catch so the missing-vaul case is just a runtime "please
43
+ // install vaul" warning instead of a build break.
44
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
45
+ let VaulDrawer = null;
46
+ try {
47
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
48
+ VaulDrawer = require('vaul').Drawer;
49
+ }
50
+ catch {
51
+ // vaul not installed — render a fallback in the component.
52
+ }
47
53
  function ChatDrawer(props) {
48
- const { open, onOpenChange, snap = 0.98, header, closeOnBackdropClick = true, drawerClassName, surfaceStyle: surfaceStyleProp, widgetProps, chatSlot, } = props;
49
- // We mount once and keep alive across close→reopen so the chat session
50
- // doesn't get destroyed. After the FIRST open the panel stays in the
51
- // DOM forever (toggled by `display:none`) `hasOpened` gates the
52
- // initial mount so SSR doesn't render an empty portal.
53
- const [hasOpened, setHasOpened] = (0, react_1.useState)(open);
54
- (0, react_1.useEffect)(() => {
55
- if (open)
56
- setHasOpened(true);
57
- }, [open]);
58
- // Visible viewport tracking. iOS Safari + Android Chrome shrink
59
- // `visualViewport.height` when the on-screen keyboard pops up; the
60
- // layout viewport stays the same. We pin the drawer's height + bottom
61
- // to the visual viewport so the composer is always above the keyboard.
62
- const [vv, setVv] = (0, react_1.useState)(() => ({
63
- h: typeof window === 'undefined' ? 0 : window.innerHeight,
64
- offsetTop: 0,
65
- }));
66
- (0, react_1.useEffect)(() => {
67
- if (typeof window === 'undefined')
68
- return;
69
- const view = window.visualViewport;
70
- if (!view)
71
- return;
72
- const update = () => setVv({ h: view.height, offsetTop: view.offsetTop });
73
- update();
74
- view.addEventListener('resize', update);
75
- view.addEventListener('scroll', update);
76
- return () => {
77
- view.removeEventListener('resize', update);
78
- view.removeEventListener('scroll', update);
79
- };
80
- }, []);
81
- // Snap-point height: % of the visible viewport. Recomputed when vv
82
- // changes so a keyboard popup keeps the drawer aligned.
83
- const drawerHeight = Math.max(0, Math.round(vv.h * Math.min(Math.max(snap, 0.1), 1)));
84
- // Drag-to-dismiss. Vanilla touch handlers — no library. We track
85
- // pointerdown on the handle, follow movement on pointermove, and
86
- // decide on pointerup whether the drag crossed the "dismiss" threshold
87
- // (30% of the panel height).
88
- const surfaceRef = (0, react_1.useRef)(null);
89
- const dragStateRef = (0, react_1.useRef)(null);
90
- const [dragOffset, setDragOffset] = (0, react_1.useState)(0);
91
- const onHandlePointerDown = (0, react_1.useCallback)((e) => {
92
- if (e.button !== 0 && e.pointerType !== 'touch')
93
- return;
94
- e.currentTarget.setPointerCapture?.(e.pointerId);
95
- dragStateRef.current = {
96
- startY: e.clientY,
97
- startTime: Date.now(),
98
- dragging: true,
99
- };
100
- }, []);
101
- const onHandlePointerMove = (0, react_1.useCallback)((e) => {
102
- const s = dragStateRef.current;
103
- if (!s?.dragging)
104
- return;
105
- const delta = Math.max(0, e.clientY - s.startY);
106
- setDragOffset(delta);
107
- }, []);
108
- const onHandlePointerUp = (0, react_1.useCallback)((e) => {
109
- const s = dragStateRef.current;
110
- if (!s?.dragging)
111
- return;
112
- dragStateRef.current = null;
113
- try {
114
- e.currentTarget.releasePointerCapture?.(e.pointerId);
115
- }
116
- catch {
117
- // Pointer was already released by some other code path; nothing
118
- // to do here. Releasing a non-captured pointer throws — swallow.
119
- }
120
- const delta = Math.max(0, e.clientY - s.startY);
121
- const dismissThreshold = drawerHeight * 0.3;
122
- const elapsed = Date.now() - s.startTime;
123
- // Dismiss on EITHER a long-drag (past the threshold) or a quick
124
- // flick (small distance but high velocity). Velocity unit is
125
- // px/ms; >0.5 is roughly the threshold iOS sheets use.
126
- const velocity = elapsed > 0 ? delta / elapsed : 0;
127
- if (delta > dismissThreshold || velocity > 0.5) {
128
- onOpenChange(false);
129
- }
130
- setDragOffset(0);
131
- }, [drawerHeight, onOpenChange]);
132
- // Lock body scroll while the drawer is open so the page underneath
133
- // doesn't move when the visitor scrolls inside the chat. Restored on
134
- // close. Necessary on iOS Safari where the rubber-band scroll bleeds
135
- // through to the body even with overflow:hidden on a child.
54
+ const { open, onOpenChange, header, closeButton, drawerClassName, surfaceStyle, widgetProps, chatSlot, } = props;
55
+ // Body scroll lock Vaul does this internally on open, but it
56
+ // also restores on unmount, which interacts badly with a portal
57
+ // re-mount when the drawer re-renders during a heavy parent
58
+ // update. Belt-and-braces.
136
59
  (0, react_1.useEffect)(() => {
137
60
  if (typeof document === 'undefined')
138
61
  return;
@@ -144,101 +67,90 @@ function ChatDrawer(props) {
144
67
  document.body.style.overflow = prev;
145
68
  };
146
69
  }, [open]);
147
- // Escape closes the drawer. Keyboard-friendly even when focus is in
148
- // the textarea preventDefault on the input itself swallows Escape
149
- // before it bubbles, so we use capture phase.
150
- (0, react_1.useEffect)(() => {
151
- if (!open)
152
- return;
153
- if (typeof window === 'undefined')
154
- return;
155
- const onKey = (e) => {
156
- if (e.key === 'Escape')
157
- onOpenChange(false);
158
- };
159
- window.addEventListener('keydown', onKey, true);
160
- return () => window.removeEventListener('keydown', onKey, true);
161
- }, [open, onOpenChange]);
162
- // Lazy portal target. We render to `document.body` so the drawer
163
- // overlays everything regardless of the consumer's stacking context.
164
- const [portalEl, setPortalEl] = (0, react_1.useState)(null);
165
- (0, react_1.useLayoutEffect)(() => {
166
- if (typeof document === 'undefined')
167
- return;
168
- setPortalEl(document.body);
169
- }, []);
170
- // Stable id so the drag handle's `aria-controls` can point at the
171
- // panel. Required for screen readers to announce the relationship.
172
- const panelId = (0, react_1.useId)();
173
- if (!portalEl)
174
- return null;
175
- if (!hasOpened)
70
+ if (!VaulDrawer) {
71
+ // Host hasn't installed vaul. We log a one-time warning and
72
+ // render nothing. Most hosts that hit this never wanted the
73
+ // drawer in the first place (they're using ChatWidget inline).
74
+ if (open && typeof console !== 'undefined') {
75
+ console.warn('[@agentforge-io/chat-sdk] <ChatDrawer> requires `vaul` to render. ' +
76
+ 'Install it with `npm install vaul` or `yarn add vaul`. ' +
77
+ 'The drawer is a no-op until vaul is available.');
78
+ }
176
79
  return null;
177
- // The translate3d ensures the drawer animates from below on first
178
- // open (initial transform = 100%, becomes 0% after the open prop
179
- // flips). When dragging we add the manual drag offset on top.
180
- const translateY = open ? `${dragOffset}px` : `${drawerHeight}px`;
181
- // Transition disabled WHILE dragging so the surface follows the
182
- // finger 1:1. Re-enabled at the end of the drag (delta back to 0
183
- // smoothly when below threshold).
184
- const isDragging = dragStateRef.current?.dragging === true;
185
- // Motion / sizing portion of the surface style, kept separate from
186
- // the host-supplied theming so a re-render driven by drag offset
187
- // doesn't smash the host's CSS vars.
188
- const surfaceMotionStyle = {
189
- height: `${drawerHeight}px`,
190
- transform: `translate3d(0, ${translateY}, 0)`,
191
- bottom: `${vv.offsetTop}px`,
192
- transition: isDragging ? 'none' : 'transform 240ms cubic-bezier(.32,.72,0,1)',
193
- willChange: 'transform',
194
- };
195
- return (0, react_dom_1.createPortal)((0, jsx_runtime_1.jsxs)("div", { className: "af-drawer-root", "data-state": open ? 'open' : 'closed', style: {
196
- position: 'fixed',
197
- inset: 0,
198
- zIndex: 2147483600,
199
- pointerEvents: open ? 'auto' : 'none',
200
- }, children: [(0, jsx_runtime_1.jsx)("div", { className: "af-drawer-backdrop", onClick: closeOnBackdropClick ? () => onOpenChange(false) : undefined, style: {
201
- position: 'absolute',
202
- inset: 0,
203
- backgroundColor: 'rgba(0, 0, 0, 0.4)',
204
- opacity: open ? 1 : 0,
205
- transition: 'opacity 200ms ease-out',
206
- }, "aria-hidden": true }), (0, jsx_runtime_1.jsxs)("div", { ref: surfaceRef, id: panelId, role: "dialog", "aria-modal": "true", className: `af-drawer-surface ${drawerClassName ?? ''}`, style: {
207
- position: 'absolute',
208
- left: 0,
209
- right: 0,
210
- width: '100%',
211
- // Host supplies the CSS vars via `surfaceStyle`; the
212
- // backgroundColor resolves from `--af-bg` (which the host
213
- // declares in that same prop). When `surfaceStyle` is
214
- // omitted we fall back to white — passable for an unthemed
215
- // demo, wrong for a themed embed; passing surfaceStyle is
216
- // the correct path.
217
- backgroundColor: 'var(--af-bg, #ffffff)',
218
- color: 'var(--af-fg, inherit)',
219
- borderTopLeftRadius: '16px',
220
- borderTopRightRadius: '16px',
221
- boxShadow: '0 -8px 24px rgba(15, 23, 42, 0.25)',
222
- display: 'flex',
223
- flexDirection: 'column',
224
- overflow: 'hidden',
225
- // Host-supplied theme vars come FIRST so the motion
226
- // properties below override any conflicting `transform` /
227
- // `height` declarations a careless host might pass.
228
- ...surfaceStyleProp,
229
- ...surfaceMotionStyle,
230
- }, children: [(0, jsx_runtime_1.jsx)("div", { onPointerDown: onHandlePointerDown, onPointerMove: onHandlePointerMove, onPointerUp: onHandlePointerUp, onPointerCancel: onHandlePointerUp, style: {
231
- padding: '10px 0',
232
- cursor: 'grab',
233
- touchAction: 'none',
234
- display: 'flex',
235
- justifyContent: 'center',
236
- flexShrink: 0,
237
- }, "aria-label": "Drag to close", children: (0, jsx_runtime_1.jsx)("span", { style: {
238
- width: '40px',
239
- height: '4px',
240
- borderRadius: '999px',
241
- backgroundColor: 'var(--af-muted, rgba(100, 116, 139, 0.45))',
242
- display: 'block',
243
- } }) }), header, (0, jsx_runtime_1.jsx)("div", { style: { flex: 1, minHeight: 0, display: 'flex', flexDirection: 'column' }, children: chatSlot ?? (widgetProps ? ((0, jsx_runtime_1.jsx)(react_2.ChatWidget, { ...widgetProps, inline: true, variant: widgetProps.variant ?? 'bare' })) : null) })] })] }), portalEl);
80
+ }
81
+ const chatNode = chatSlot ?? (widgetProps ? ((0, jsx_runtime_1.jsx)(react_2.ChatWidget, { ...widgetProps, inline: true, variant: widgetProps.variant ?? 'bare' })) : null);
82
+ return ((0, jsx_runtime_1.jsx)(VaulDrawer.Root, { open: open, onOpenChange: onOpenChange,
83
+ // `direction="bottom"` is the standard bottom-sheet origin.
84
+ direction: "bottom",
85
+ // No drag-to-dismiss. The visitor closes with the explicit
86
+ // back button in the header (or ESC). Prevents accidental
87
+ // dismissals when the visitor scrolls fast in the transcript.
88
+ dismissible: false,
89
+ // No native snap points we WANT fullscreen.
90
+ shouldScaleBackground: false, children: (0, jsx_runtime_1.jsxs)(VaulDrawer.Portal, { children: [(0, jsx_runtime_1.jsx)(VaulDrawer.Overlay, { className: "af-drawer-overlay", style: {
91
+ position: 'fixed',
92
+ inset: 0,
93
+ backgroundColor: 'rgba(0, 0, 0, 0.6)',
94
+ zIndex: 2147483600,
95
+ } }), (0, jsx_runtime_1.jsxs)(VaulDrawer.Content, { className: `af-drawer-surface ${drawerClassName ?? ''}`, style: {
96
+ // Fullscreen: 100% of the visual viewport height. Vaul
97
+ // tracks visualViewport internally so this height
98
+ // shrinks when the keyboard pops up, keeping the
99
+ // composer always above the keys.
100
+ position: 'fixed',
101
+ inset: 0,
102
+ zIndex: 2147483600,
103
+ display: 'flex',
104
+ flexDirection: 'column',
105
+ outline: 'none',
106
+ backgroundColor: 'var(--af-bg, #ffffff)',
107
+ color: 'var(--af-fg, inherit)',
108
+ // Re-apply the host's theme vars on the portalled
109
+ // surface so the chat widget below picks them up. The
110
+ // `--af-bg` declared here is what the chat panel reads
111
+ // for its message background; without this the drawer
112
+ // would strobe white over a dark themed page.
113
+ ...surfaceStyle,
114
+ }, children: [(0, jsx_runtime_1.jsx)(VaulDrawer.Title, { style: {
115
+ position: 'absolute',
116
+ width: '1px',
117
+ height: '1px',
118
+ padding: 0,
119
+ margin: '-1px',
120
+ overflow: 'hidden',
121
+ clip: 'rect(0, 0, 0, 0)',
122
+ whiteSpace: 'nowrap',
123
+ border: 0,
124
+ }, children: "Chat" }), (0, jsx_runtime_1.jsxs)("div", { style: {
125
+ flexShrink: 0,
126
+ position: 'sticky',
127
+ top: 0,
128
+ zIndex: 1,
129
+ backgroundColor: 'var(--af-bg, #ffffff)',
130
+ borderBottom: '1px solid var(--af-border, rgba(15, 23, 42, 0.08))',
131
+ }, children: [closeButton ?? (0, jsx_runtime_1.jsx)(DefaultCloseButton, { onClose: () => onOpenChange(false) }), header] }), (0, jsx_runtime_1.jsx)("div", { style: {
132
+ flex: 1,
133
+ minHeight: 0,
134
+ display: 'flex',
135
+ flexDirection: 'column',
136
+ }, "data-af-drawer-body": true, children: chatNode })] })] }) }));
137
+ }
138
+ function DefaultCloseButton({ onClose }) {
139
+ return ((0, jsx_runtime_1.jsx)("div", { style: {
140
+ display: 'flex',
141
+ alignItems: 'center',
142
+ padding: '8px 12px',
143
+ }, children: (0, jsx_runtime_1.jsx)("button", { type: "button", onClick: onClose, "aria-label": "Close chat", style: {
144
+ appearance: 'none',
145
+ background: 'transparent',
146
+ border: 'none',
147
+ padding: '8px',
148
+ margin: 0,
149
+ cursor: 'pointer',
150
+ color: 'var(--af-fg, inherit)',
151
+ display: 'inline-flex',
152
+ alignItems: 'center',
153
+ justifyContent: 'center',
154
+ borderRadius: '8px',
155
+ }, children: (0, jsx_runtime_1.jsx)("svg", { width: "20", height: "20", viewBox: "0 0 24 24", fill: "none", stroke: "currentColor", strokeWidth: "2", strokeLinecap: "round", strokeLinejoin: "round", "aria-hidden": true, children: (0, jsx_runtime_1.jsx)("polyline", { points: "15 18 9 12 15 6" }) }) }) }));
244
156
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentforge-io/chat-sdk",
3
- "version": "2.3.1",
3
+ "version": "2.4.0-dev.0",
4
4
  "description": "Framework-free chat session SDK for AgentForge public chat tokens. Headless — no DOM. Drop into any frontend (React, Vue, Svelte, vanilla) and listen for events.",
5
5
  "license": "MIT",
6
6
  "main": "dist/index.js",
@@ -24,17 +24,23 @@
24
24
  "clean": "rm -rf dist *.tgz"
25
25
  },
26
26
  "peerDependencies": {
27
- "react": ">=18.0.0"
27
+ "react": ">=18.0.0",
28
+ "vaul": ">=1.0.0"
28
29
  },
29
30
  "peerDependenciesMeta": {
30
31
  "react": {
31
32
  "optional": true
33
+ },
34
+ "vaul": {
35
+ "optional": true
32
36
  }
33
37
  },
34
38
  "devDependencies": {
35
39
  "@types/node": "^20.0.0",
36
40
  "@types/react": "^18.3.28",
37
41
  "react": "^18.3.1",
38
- "typescript": "^5.0.0"
42
+ "react-dom": "^18.3.1",
43
+ "typescript": "^5.0.0",
44
+ "vaul": "^1.1.2"
39
45
  }
40
46
  }