@agentforge-io/chat-sdk 2.4.0-dev.4 → 2.4.0-dev.5

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,41 +1,24 @@
1
1
  /**
2
- * `<ChatDrawer>` — standard mobile chat shell.
2
+ * `<ChatDrawer>` — fullscreen mobile chat shell.
3
3
  *
4
- * Mobile-first fullscreen modal that wraps `<ChatWidget>` and gets the
5
- * three pieces of mobile UX every embed needs right:
4
+ * Plain `position: fixed` modal. No Vaul, no Radix Dialog, no focus
5
+ * trap. Those libraries fought us repeatedly: Radix's `onPointerDownOutside`
6
+ * mis-classified the Send tap as "interact outside" and closed the
7
+ * drawer; Radix's focus trap moved focus to the wrapper when the send
8
+ * button disabled, collapsing the mobile keyboard.
6
9
  *
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.
12
- *
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.
10
+ * What this gives us:
11
+ * - 100dvh (dynamic viewport height): the surface shrinks automatically
12
+ * when the on-screen keyboard appears, so the composer stays above
13
+ * the keys without any visualViewport bookkeeping.
14
+ * - No focus interception: the textarea owns its own focus. As long
15
+ * as the host's Send button uses `onPointerDown preventDefault`,
16
+ * focus never leaves the textarea and the keyboard never collapses.
17
+ * - No drag-to-dismiss, no click-outside, no ESC handling. The host
18
+ * closes via the back chevron in its header.
31
19
  */
32
20
  import { type CSSProperties, type ReactNode } from 'react';
33
21
  import { type ChatWidgetProps } from './react';
34
- /**
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.
38
- */
39
22
  type ChatSurface = {
40
23
  widgetProps: ChatWidgetProps;
41
24
  chatSlot?: never;
@@ -46,22 +29,21 @@ type ChatSurface = {
46
29
  export type ChatDrawerProps = ChatSurface & {
47
30
  /** Controlled visibility. */
48
31
  open: boolean;
49
- /** Fired when the drawer wants to close (back button, ESC). The
50
- * host is responsible for setting `open=false`. */
32
+ /** Fired when the drawer wants to close. Today only the host's back
33
+ * chevron emits this no auto-dismiss paths exist. Kept in the
34
+ * shape so the host doesn't have to change call sites. */
51
35
  onOpenChange: (open: boolean) => void;
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. */
36
+ /** Sticky header above the chat panel. */
55
37
  header?: ReactNode;
56
- /** Custom close button. Defaults to a chevron-left back button at
57
- * the left edge of the header. */
38
+ /** Custom close button rendered ABOVE `header`. Default is the SDK's
39
+ * chevron-left. Pass `null` to opt out (when the host renders its
40
+ * own close affordance inline within `header`). */
58
41
  closeButton?: ReactNode;
59
- /** Extra class on the drawer surface (the full-height card). */
42
+ /** Extra class on the drawer surface. */
60
43
  drawerClassName?: string;
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. */
44
+ /** CSS vars (`--af-bg`, `--af-fg`, `--af-bubble-*`, ) for the
45
+ * surface. The drawer renders into a portal, so the host's
46
+ * page-wrapper vars don't cascade in — re-declare them here. */
65
47
  surfaceStyle?: CSSProperties & Record<string, string>;
66
48
  };
67
49
  export declare function ChatDrawer(props: ChatDrawerProps): JSX.Element | null;
@@ -3,59 +3,36 @@ 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 mobile chat shell.
6
+ * `<ChatDrawer>` — fullscreen mobile chat shell.
7
7
  *
8
- * Mobile-first fullscreen modal that wraps `<ChatWidget>` and gets the
9
- * three pieces of mobile UX every embed needs right:
8
+ * Plain `position: fixed` modal. No Vaul, no Radix Dialog, no focus
9
+ * trap. Those libraries fought us repeatedly: Radix's `onPointerDownOutside`
10
+ * mis-classified the Send tap as "interact outside" and closed the
11
+ * drawer; Radix's focus trap moved focus to the wrapper when the send
12
+ * button disabled, collapsing the mobile keyboard.
10
13
  *
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.
16
- *
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.
14
+ * What this gives us:
15
+ * - 100dvh (dynamic viewport height): the surface shrinks automatically
16
+ * when the on-screen keyboard appears, so the composer stays above
17
+ * the keys without any visualViewport bookkeeping.
18
+ * - No focus interception: the textarea owns its own focus. As long
19
+ * as the host's Send button uses `onPointerDown preventDefault`,
20
+ * focus never leaves the textarea and the keyboard never collapses.
21
+ * - No drag-to-dismiss, no click-outside, no ESC handling. The host
22
+ * closes via the back chevron in its header.
35
23
  */
36
24
  const react_1 = require("react");
37
25
  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
- }
53
26
  function ChatDrawer(props) {
54
27
  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.
28
+ // Defer the first paint until after mount so SSR doesn't try to
29
+ // render a portal target that doesn't exist yet.
30
+ const [mounted, setMounted] = (0, react_1.useState)(false);
31
+ (0, react_1.useEffect)(() => {
32
+ setMounted(true);
33
+ }, []);
34
+ // Lock body scroll while the drawer is open. Standard pattern: save
35
+ // the previous overflow, set hidden, restore on close/unmount.
59
36
  (0, react_1.useEffect)(() => {
60
37
  if (typeof document === 'undefined')
61
38
  return;
@@ -67,73 +44,31 @@ function ChatDrawer(props) {
67
44
  document.body.style.overflow = prev;
68
45
  };
69
46
  }, [open]);
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
- }
47
+ if (!mounted || !open)
79
48
  return null;
80
- }
81
49
  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 === undefined ? ((0, jsx_runtime_1.jsx)(DefaultCloseButton, { onClose: () => onOpenChange(false) })) : (closeButton), 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 })] })] }) }));
