@btst/stack 4.2.0 → 4.3.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 (56) hide show
  1. package/README.md +2 -2
  2. package/dist/packages/stack/src/plugins/ai-chat/client/components/chat-layout.cjs +173 -72
  3. package/dist/packages/stack/src/plugins/ai-chat/client/components/chat-layout.mjs +176 -75
  4. package/dist/plugins/ai-chat/client/components/index.d.cts +1 -1
  5. package/dist/plugins/ai-chat/client/components/index.d.mts +1 -1
  6. package/dist/plugins/ai-chat/client/components/index.d.ts +1 -1
  7. package/dist/plugins/ai-chat/client/index.d.cts +2 -2
  8. package/dist/plugins/ai-chat/client/index.d.mts +2 -2
  9. package/dist/plugins/ai-chat/client/index.d.ts +2 -2
  10. package/dist/plugins/blog/api/index.d.cts +2 -2
  11. package/dist/plugins/blog/api/index.d.mts +2 -2
  12. package/dist/plugins/blog/api/index.d.ts +2 -2
  13. package/dist/plugins/blog/client/hooks/index.d.cts +2 -2
  14. package/dist/plugins/blog/client/hooks/index.d.mts +2 -2
  15. package/dist/plugins/blog/client/hooks/index.d.ts +2 -2
  16. package/dist/plugins/blog/client/index.d.cts +2 -2
  17. package/dist/plugins/blog/client/index.d.mts +2 -2
  18. package/dist/plugins/blog/client/index.d.ts +2 -2
  19. package/dist/plugins/blog/query-keys.d.cts +2 -2
  20. package/dist/plugins/blog/query-keys.d.mts +2 -2
  21. package/dist/plugins/blog/query-keys.d.ts +2 -2
  22. package/dist/plugins/cms/api/index.d.cts +1 -1
  23. package/dist/plugins/cms/api/index.d.mts +1 -1
  24. package/dist/plugins/cms/api/index.d.ts +1 -1
  25. package/dist/plugins/cms/query-keys.d.cts +1 -1
  26. package/dist/plugins/cms/query-keys.d.mts +1 -1
  27. package/dist/plugins/cms/query-keys.d.ts +1 -1
  28. package/dist/plugins/ui-builder/client/hooks/index.d.cts +1 -1
  29. package/dist/plugins/ui-builder/client/hooks/index.d.mts +1 -1
  30. package/dist/plugins/ui-builder/client/hooks/index.d.ts +1 -1
  31. package/dist/plugins/ui-builder/client/index.d.cts +1 -1
  32. package/dist/plugins/ui-builder/client/index.d.mts +1 -1
  33. package/dist/plugins/ui-builder/client/index.d.ts +1 -1
  34. package/dist/plugins/ui-builder/query-keys.d.cts +1 -1
  35. package/dist/plugins/ui-builder/query-keys.d.mts +1 -1
  36. package/dist/plugins/ui-builder/query-keys.d.ts +1 -1
  37. package/dist/shared/{stack.C5FZr-jJ.d.mts → stack.CHrtGKLz.d.mts} +9 -3
  38. package/dist/shared/{stack.CTneIW0u.d.mts → stack.CZ1h_T4k.d.mts} +88 -88
  39. package/dist/shared/{stack.DfrRYOIl.d.ts → stack.CnDMBj4z.d.ts} +5 -5
  40. package/dist/shared/{stack.94kN3pEK.d.mts → stack.D2y4gl03.d.mts} +5 -5
  41. package/dist/shared/{stack.D5b9l24t.d.ts → stack.DQpyA3Cq.d.ts} +88 -88
  42. package/dist/shared/{stack.Ce6V7eDj.d.cts → stack.DiYg7Jw1.d.cts} +88 -88
  43. package/dist/shared/{stack.xAiy2VtO.d.cts → stack.DzJgoNqJ.d.cts} +9 -3
  44. package/dist/shared/{stack.C-eQN_Jt.d.ts → stack.qMp2VzNw.d.ts} +9 -3
  45. package/dist/shared/{stack.BTStHVTy.d.cts → stack.rmWRtSjs.d.cts} +5 -5
  46. package/package.json +2 -2
  47. package/src/plugins/ai-chat/__tests__/classnames-overrides.test.tsx +22 -2
  48. package/src/plugins/ai-chat/__tests__/page-widget.test.tsx +103 -1
  49. package/src/plugins/ai-chat/client/components/chat-layout.tsx +197 -72
  50. package/src/plugins/ai-chat/client/localization/index.ts +4 -0
  51. package/dist/shared/{stack.CyalJ0j6.d.ts → stack.BMlTZnZp.d.ts} +16 -16
  52. package/dist/shared/{stack.D6Pg-ufX.d.cts → stack.BV0uNV4f.d.cts} +16 -16
  53. package/dist/shared/{stack.CKc_Bais.d.cts → stack.DtZgEX84.d.cts} +6 -6
  54. package/dist/shared/{stack.CKc_Bais.d.mts → stack.DtZgEX84.d.mts} +6 -6
  55. package/dist/shared/{stack.CKc_Bais.d.ts → stack.DtZgEX84.d.ts} +6 -6
  56. package/dist/shared/{stack.B8PG21zX.d.mts → stack.lsbdKRj7.d.mts} +16 -16
