@mordn/chat-widget 0.4.2 → 0.5.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/dist/index.d.mts CHANGED
@@ -67,6 +67,21 @@ interface ChatWidgetConfig {
67
67
  * (e.g. setting their open state, navigating away).
68
68
  */
69
69
  onClose?: () => void;
70
+ /**
71
+ * Controlled open state for `popup` layout. When provided, the consumer
72
+ * owns the show/hide lifecycle and the widget will NOT render its own
73
+ * floating toggle button (FAB). The consumer renders whatever trigger
74
+ * they want and toggles `open` themselves.
75
+ *
76
+ * Leave undefined to use the widget's built-in uncontrolled behaviour
77
+ * (FAB appears when closed, click opens, internal state).
78
+ */
79
+ open?: boolean;
80
+ /**
81
+ * Called when the widget wants to change its open state (e.g. user
82
+ * clicked the close X). Required when using `open` (controlled mode).
83
+ */
84
+ onOpenChange?: (open: boolean) => void;
70
85
  /**
71
86
  * Custom buttons rendered in the widget header next to the close X.
72
87
  * Use this for "expand to full page", "settings", or any consumer-defined
@@ -211,7 +226,7 @@ interface ChatWidgetProps extends ChatWidgetConfig {
211
226
  */
212
227
  widgetId?: string;
213
228
  }
214
- declare function ChatWidget({ userId, conversationId, initialMessages, className, model, systemPrompt, temperature, theme, features, display, starterPrompts, onClose, headerActions, }: ChatWidgetProps): react_jsx_runtime.JSX.Element;
229
+ declare function ChatWidget({ userId, conversationId, initialMessages, className, model, systemPrompt, temperature, theme, features, display, starterPrompts, onClose, headerActions, open, onOpenChange, }: ChatWidgetProps): react_jsx_runtime.JSX.Element;
215
230
 