50
+ return ((0, jsx_runtime_1.jsxs)("div", { role: "dialog", "aria-modal": "true", "aria-label": "Chat", className: `af-drawer-surface ${drawerClassName ?? ''}`, style: {
51
+ // 100dvh shrinks when the on-screen keyboard appears.
52
+ // `inset: 0` + `position: fixed` covers the viewport.
53
+ position: 'fixed',
54
+ inset: 0,
55
+ height: '100dvh',
56
+ zIndex: 2147483600,
57
+ display: 'flex',
58
+ flexDirection: 'column',
59
+ backgroundColor: 'var(--af-bg, #ffffff)',
60
+ color: 'var(--af-fg, inherit)',
61
+ ...surfaceStyle,
62
+ }, children: [(0, jsx_runtime_1.jsxs)("div", { style: {
63
+ flexShrink: 0,
64
+ backgroundColor: 'var(--af-bg, #ffffff)',
65
+ borderBottom: '1px solid var(--af-border, rgba(15, 23, 42, 0.08))',
66
+ }, children: [closeButton === undefined ? ((0, jsx_runtime_1.jsx)(DefaultCloseButton, { onClose: () => onOpenChange(false) })) : (closeButton), header] }), (0, jsx_runtime_1.jsx)("div", { style: {
67
+ flex: 1,
68
+ minHeight: 0,
69
+ display: 'flex',
70
+ flexDirection: 'column',
71
+ }, "data-af-drawer-body": true, children: chatNode })] }));
137
72
  }
138
73
  function DefaultCloseButton({ onClose }) {
139
74
  return ((0, jsx_runtime_1.jsx)("div", { style: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentforge-io/chat-sdk",
3
- "version": "2.4.0-dev.4",
3
+ "version": "2.4.0-dev.5",
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,15 +24,11 @@
24
24
  "clean": "rm -rf dist *.tgz"
25
25
  },
26
26
  "peerDependencies": {
27
- "react": ">=18.0.0",
28
- "vaul": ">=1.0.0"
27
+ "react": ">=18.0.0"
29
28
  },
30
29
  "peerDependenciesMeta": {
31
30
  "react": {
32
31
  "optional": true
33
- },
34
- "vaul": {
35
- "optional": true
36
32
  }
37
33
  },
38
34
  "devDependencies": {
@@ -40,7 +36,6 @@
40
36
  "@types/react": "^18.3.28",
41
37
  "react": "^18.3.1",
42
38
  "react-dom": "^18.3.1",
43
- "typescript": "^5.0.0",
44
- "vaul": "^1.1.2"
39
+ "typescript": "^5.0.0"
45
40
  }
46
41
  }