package/README.md CHANGED
@@ -52,13 +52,13 @@ Render the generated `<Toaster />` in your root layout and start from a clean co
52
52
  Run the released generator from the application root:
53
53
 
54
54
  ```bash
55
- pnpm dlx @btst/codegen@4.2.0 init \
55
+ pnpm dlx @btst/codegen@4.3.0 init \
56
56
  --framework nextjs \
57
57
  --adapter memory \
58
58
  --plugins blog
59
59
  ```
60
60
 
61
- The generator shows detected-file and conflict prompts before it writes. It installs `@btst/stack@4.2.0`, `next-themes@0.4.6`, registers Blog on the backend and client, mounts the API and page routes, adds the plugin CSS, and wires the shared provider.
61
+ The generator shows detected-file and conflict prompts before it writes. It installs `@btst/stack@4.3.0`, `next-themes@0.4.6`, registers Blog on the backend and client, mounts the API and page routes, adds the plugin CSS, and wires the shared provider.
62
62
 
63
63
  Set the trusted local origins in `.env.local`:
64
64
 
@@ -3,6 +3,8 @@
3
3
 
4
4
  const jsxRuntime = require('react/jsx-runtime');
5
5
  const React = require('react');
6
+ const ReactDOM = require('react-dom');
7
+ const dialog = require('../../../../../../ui/src/components/dialog.cjs');
6
8
  const button = require('../../../../../../ui/src/components/button.cjs');
7
9
  const badge = require('../../../../../../ui/src/components/badge.cjs');
8
10
  const sheet = require('../../../../../../ui/src/components/sheet.cjs');
@@ -36,14 +38,44 @@ function ChatLayoutContent(props) {
36
38
  const resolvedMode = overrides.resolveAiChatMode(plugins?.aiChat?.config);
37
39
  const showSidebar = requestedShowSidebar && resolvedMode !== "public";
38
40
  const tr = index.useAiChatTranslation(localization);
39
- const widgetHeight = props.layout === "widget" ? props.widgetHeight ?? "min(600px, calc(100dvh - 112px))" : void 0;
40
- const widgetWidth = props.layout === "widget" ? props.widgetWidth ?? "min(380px, calc(100vw - 32px))" : void 0;
41
+ const widgetHeight = props.layout === "widget" ? props.widgetHeight ?? "min(640px, calc(100dvh - 112px))" : void 0;
42
+ const widgetWidth = props.layout === "widget" ? props.widgetWidth ?? "min(440px, calc(100vw - 32px))" : void 0;
41
43
  const defaultOpen = props.layout === "widget" ? props.defaultOpen ?? false : false;
42
44
  const showTrigger = props.layout === "widget" ? props.showTrigger ?? true : true;
45
+ const triggerIcon = props.layout === "widget" ? props.triggerIcon : void 0;
43
46
  const [sidebarOpen, setSidebarOpen] = React.useState(true);
44
47
  const [mobileSidebarOpen, setMobileSidebarOpen] = React.useState(false);
45
48
  const [chatResetKey, setChatResetKey] = React.useState(0);
46
49
  const [widgetOpen, setWidgetOpen] = React.useState(defaultOpen);
50
+ const [expanded, setExpanded] = React.useState(false);
51
+ const expandable = props.layout === "widget" && props.expandable === true;
52
+ const widgetExpanded = expandable && expanded && widgetOpen;
53
+ React.useEffect(() => {
54
+ if (!expandable) setExpanded(false);
55
+ }, [expandable]);
56
+ const compactPanelRef = React.useRef(null);
57
+ const triggerRef = React.useRef(null);
58
+ const expandRef = React.useRef(null);
59
+ const [panelContainer, setPanelContainer] = React.useState(
60
+ null
61
+ );
62
+ const expandLabel = widgetExpanded ? tr("A11Y_COLLAPSE_CHAT", "aiChat.a11y.collapseChat", "Collapse chat") : tr("A11Y_EXPAND_CHAT", "aiChat.a11y.expandChat", "Expand chat");
63
+ React.useEffect(() => {
64
+ if (layout !== "widget") return;
65
+ const container = document.createElement("div");
66
+ container.className = "flex h-full min-h-0 flex-col";
67
+ compactPanelRef.current?.appendChild(container);
68
+ setPanelContainer(container);
69
+ return () => container.remove();
70
+ }, [layout]);
71
+ const mountExpandedPanel = React.useCallback(
72
+ (node) => {
73
+ if (panelContainer) {
74
+ (node ?? compactPanelRef.current)?.appendChild(panelContainer);
75
+ }
76
+ },
77
+ [panelContainer]
78
+ );
47
79
  const [widgetResetKey, setWidgetResetKey] = React.useState(0);
48
80
  const [widgetEverOpened, setWidgetEverOpened] = React.useState(defaultOpen);
49
81
  const [tipDismissed, setTipDismissed] = React.useState(defaultOpen);
@@ -121,97 +153,166 @@ function ChatLayoutContent(props) {
121
153
  ]
122
154
  }
123
155
  ),