216
231
  interface ChatTheme {
217
232
  lightPrimary: string;
package/dist/index.d.ts CHANGED
@@ -67,6 +67,21 @@ interface ChatWidgetConfig {
67
67
  * (e.g. setting their open state, navigating away).
68
68
  */
69
69
  onClose?: () => void;
70
+ /**
71
+ * Controlled open state for `popup` layout. When provided, the consumer
72
+ * owns the show/hide lifecycle and the widget will NOT render its own
73
+ * floating toggle button (FAB). The consumer renders whatever trigger
74
+ * they want and toggles `open` themselves.
75
+ *
76
+ * Leave undefined to use the widget's built-in uncontrolled behaviour
77
+ * (FAB appears when closed, click opens, internal state).
78
+ */
79
+ open?: boolean;
80
+ /**
81
+ * Called when the widget wants to change its open state (e.g. user
82
+ * clicked the close X). Required when using `open` (controlled mode).
83
+ */
84
+ onOpenChange?: (open: boolean) => void;
70
85
  /**
71
86
  * Custom buttons rendered in the widget header next to the close X.
72
87
  * Use this for "expand to full page", "settings", or any consumer-defined
@@ -211,7 +226,7 @@ interface ChatWidgetProps extends ChatWidgetConfig {
211
226
  */
212
227
  widgetId?: string;
213
228
  }
214
- declare function ChatWidget({ userId, conversationId, initialMessages, className, model, systemPrompt, temperature, theme, features, display, starterPrompts, onClose, headerActions, }: ChatWidgetProps): react_jsx_runtime.JSX.Element;
229
+ declare function ChatWidget({ userId, conversationId, initialMessages, className, model, systemPrompt, temperature, theme, features, display, starterPrompts, onClose, headerActions, open, onOpenChange, }: ChatWidgetProps): react_jsx_runtime.JSX.Element;
215
230
 
216
231
  interface ChatTheme {
217
232
  lightPrimary: string;
package/dist/index.js CHANGED
@@ -520,7 +520,11 @@ var PromptInput = ({
520
520
  "form",
521
521
  {
522
522
  className: cn(
523
- "w-full divide-y divide-[hsl(var(--chat-text)/0.1)] focus-within:divide-ring overflow-hidden rounded-xl border border-[hsl(var(--chat-text)/0.1)] bg-background focus-within:border-ring transition-colors",
523
+ // The form border + horizontal divider colour both come from
524
+ // the styles.src.css `.chat-widget-container form ...` rules so
525
+ // there's a single token (--chat-divider) controlling both.
526
+ // No utility classes for border colour here.
527
+ "w-full overflow-hidden rounded-xl bg-background transition-colors",
524
528
  "[&:focus-within]:shadow-none [&:focus]:shadow-none shadow-none",
525
529
  className
526
530
  ),
@@ -1194,7 +1198,7 @@ var CodeBlock = ({
1194
1198
  "div",
1195
1199
  {
1196
1200
  className: cn(
1197
- "relative w-full overflow-hidden rounded-lg bg-[hsl(var(--chat-text)/0.03)] border border-[hsl(var(--chat-text)/0.1)]",
1201
+ "relative w-full overflow-hidden rounded-lg bg-[hsl(var(--chat-text)/0.03)] border border-[var(--chat-divider)]",
1198
1202
  className
1199
1203
  ),
1200
1204
  ...props,
@@ -1261,7 +1265,7 @@ var import_jsx_runtime17 = require("react/jsx-runtime");
1261
1265
  var Tool = ({ className, ...props }) => /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(
1262
1266
  Collapsible,
1263
1267
  {
1264
- className: cn("not-prose w-full rounded-md border border-[hsl(var(--chat-text)/0.1)]", className),
1268
+ className: cn("not-prose w-full rounded-md border border-[var(--chat-divider)]", className),
1265
1269
  ...props
1266
1270
  }
1267
1271
  );
@@ -1287,6 +1291,7 @@ var ToolHeader = ({
1287
1291
  className,
1288
1292
  title,
1289
1293
  type,
1294
+ toolName,
1290
1295
  state,
1291
1296
  ...props
1292
1297
  }) => /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)(
@@ -1300,7 +1305,7 @@ var ToolHeader = ({
1300
1305
  children: [
1301
1306
  /* @__PURE__ */ (0, import_jsx_runtime17.jsxs)("div", { className: "flex items-center gap-2", children: [
1302
1307
  /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_lucide_react9.WrenchIcon, { className: "size-4 text-muted-foreground" }),
1303
- /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("span", { className: "font-medium text-sm", children: title ?? type.split("-").slice(1).join("-") }),
1308
+ /* @__PURE__ */ (0, import_jsx_runtime17.jsx)("span", { className: "font-medium text-sm", children: title ?? toolName ?? type.split("-").slice(1).join("-") }),
1304
1309
  getStatusBadge(state)
1305
1310
  ] }),
1306
1311
  /* @__PURE__ */ (0, import_jsx_runtime17.jsx)(import_lucide_react9.ChevronDownIcon, { className: "size-4 text-muted-foreground transition-transform group-data-[state=open]:rotate-180" })
@@ -1379,7 +1384,10 @@ function StarterMessages({
1379
1384
  onClick: () => onPromptSelect(prompt)
1380
1385
  }
1381
1386
  ),
1382
- index < prompts.length - 1 && /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { className: "h-px bg-[hsl(var(--chat-text)/0.08)] mx-3" })
1387
+ index < prompts.length - 1 && // 1px-tall element used as a divider same --chat-divider token
1388
+ // every other separator in the widget uses, so consumers only
1389
+ // need to override one variable to recolour all of them.
1390
+ /* @__PURE__ */ (0, import_jsx_runtime18.jsx)("div", { className: "h-px mx-3", style: { backgroundColor: "var(--chat-divider)" } })
1383
1391
  ] }, index))
1384
1392
  }
1385
1393
  );
@@ -1452,6 +1460,7 @@ function ChatInterface({ id, initialMessages, config, onClose, headerActions } =
1452
1460
  const [activeTabId, setActiveTabId] = (0, import_react9.useState)("");
1453
1461
  const [initialTabCreated, setInitialTabCreated] = (0, import_react9.useState)(false);
1454
1462
  const [isInitializing, setIsInitializing] = (0, import_react9.useState)(true);
1463
+ const [isLoadingMessages, setIsLoadingMessages] = (0, import_react9.useState)(false);
1455
1464
  const lastSyncedTabId = (0, import_react9.useRef)("");
1456
1465
  const hasInitialized = (0, import_react9.useRef)(false);
1457
1466
  const { messages, sendMessage, status, setMessages } = (0, import_react11.useChat)({
@@ -1631,7 +1640,12 @@ function ChatInterface({ id, initialMessages, config, onClose, headerActions } =
1631
1640
  }))
1632
1641
  );
1633
1642
  setActiveTabId(tabId);
1634
- await loadConversation(tabId);
1643
+ setIsLoadingMessages(true);
1644
+ try {
1645
+ await loadConversation(tabId);
1646
+ } finally {
1647
+ setIsLoadingMessages(false);
1648
+ }
1635
1649
  };
1636
1650
  const closeTab = (tabId) => {
1637
1651
  if (tabs.length <= 1) return;
@@ -1856,7 +1870,14 @@ function ChatInterface({ id, initialMessages, config, onClose, headerActions } =
1856
1870
  if (part.type.startsWith("tool-") || part.type === "dynamic-tool") {
1857
1871
  const toolPart = part;
1858
1872
  return /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)(Tool, { children: [
1859
- /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(ToolHeader, { type: part.type, state: toolPart.state }),
1873
+ /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
1874
+ ToolHeader,
1875
+ {
1876
+ type: part.type,
1877
+ toolName: toolPart.toolName,
1878
+ state: toolPart.state
1879
+ }
1880
+ ),
1860
1881
  /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)(ToolContent, { children: [
1861
1882
  /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(ToolInput, { input: toolPart.input }),
1862
1883
  /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(ToolOutput, { output: toolPart.output, errorText: toolPart.errorText })
@@ -1922,7 +1943,7 @@ function ChatInterface({ id, initialMessages, config, onClose, headerActions } =
1922
1943
  ),
1923
1944
  children: [
1924
1945
  /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)("div", { className: "flex items-center gap-2 px-3 py-2 border-b backdrop-blur-sm relative z-20", style: {
1925
- borderColor: "var(--chat-border-soft)",
1946
+ borderColor: "var(--chat-divider)",
1926
1947
  backgroundColor: "var(--chat-header-bg)"
1927
1948
  }, children: [
1928
1949
  /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("div", { className: "flex items-center gap-1 flex-1 min-w-0 overflow-x-auto scrollbar-hide py-0.5 scroll-smooth", children: tabs.map((tab, index) => /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)(
@@ -2024,10 +2045,10 @@ function ChatInterface({ id, initialMessages, config, onClose, headerActions } =
2024
2045
  ),
2025
2046
  showHistory && /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)("div", { className: "absolute right-0 top-full mt-1.5 w-72 rounded-xl shadow-[0_4px_24px_rgba(0,0,0,0.08)] dark:shadow-[0_4px_24px_rgba(0,0,0,0.3)] z-50 animate-in fade-in slide-in-from-top-1 duration-150 overflow-hidden", style: {
2026
2047
  backgroundColor: "hsl(var(--chat-background))",
2027
- border: `1px solid ${"var(--chat-border-medium)"}`
2048
+ border: `1px solid ${"var(--chat-divider)"}`
2028
2049
  }, children: [
2029
2050
  /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("div", { className: "p-2.5 border-b", style: {
2030
- borderColor: "var(--chat-border-soft)",
2051
+ borderColor: "var(--chat-divider)",
2031
2052
  backgroundColor: "var(--chat-overlay)"
2032
2053
  }, children: /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("div", { className: "relative", children: /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
2033
2054
  "input",
@@ -2039,7 +2060,7 @@ function ChatInterface({ id, initialMessages, config, onClose, headerActions } =
2039
2060
  className: "w-full h-7 px-2.5 text-[13px] rounded-lg focus:outline-none transition-all",
2040
2061
  style: {
2041
2062
  backgroundColor: "hsl(var(--chat-surface-deep))",
2042
- border: `1px solid ${"var(--chat-border-medium)"}`,
2063
+ border: `1px solid ${"var(--chat-divider)"}`,
2043
2064
  color: "hsl(var(--chat-text))"
2044
2065
  }
2045
2066
  }
@@ -2136,7 +2157,7 @@ function ChatInterface({ id, initialMessages, config, onClose, headerActions } =
2136
2157
  ] }),
2137
2158
  /* @__PURE__ */ (0, import_jsx_runtime20.jsxs)("div", { className: "px-5 pb-5", children: [
2138
2159
  uploadError && /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("div", { className: "mb-3 px-4 py-3 bg-red-50 dark:bg-red-900/20 border border-red-200/60 dark:border-red-800/60 rounded-2xl text-sm text-red-700 dark:text-red-400 shadow-sm", children: uploadError }),
2139
- isInitializing ? /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("div", { className: "flex items-center justify-center py-8", role: "status", "aria-label": "Loading conversation", children: /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("div", { className: "h-4 w-4 rounded-full border-2 border-current border-t-transparent animate-spin", style: { color: "hsl(var(--chat-text-muted))" } }) }) : messages.length === 0 && status !== "submitted" && config?.starterPrompts && /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
2160
+ isInitializing || isLoadingMessages ? /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("div", { className: "flex items-center justify-center py-8", role: "status", "aria-label": "Loading conversation", children: /* @__PURE__ */ (0, import_jsx_runtime20.jsx)("div", { className: "h-4 w-4 rounded-full border-2 border-current border-t-transparent animate-spin", style: { color: "hsl(var(--chat-text-muted))" } }) }) : messages.length === 0 && status !== "submitted" && config?.starterPrompts && /* @__PURE__ */ (0, import_jsx_runtime20.jsx)(
2140
2161
  StarterMessages,
2141
2162
  {
2142
2163
  prompts: config.starterPrompts,
@@ -2220,15 +2241,26 @@ function ChatWidget({
2220
2241
  display,
2221
2242
  starterPrompts,
2222
2243
  onClose,
2223
- headerActions
2244
+ headerActions,
2245
+ open,
2246
+ onOpenChange
2224
2247
  }) {
2225
2248
  const layout = display?.layout || "popup";
2226
- const showToggleButton = display?.showToggleButton !== false;
2249
+ const isControlled = open !== void 0;
2250
+ const showToggleButton = !isControlled && display?.showToggleButton !== false;
2227
2251
  const resizable = layout === "popup" && display?.resizable !== false;
2228
2252
  const size = display?.size || "default";
2229
- const [isOpen, setIsOpen] = (0, import_react12.useState)(
2253
+ const [internalIsOpen, setInternalIsOpen] = (0, import_react12.useState)(
2230
2254
  layout !== "popup" ? true : display?.defaultOpen || false
2231
2255
  );
2256
+ const isOpen = isControlled ? open : internalIsOpen;
2257
+ const setIsOpen = (next) => {
2258
+ if (isControlled) {
2259
+ onOpenChange?.(next);
2260
+ } else {
2261
+ setInternalIsOpen(next);
2262
+ }
2263
+ };
2232
2264
  const [isResizing, setIsResizing] = (0, import_react12.useState)(false);
2233
2265
  const containerRef = (0, import_react12.useRef)(null);
2234
2266
  const customStyles = (0, import_react12.useMemo)(() => {