124
- /* @__PURE__ */ jsxRuntime.jsxs(
156
+ /* @__PURE__ */ jsxRuntime.jsx(
125
157
  "div",
126
158
  {
159
+ ref: compactPanelRef,
160
+ role: "dialog",
161
+ tabIndex: -1,
162
+ "aria-label": tr("A11Y_CHAT_TITLE", "aiChat.a11y.title", "AI Chat"),
163
+ className: utils.cn(
164
+ "overflow-hidden rounded-xl border bg-background shadow-xl",
165
+ (!widgetOpen || widgetExpanded) && "hidden"
166
+ ),
167
+ style: { height: widgetHeight, width: widgetWidth }
168
+ }
169
+ ),
170
+ /* @__PURE__ */ jsxRuntime.jsx(dialog.Dialog, { open: widgetExpanded, onOpenChange: setExpanded, children: /* @__PURE__ */ jsxRuntime.jsxs(
171
+ dialog.DialogContent,
172
+ {
173
+ showCloseButton: false,
174
+ style: {
175
+ ...style,
176
+ position: "fixed",
177
+ inset: 0,
178
+ top: 0,
179
+ right: 0,
180
+ bottom: 0,
181
+ left: 0,
182
+ width: "100vw",
183
+ height: "100dvh",
184
+ minWidth: 0,
185
+ minHeight: 0,
186
+ maxWidth: "none",
187
+ maxHeight: "none",
188
+ margin: 0,
189
+ transform: "none",
190
+ translate: "none",
191
+ zIndex: 50,
192
+ animation: "none"
193
+ },
194
+ "aria-describedby": void 0,
127
195
  className: utils.cn(
128
- "flex flex-col border rounded-xl overflow-hidden bg-background shadow-xl",
129
- widgetOpen ? "flex" : "hidden"
196
+ className,
197
+ classNames?.container,
198
+ "fixed inset-0 z-50 flex h-dvh w-screen max-w-none translate-x-0 translate-y-0 flex-col gap-0 rounded-none border-0 p-0 sm:max-w-none pt-[env(safe-area-inset-top)] pb-[env(safe-area-inset-bottom)] pl-[env(safe-area-inset-left)] pr-[env(safe-area-inset-right)]"
130
199
  ),
131
- style: { height: widgetHeight, width: widgetWidth },
200
+ onCloseAutoFocus: (event) => {
201
+ const target = widgetOpen ? expandRef.current ?? compactPanelRef.current : triggerRef.current;
202
+ if (target) {
203
+ event.preventDefault();
204
+ target.focus();
205
+ }
206
+ },
132
207
  children: [
133
- /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex items-center gap-1.5 px-3 py-1.5 border-b bg-muted/40", children: [
134
- /* @__PURE__ */ jsxRuntime.jsx(LucideIcons.Sparkles, { className: "h-3 w-3 text-muted-foreground" }),
135
- pageAIContext ? /* @__PURE__ */ jsxRuntime.jsx(
136
- badge.Badge,
137
- {
138
- variant: "secondary",
139
- className: "text-xs",
140
- "data-testid": "page-context-badge",
141
- children: pageAIContext.routeName
142
- }
143
- ) : /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs text-muted-foreground font-medium", children: tr("A11Y_CHAT_TITLE", "aiChat.a11y.title", "AI Chat") }),
144
- /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex-1" }),
145
- /* @__PURE__ */ jsxRuntime.jsx(
146
- button.Button,
147
- {
148
- variant: "ghost",
149
- size: "icon",
150
- className: "h-5 w-5",
151
- onClick: () => {
152
- track({ type: "conversation_cleared" });
153
- onClear?.();
154
- setWidgetResetKey((prev) => prev + 1);
155
- },
156
- "aria-label": tr(
157
- "A11Y_CLEAR_CHAT",
158
- "aiChat.a11y.clearChat",
159
- "Clear chat"
160
- ),
161
- title: tr(
162
- "A11Y_CLEAR_CHAT",
163
- "aiChat.a11y.clearChat",
164
- "Clear chat"
165
- ),
166
- children: /* @__PURE__ */ jsxRuntime.jsx(LucideIcons.Trash2, { className: "h-3.5 w-3.5" })
167
- }
168
- ),
169
- /* @__PURE__ */ jsxRuntime.jsx(
170
- button.Button,
171
- {
172
- variant: "ghost",
173
- size: "icon",
174
- className: "h-5 w-5",
175
- onClick: () => {
176
- setWidgetOpen(false);
177
- track({ type: "widget_closed" });
178
- },
179
- "aria-label": tr(
180
- "A11Y_CLOSE_CHAT",
181
- "aiChat.a11y.closeChat",
182
- "Close chat"
183
- ),
184
- children: /* @__PURE__ */ jsxRuntime.jsx(LucideIcons.X, { className: "h-3.5 w-3.5" })
185
- }
186
- )
187
- ] }),
188
- widgetEverOpened && /* @__PURE__ */ jsxRuntime.jsx(
189
- chatInterface.ChatInterface,
190
- {
191
- id: conversationId,
192
- variant: "widget",
193
- initialMessages,
194
- onMessagesChange
195
- },
196
- `widget-${conversationId ?? "new"}-${widgetResetKey}`
197
- )
208
+ /* @__PURE__ */ jsxRuntime.jsx(dialog.DialogTitle, { className: "sr-only", children: tr("A11Y_CHAT_TITLE", "aiChat.a11y.title", "AI Chat") }),
209
+ /* @__PURE__ */ jsxRuntime.jsx("div", { ref: mountExpandedPanel, className: "min-h-0 flex-1" })
198
210
  ]
199
211
  }
212
+ ) }),
213
+ panelContainer && ReactDOM.createPortal(
214
+ /* @__PURE__ */ jsxRuntime.jsxs(jsxRuntime.Fragment, { children: [
215
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { className: "flex shrink-0 items-center gap-1.5 px-3 py-1.5 border-b bg-muted/40", children: [
216
+ /* @__PURE__ */ jsxRuntime.jsx(LucideIcons.Sparkles, { className: "h-3 w-3 text-muted-foreground" }),
217
+ pageAIContext ? /* @__PURE__ */ jsxRuntime.jsx(
218
+ badge.Badge,
219
+ {
220
+ variant: "secondary",
221
+ className: "text-xs",
222
+ "data-testid": "page-context-badge",
223
+ children: pageAIContext.routeName
224
+ }
225
+ ) : /* @__PURE__ */ jsxRuntime.jsx("span", { className: "text-xs text-muted-foreground font-medium", children: tr("A11Y_CHAT_TITLE", "aiChat.a11y.title", "AI Chat") }),
226
+ /* @__PURE__ */ jsxRuntime.jsx("div", { className: "flex-1" }),
227
+ expandable && /* @__PURE__ */ jsxRuntime.jsx(
228
+ button.Button,
229
+ {
230
+ type: "button",
231
+ variant: "ghost",
232
+ size: "icon",
233
+ className: "h-7 w-7",
234
+ ref: expandRef,
235
+ onClick: () => setExpanded((prev) => !prev),
236
+ "aria-label": expandLabel,
237
+ title: expandLabel,
238
+ "aria-expanded": widgetExpanded,
239
+ children: widgetExpanded ? /* @__PURE__ */ jsxRuntime.jsx(LucideIcons.Minimize2, { className: "h-3.5 w-3.5", "aria-hidden": "true" }) : /* @__PURE__ */ jsxRuntime.jsx(LucideIcons.Maximize2, { className: "h-3.5 w-3.5", "aria-hidden": "true" })
240
+ }
241
+ ),
242
+ /* @__PURE__ */ jsxRuntime.jsx(
243
+ button.Button,
244
+ {
245
+ variant: "ghost",
246
+ size: "icon",
247
+ className: "h-5 w-5",
248
+ onClick: () => {
249
+ track({ type: "conversation_cleared" });
250
+ onClear?.();
251
+ setWidgetResetKey((prev) => prev + 1);
252
+ },
253
+ "aria-label": tr(
254
+ "A11Y_CLEAR_CHAT",
255
+ "aiChat.a11y.clearChat",
256
+ "Clear chat"
257
+ ),
258
+ title: tr(
259
+ "A11Y_CLEAR_CHAT",
260
+ "aiChat.a11y.clearChat",
261
+ "Clear chat"
262
+ ),
263
+ children: /* @__PURE__ */ jsxRuntime.jsx(LucideIcons.Trash2, { className: "h-3.5 w-3.5" })
264
+ }
265
+ ),
266
+ /* @__PURE__ */ jsxRuntime.jsx(
267
+ button.Button,
268
+ {
269
+ variant: "ghost",
270
+ size: "icon",
271
+ className: "h-5 w-5",
272
+ onClick: () => {
273
+ setWidgetOpen(false);
274
+ setExpanded(false);
275
+ triggerRef.current?.focus();
276
+ track({ type: "widget_closed" });
277
+ },
278
+ "aria-label": tr(
279
+ "A11Y_CLOSE_CHAT",
280
+ "aiChat.a11y.closeChat",
281
+ "Close chat"
282
+ ),
283
+ children: /* @__PURE__ */ jsxRuntime.jsx(LucideIcons.X, { className: "h-3.5 w-3.5" })
284
+ }
285
+ )
286
+ ] }),
287
+ widgetEverOpened && /* @__PURE__ */ jsxRuntime.jsx(
288
+ chatInterface.ChatInterface,
289
+ {
290
+ id: conversationId,
291
+ variant: "widget",
292
+ initialMessages,
293
+ onMessagesChange
294
+ },
295
+ `widget-${conversationId ?? "new"}-${widgetResetKey}`
296
+ )
297
+ ] }),
298
+ panelContainer
200
299
  ),
201
300
  showTrigger && /* @__PURE__ */ jsxRuntime.jsx(
202
301
  button.Button,
203
302
  {
303
+ ref: triggerRef,
204
304
  size: "icon",
205
305
  className: "h-12 w-12 rounded-full shadow-lg",
206
306
  onClick: () => {
207
307
  dismissTip("open");
208
308
  track({ type: widgetOpen ? "widget_closed" : "widget_opened" });
209
309
  setWidgetOpen((prev) => !prev);
310
+ setExpanded(false);
210
311
  setWidgetEverOpened(true);
211
312
  },
212
313
  "aria-label": widgetOpen ? tr("A11Y_CLOSE_CHAT", "aiChat.a11y.closeChat", "Close chat") : tr("A11Y_OPEN_CHAT", "aiChat.a11y.openChat", "Open chat"),
213
314
  "data-testid": "widget-trigger",
214
- children: widgetOpen ? /* @__PURE__ */ jsxRuntime.jsx(LucideIcons.X, { className: "h-5 w-5" }) : /* @__PURE__ */ jsxRuntime.jsx(LucideIcons.Sparkles, { className: "h-5 w-5" })
315
+ children: widgetOpen ? /* @__PURE__ */ jsxRuntime.jsx(LucideIcons.X, { className: "h-5 w-5" }) : triggerIcon ?? /* @__PURE__ */ jsxRuntime.jsx(LucideIcons.Sparkles, { className: "h-5 w-5" })
215
316
  }
216
317
  )
217
318
  ]
@@ -1,10 +1,12 @@
1
1
  "use client";
2
- import { jsx, jsxs } from 'react/jsx-runtime';
3
- import { useState, useRef, useCallback, useEffect } from 'react';
2
+ import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
3
+ import { useState, useEffect, useRef, useCallback } from 'react';
4
+ import { createPortal } from 'react-dom';
5
+ import { Dialog, DialogContent, DialogTitle } from '../../../../../../ui/src/components/dialog.mjs';
4
6
  import { Button } from '../../../../../../ui/src/components/button.mjs';
5
7
  import { Badge } from '../../../../../../ui/src/components/badge.mjs';
6
8
  import { Sheet, SheetTrigger, SheetContent } from '../../../../../../ui/src/components/sheet.mjs';
7
- import { X, Sparkles, Trash2, Menu, PanelLeftClose, PanelLeft } from 'lucide-react';
9
+ import { X, Sparkles, Minimize2, Maximize2, Trash2, Menu, PanelLeftClose, PanelLeft } from 'lucide-react';
8
10
  import { cn } from '../../../../../../ui/src/lib/utils.mjs';
9
11
  import { ChatSidebar } from './chat-sidebar.mjs';
10
12
  import { ChatInterface } from './chat-interface.mjs';
@@ -34,14 +36,44 @@ function ChatLayoutContent(props) {
34
36
  const resolvedMode = resolveAiChatMode(plugins?.aiChat?.config);
35
37
  const showSidebar = requestedShowSidebar && resolvedMode !== "public";
36
38
  const tr = useAiChatTranslation(localization);
37
- const widgetHeight = props.layout === "widget" ? props.widgetHeight ?? "min(600px, calc(100dvh - 112px))" : void 0;
38
- const widgetWidth = props.layout === "widget" ? props.widgetWidth ?? "min(380px, calc(100vw - 32px))" : void 0;
39
+ const widgetHeight = props.layout === "widget" ? props.widgetHeight ?? "min(640px, calc(100dvh - 112px))" : void 0;
40
+ const widgetWidth = props.layout === "widget" ? props.widgetWidth ?? "min(440px, calc(100vw - 32px))" : void 0;
39
41
  const defaultOpen = props.layout === "widget" ? props.defaultOpen ?? false : false;
40
42
  const showTrigger = props.layout === "widget" ? props.showTrigger ?? true : true;
43
+ const triggerIcon = props.layout === "widget" ? props.triggerIcon : void 0;
41
44
  const [sidebarOpen, setSidebarOpen] = useState(true);
42
45
  const [mobileSidebarOpen, setMobileSidebarOpen] = useState(false);
43
46
  const [chatResetKey, setChatResetKey] = useState(0);
44
47
  const [widgetOpen, setWidgetOpen] = useState(defaultOpen);
48
+ const [expanded, setExpanded] = useState(false);
49
+ const expandable = props.layout === "widget" && props.expandable === true;
50
+ const widgetExpanded = expandable && expanded && widgetOpen;
51
+ useEffect(() => {
52
+ if (!expandable) setExpanded(false);
53
+ }, [expandable]);
54
+ const compactPanelRef = useRef(null);
55
+ const triggerRef = useRef(null);
56
+ const expandRef = useRef(null);
57
+ const [panelContainer, setPanelContainer] = useState(
58
+ null
59
+ );
60
+ const expandLabel = widgetExpanded ? tr("A11Y_COLLAPSE_CHAT", "aiChat.a11y.collapseChat", "Collapse chat") : tr("A11Y_EXPAND_CHAT", "aiChat.a11y.expandChat", "Expand chat");
61
+ useEffect(() => {
62
+ if (layout !== "widget") return;
63
+ const container = document.createElement("div");
64
+ container.className = "flex h-full min-h-0 flex-col";
65
+ compactPanelRef.current?.appendChild(container);
66
+ setPanelContainer(container);
67
+ return () => container.remove();
68
+ }, [layout]);
69
+ const mountExpandedPanel = useCallback(
70
+ (node) => {
71
+ if (panelContainer) {
72
+ (node ?? compactPanelRef.current)?.appendChild(panelContainer);
73
+ }
74
+ },
75
+ [panelContainer]
76
+ );
45
77
  const [widgetResetKey, setWidgetResetKey] = useState(0);
46
78
  const [widgetEverOpened, setWidgetEverOpened] = useState(defaultOpen);
47
79
  const [tipDismissed, setTipDismissed] = useState(defaultOpen);
@@ -119,97 +151,166 @@ function ChatLayoutContent(props) {
119
151
  ]
120
152
  }
121
153
  ),
122
- /* @__PURE__ */ jsxs(
154
+ /* @__PURE__ */ jsx(
123
155
  "div",
124
156
  {
157
+ ref: compactPanelRef,
158
+ role: "dialog",
159
+ tabIndex: -1,
160
+ "aria-label": tr("A11Y_CHAT_TITLE", "aiChat.a11y.title", "AI Chat"),
161
+ className: cn(
162
+ "overflow-hidden rounded-xl border bg-background shadow-xl",
163
+ (!widgetOpen || widgetExpanded) && "hidden"
164
+ ),
165
+ style: { height: widgetHeight, width: widgetWidth }
166
+ }
167
+ ),
168
+ /* @__PURE__ */ jsx(Dialog, { open: widgetExpanded, onOpenChange: setExpanded, children: /* @__PURE__ */ jsxs(
169
+ DialogContent,
170
+ {
171
+ showCloseButton: false,
172
+ style: {
173
+ ...style,
174
+ position: "fixed",
175
+ inset: 0,
176
+ top: 0,
177
+ right: 0,
178
+ bottom: 0,
179
+ left: 0,
180
+ width: "100vw",
181
+ height: "100dvh",
182
+ minWidth: 0,
183
+ minHeight: 0,
184
+ maxWidth: "none",
185
+ maxHeight: "none",
186
+ margin: 0,
187
+ transform: "none",
188
+ translate: "none",
189
+ zIndex: 50,
190
+ animation: "none"
191
+ },
192
+ "aria-describedby": void 0,
125
193
  className: cn(
126
- "flex flex-col border rounded-xl overflow-hidden bg-background shadow-xl",
127
- widgetOpen ? "flex" : "hidden"
194
+ className,
195
+ classNames?.container,
196
+ "fixed inset-0 z-50 flex h-dvh w-screen max-w-none translate-x-0 translate-y-0 flex-col gap-0 rounded-none border-0 p-0 sm:max-w-none pt-[env(safe-area-inset-top)] pb-[env(safe-area-inset-bottom)] pl-[env(safe-area-inset-left)] pr-[env(safe-area-inset-right)]"
128
197
  ),
129
- style: { height: widgetHeight, width: widgetWidth },
198
+ onCloseAutoFocus: (event) => {
199
+ const target = widgetOpen ? expandRef.current ?? compactPanelRef.current : triggerRef.current;
200
+ if (target) {
201
+ event.preventDefault();
202
+ target.focus();
203
+ }
204
+ },
130
205
  children: [
131
- /* @__PURE__ */ jsxs("div", { className: "flex items-center gap-1.5 px-3 py-1.5 border-b bg-muted/40", children: [
132
- /* @__PURE__ */ jsx(Sparkles, { className: "h-3 w-3 text-muted-foreground" }),
133
- pageAIContext ? /* @__PURE__ */ jsx(
134
- Badge,
135
- {
136
- variant: "secondary",
137
- className: "text-xs",
138
- "data-testid": "page-context-badge",
139
- children: pageAIContext.routeName
140
- }
141
- ) : /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground font-medium", children: tr("A11Y_CHAT_TITLE", "aiChat.a11y.title", "AI Chat") }),
142
- /* @__PURE__ */ jsx("div", { className: "flex-1" }),
143
- /* @__PURE__ */ jsx(
144
- Button,
145
- {
146
- variant: "ghost",
147
- size: "icon",
148
- className: "h-5 w-5",
149
- onClick: () => {
150
- track({ type: "conversation_cleared" });
151
- onClear?.();
152
- setWidgetResetKey((prev) => prev + 1);
153
- },
154
- "aria-label": tr(
155
- "A11Y_CLEAR_CHAT",
156
- "aiChat.a11y.clearChat",
157
- "Clear chat"
158
- ),
159
- title: tr(
160
- "A11Y_CLEAR_CHAT",
161
- "aiChat.a11y.clearChat",
162
- "Clear chat"
163
- ),
164
- children: /* @__PURE__ */ jsx(Trash2, { className: "h-3.5 w-3.5" })
165
- }
166
- ),
167
- /* @__PURE__ */ jsx(
168
- Button,
169
- {
170
- variant: "ghost",
171
- size: "icon",
172
- className: "h-5 w-5",
173
- onClick: () => {
174
- setWidgetOpen(false);
175
- track({ type: "widget_closed" });
176
- },
177
- "aria-label": tr(
178
- "A11Y_CLOSE_CHAT",
179
- "aiChat.a11y.closeChat",
180
- "Close chat"
181
- ),
182
- children: /* @__PURE__ */ jsx(X, { className: "h-3.5 w-3.5" })
183
- }
184
- )
185
- ] }),
186
- widgetEverOpened && /* @__PURE__ */ jsx(
187
- ChatInterface,
188
- {
189
- id: conversationId,
190
- variant: "widget",
191
- initialMessages,
192
- onMessagesChange
193
- },
194
- `widget-${conversationId ?? "new"}-${widgetResetKey}`
195
- )
206
+ /* @__PURE__ */ jsx(DialogTitle, { className: "sr-only", children: tr("A11Y_CHAT_TITLE", "aiChat.a11y.title", "AI Chat") }),
207
+ /* @__PURE__ */ jsx("div", { ref: mountExpandedPanel, className: "min-h-0 flex-1" })
196
208
  ]
197
209
  }
210
+ ) }),
211
+ panelContainer && createPortal(
212
+ /* @__PURE__ */ jsxs(Fragment, { children: [
213
+ /* @__PURE__ */ jsxs("div", { className: "flex shrink-0 items-center gap-1.5 px-3 py-1.5 border-b bg-muted/40", children: [
214
+ /* @__PURE__ */ jsx(Sparkles, { className: "h-3 w-3 text-muted-foreground" }),
215
+ pageAIContext ? /* @__PURE__ */ jsx(
216
+ Badge,
217
+ {
218
+ variant: "secondary",
219
+ className: "text-xs",
220
+ "data-testid": "page-context-badge",
221
+ children: pageAIContext.routeName
222
+ }
223
+ ) : /* @__PURE__ */ jsx("span", { className: "text-xs text-muted-foreground font-medium", children: tr("A11Y_CHAT_TITLE", "aiChat.a11y.title", "AI Chat") }),
224
+ /* @__PURE__ */ jsx("div", { className: "flex-1" }),
225
+ expandable && /* @__PURE__ */ jsx(
226
+ Button,
227
+ {
228
+ type: "button",
229
+ variant: "ghost",
230
+ size: "icon",
231
+ className: "h-7 w-7",
232
+ ref: expandRef,
233
+ onClick: () => setExpanded((prev) => !prev),
234
+ "aria-label": expandLabel,
235
+ title: expandLabel,
236
+ "aria-expanded": widgetExpanded,
237
+ children: widgetExpanded ? /* @__PURE__ */ jsx(Minimize2, { className: "h-3.5 w-3.5", "aria-hidden": "true" }) : /* @__PURE__ */ jsx(Maximize2, { className: "h-3.5 w-3.5", "aria-hidden": "true" })
238
+ }
239
+ ),
240
+ /* @__PURE__ */ jsx(
241
+ Button,
242
+ {
243
+ variant: "ghost",
244
+ size: "icon",
245
+ className: "h-5 w-5",
246
+ onClick: () => {
247
+ track({ type: "conversation_cleared" });
248
+ onClear?.();
249
+ setWidgetResetKey((prev) => prev + 1);
250
+ },
251
+ "aria-label": tr(
252
+ "A11Y_CLEAR_CHAT",
253
+ "aiChat.a11y.clearChat",
254
+ "Clear chat"
255
+ ),
256
+ title: tr(
257
+ "A11Y_CLEAR_CHAT",
258
+ "aiChat.a11y.clearChat",
259
+ "Clear chat"
260
+ ),
261
+ children: /* @__PURE__ */ jsx(Trash2, { className: "h-3.5 w-3.5" })
262
+ }
263
+ ),
264
+ /* @__PURE__ */ jsx(
265
+ Button,
266
+ {
267
+ variant: "ghost",
268
+ size: "icon",
269
+ className: "h-5 w-5",
270
+ onClick: () => {
271
+ setWidgetOpen(false);
272
+ setExpanded(false);
273
+ triggerRef.current?.focus();
274
+ track({ type: "widget_closed" });
275
+ },
276
+ "aria-label": tr(
277
+ "A11Y_CLOSE_CHAT",
278
+ "aiChat.a11y.closeChat",
279
+ "Close chat"
280
+ ),
281
+ children: /* @__PURE__ */ jsx(X, { className: "h-3.5 w-3.5" })
282
+ }
283
+ )
284
+ ] }),
285
+ widgetEverOpened && /* @__PURE__ */ jsx(
286
+ ChatInterface,
287
+ {
288
+ id: conversationId,
289
+ variant: "widget",
290
+ initialMessages,
291
+ onMessagesChange
292
+ },
293
+ `widget-${conversationId ?? "new"}-${widgetResetKey}`
294
+ )
295
+ ] }),
296
+ panelContainer
198
297
  ),
199
298
  showTrigger && /* @__PURE__ */ jsx(
200
299
  Button,
201
300
  {
301
+ ref: triggerRef,
202
302
  size: "icon",
203
303
  className: "h-12 w-12 rounded-full shadow-lg",
204
304
  onClick: () => {
205
305
  dismissTip("open");
206
306
  track({ type: widgetOpen ? "widget_closed" : "widget_opened" });
207
307
  setWidgetOpen((prev) => !prev);
308
+ setExpanded(false);
208
309
  setWidgetEverOpened(true);
209
310
  },
210
311
  "aria-label": widgetOpen ? tr("A11Y_CLOSE_CHAT", "aiChat.a11y.closeChat", "Close chat") : tr("A11Y_OPEN_CHAT", "aiChat.a11y.openChat", "Open chat"),
211
312
  "data-testid": "widget-trigger",
212
- children: widgetOpen ? /* @__PURE__ */ jsx(X, { className: "h-5 w-5" }) : /* @__PURE__ */ jsx(Sparkles, { className: "h-5 w-5" })
313
+ children: widgetOpen ? /* @__PURE__ */ jsx(X, { className: "h-5 w-5" }) : triggerIcon ?? /* @__PURE__ */ jsx(Sparkles, { className: "h-5 w-5" })
213
314
  }
214
315
  )
215
316
  ]
@@ -1,4 +1,4 @@
1
- export { j as ChatInput, C as ChatInterface, f as ChatLayout, g as ChatLayoutProps, i as ChatMessage, h as ChatSidebar, k as ToolCallDisplay } from '../../../../shared/stack.xAiy2VtO.cjs';
1
+ export { j as ChatInput, C as ChatInterface, f as ChatLayout, g as ChatLayoutProps, i as ChatMessage, h as ChatSidebar, k as ToolCallDisplay } from '../../../../shared/stack.DzJgoNqJ.cjs';
2
2
  import * as react_jsx_runtime from 'react/jsx-runtime';
3
3
  import { FallbackProps } from 'react-error-boundary';
4
4
  import 'ai';
@@ -1,4 +1,4 @@
1
- export { j as ChatInput, C as ChatInterface, f as ChatLayout, g as ChatLayoutProps, i as ChatMessage, h as ChatSidebar, k as ToolCallDisplay } from '../../../../shared/stack.C5FZr-jJ.mjs';
1
+ export { j as ChatInput, C as ChatInterface, f as ChatLayout, g as ChatLayoutProps, i as ChatMessage, h as ChatSidebar, k as ToolCallDisplay } from '../../../../shared/stack.CHrtGKLz.mjs';
2
2
  import * as react_jsx_runtime from 'react/jsx-runtime';
3
3
  import { FallbackProps } from 'react-error-boundary';
4
4
  import 'ai';
@@ -1,4 +1,4 @@
1
- export { j as ChatInput, C as ChatInterface, f as ChatLayout, g as ChatLayoutProps, i as ChatMessage, h as ChatSidebar, k as ToolCallDisplay } from '../../../../shared/stack.C-eQN_Jt.js';
1
+ export { j as ChatInput, C as ChatInterface, f as ChatLayout, g as ChatLayoutProps, i as ChatMessage, h as ChatSidebar, k as ToolCallDisplay } from '../../../../shared/stack.qMp2VzNw.js';
2
2
  import * as react_jsx_runtime from 'react/jsx-runtime';
3
3
  import { FallbackProps } from 'react-error-boundary';
4
4
  import 'ai';
@@ -3,8 +3,8 @@ import * as _btst_yar from '@btst/yar';
3
3
  import * as react_jsx_runtime from 'react/jsx-runtime';
4
4
  import { ComponentType } from 'react';
5
5
  import { S as SerializedConversation, a as SerializedMessage, A as AiChatIdentityPartition } from '../../../shared/stack.Dgt3MCtW.cjs';
6
- import { A as AiChatMode, a as AiChatPluginOverrides } from '../../../shared/stack.xAiy2VtO.cjs';
7
- export { l as AiChatAnalyticsEvent, b as AiChatProviderConfig, c as AllowedFileType, j as ChatInput, C as ChatInterface, f as ChatLayout, g as ChatLayoutProps, i as ChatMessage, h as ChatSidebar, D as DEFAULT_ALLOWED_FILE_TYPES, k as ToolCallDisplay, T as ToolCallProps, e as ToolCallRenderer, d as ToolCallState } from '../../../shared/stack.xAiy2VtO.cjs';
6
+ import { A as AiChatMode, a as AiChatPluginOverrides } from '../../../shared/stack.DzJgoNqJ.cjs';
7
+ export { l as AiChatAnalyticsEvent, b as AiChatProviderConfig, c as AllowedFileType, j as ChatInput, C as ChatInterface, f as ChatLayout, g as ChatLayoutProps, i as ChatMessage, h as ChatSidebar, D as DEFAULT_ALLOWED_FILE_TYPES, k as ToolCallDisplay, T as ToolCallProps, e as ToolCallRenderer, d as ToolCallState } from '../../../shared/stack.DzJgoNqJ.cjs';
8
8
  export { UIMessage } from 'ai';
9
9
  import '@btst/stack/plugins/api';
10
10
  import '@btst/stack/authorization';
@@ -3,8 +3,8 @@ import * as _btst_yar from '@btst/yar';
3
3
  import * as react_jsx_runtime from 'react/jsx-runtime';
4
4
  import { ComponentType } from 'react';
5
5
  import { S as SerializedConversation, a as SerializedMessage, A as AiChatIdentityPartition } from '../../../shared/stack.B7Mj74BQ.mjs';
6
- import { A as AiChatMode, a as AiChatPluginOverrides } from '../../../shared/stack.C5FZr-jJ.mjs';
7
- export { l as AiChatAnalyticsEvent, b as AiChatProviderConfig, c as AllowedFileType, j as ChatInput, C as ChatInterface, f as ChatLayout, g as ChatLayoutProps, i as ChatMessage, h as ChatSidebar, D as DEFAULT_ALLOWED_FILE_TYPES, k as ToolCallDisplay, T as ToolCallProps, e as ToolCallRenderer, d as ToolCallState } from '../../../shared/stack.C5FZr-jJ.mjs';
6
+ import { A as AiChatMode, a as AiChatPluginOverrides } from '../../../shared/stack.CHrtGKLz.mjs';
7
+ export { l as AiChatAnalyticsEvent, b as AiChatProviderConfig, c as AllowedFileType, j as ChatInput, C as ChatInterface, f as ChatLayout, g as ChatLayoutProps, i as ChatMessage, h as ChatSidebar, D as DEFAULT_ALLOWED_FILE_TYPES, k as ToolCallDisplay, T as ToolCallProps, e as ToolCallRenderer, d as ToolCallState } from '../../../shared/stack.CHrtGKLz.mjs';
8
8
  export { UIMessage } from 'ai';
9
9
  import '@btst/stack/plugins/api';
10
10
  import '@btst/stack/authorization';