@parallelworks/ui 0.25.0 → 0.27.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/ai/ai.css CHANGED
@@ -105,9 +105,10 @@
105
105
  border-radius: 0.5rem;
106
106
  }
107
107
 
108
- /* Streamdown tables: the body's own type,
109
- * hairlines at a tenth of the ink, a header on a five-percent tint, and the
110
- * copy/download controls only while the pointer is over the table. */
108
+ /* Streamdown tables: the body's own type, open rows on hairlines at a tenth
109
+ * of the ink under a firmer rule for the header (no frame, no fill, so a
110
+ * table reads as part of the answer rather than a widget dropped into it),
111
+ * and the copy/download controls only while the pointer is over the table. */
111
112
  .markdown [data-streamdown="table-wrapper"] {
112
113
  --table-line: color-mix(in oklab, var(--theme-panel, #000) 10%, transparent);
113
114
  position: relative;
@@ -136,8 +137,8 @@
136
137
  }
137
138
  .markdown [data-streamdown="table-wrapper"] > div:last-child {
138
139
  padding: 0;
139
- border: 1px solid var(--table-line);
140
- border-radius: 0.5rem;
140
+ border: 0;
141
+ border-radius: 0;
141
142
  background: transparent;
142
143
  }
143
144
  .markdown [data-streamdown="table-wrapper"] table {
@@ -148,10 +149,10 @@
148
149
  .markdown [data-streamdown="table"] th {
149
150
  padding: 0.4375rem 0.75rem;
150
151
  font-size: 1rem;
151
- font-weight: 500;
152
+ font-weight: 600;
152
153
  color: inherit;
153
- background: color-mix(in oklab, var(--theme-panel, #000) 5%, transparent);
154
- border-bottom: 1px solid var(--table-line);
154
+ background: transparent;
155
+ border-bottom: 1px solid color-mix(in oklab, var(--theme-panel, #000) 28%, transparent);
155
156
  }
156
157
  .markdown [data-streamdown="table"] td {
157
158
  padding: 0.4375rem 0.75rem;
@@ -163,6 +164,16 @@
163
164
  /* A prose column keeps enough room to read; a wide table scrolls instead. */
164
165
  min-width: 9rem;
165
166
  }
167
+ .markdown [data-streamdown="table"] thead {
168
+ background: transparent;
169
+ }
170
+ /* Unframed, the outer columns line up with the prose around the table. */
171
+ .markdown [data-streamdown="table"] :is(th, td):first-child {
172
+ padding-left: 0;
173
+ }
174
+ .markdown [data-streamdown="table"] :is(th, td):last-child {
175
+ padding-right: 0;
176
+ }
166
177
  .markdown [data-streamdown="table"] tbody tr {
167
178
  border-bottom: 1px solid var(--table-line);
168
179
  }
@@ -175,24 +186,54 @@
175
186
  }
176
187
  }
177
188
 
178
- /* Streamdown code blocks — tighten spacing, remove inner double-border */
189
+ /* Streamdown code blocks: one surface, not a frame around an inset panel.
190
+ * A wash of the ink with a hairline edge holds the language on its top edge,
191
+ * the copy and download controls quiet beside it, and the code on the same
192
+ * ground, so the block reads as part of the answer. */
179
193
  .markdown [data-streamdown="code-block"] {
180
- padding: 0.375rem 0.5rem 0;
181
- margin: 0.5em 0;
182
194
  gap: 0;
195
+ margin: 0.875rem 0;
196
+ padding: 0;
197
+ border: 0;
198
+ border-radius: 0.75rem;
199
+ overflow: hidden;
200
+ background: color-mix(in oklab, var(--theme-panel, #000) 4%, var(--theme-panel-bg, #fff));
201
+ box-shadow: 0 0 0 1px color-mix(in oklab, var(--theme-panel, #000) 8%, transparent);
183
202
  }
184
203
  .markdown [data-streamdown="code-block-header"] {
185
- padding: 0.25rem 0.25rem;
204
+ height: 2.25rem;
205
+ padding: 0 0.75rem 0 0.75rem;
206
+ font-size: 0.75rem;
207
+ color: var(--theme-muted-text-color, #6a7282);
208
+ }
209
+ .markdown [data-streamdown="code-block-actions"] {
210
+ gap: 0.125rem;
211
+ padding: 0;
212
+ border: 0;
213
+ background: transparent;
214
+ backdrop-filter: none;
215
+ }
216
+ .markdown [data-streamdown="code-block-actions"] button {
217
+ padding: 0.3125rem;
218
+ border-radius: 0.375rem;
219
+ color: var(--theme-muted-text-color, #6a7282);
220
+ transition:
221
+ background-color 150ms ease-out,
222
+ color 150ms ease-out;
223
+ }
224
+ .markdown [data-streamdown="code-block-actions"] button:hover {
225
+ color: var(--theme-panel, #000);
226
+ background: color-mix(in oklab, var(--theme-panel, #000) 7%, transparent);
186
227
  }
187
228
  .markdown [data-streamdown="code-block-body"] {
188
- border: none;
189
- border-radius: 0 0 0.5rem 0.5rem;
229
+ border: 0;
230
+ border-radius: 0;
190
231
  padding: 0;
232
+ background: transparent;
191
233
  }
192
- /* Remove prose pre margin and code block body inner pre margin */
193
234
  .markdown [data-streamdown="code-block-body"] pre {
194
235
  margin: 0;
195
- padding: 0.75rem 1rem;
236
+ padding: 0.125rem 1rem 0.875rem;
196
237
  overflow-x: auto;
197
238
  /* Code reads looser than prose; Streamdown's text-sm line is too tight. */
198
239
  line-height: 1.6;
@@ -206,20 +247,21 @@
206
247
  explicitly to ensure correct colors in all modes.
207
248
  See: https://shiki.style/guide/dual-themes#class-based-dark-mode */
208
249
 
209
- /* Light mode: use the primary Shiki theme (--sdm-bg/--sdm-c) */
250
+ /* Light mode: the primary Shiki theme's colours (--sdm-c) on the block's own
251
+ surface, which stands in for the theme's background. */
210
252
  .markdown [data-streamdown="code-block-body"] pre {
211
253
  /* biome-ignore lint/complexity/noImportantStyles: the highlighter sets the background as an inline style */
212
- background-color: var(--sdm-bg, inherit) !important;
254
+ background-color: transparent !important;
213
255
  color: var(--sdm-fg, inherit);
214
256
  }
215
257
  .markdown [data-streamdown="code-block-body"] pre code > span > span {
216
258
  color: var(--sdm-c, inherit);
217
259
  }
218
260
 
219
- /* Dark mode: switch to --shiki-dark variants */
261
+ /* Dark mode: the --shiki-dark colours, again on the block's surface. */
220
262
  .dark .markdown [data-streamdown="code-block-body"] pre {
221
263
  /* biome-ignore lint/complexity/noImportantStyles: the highlighter sets the background as an inline style */
222
- background-color: var(--shiki-dark-bg, var(--sdm-bg, inherit)) !important;
264
+ background-color: transparent !important;
223
265
  color: var(--shiki-dark, var(--sdm-fg, inherit));
224
266
  }
225
267
  .dark .markdown [data-streamdown="code-block-body"] pre code > span > span {
@@ -301,7 +343,7 @@
301
343
  background-image: linear-gradient(
302
344
  90deg,
303
345
  var(--theme-muted-text-color, #6a7282) 0%,
304
- var(--theme-app, #d1d5db) 20%,
346
+ var(--theme-accent, #06354f) 20%,
305
347
  var(--theme-muted-text-color, #6a7282) 40%
306
348
  );
307
349
  -webkit-background-clip: text;
@@ -328,3 +370,137 @@
328
370
  animation: none;
329
371
  }
330
372
  }
373
+
374
+ /* Tints of the ink for controls that sit on the panel: the theme's hover is a
375
+ * shade off another surface and would not read as a highlight here. */
376
+ @utility chat-tint {
377
+ background-color: color-mix(in oklab, var(--theme-panel, #000) 6%, transparent);
378
+ }
379
+ @utility chat-tint-strong {
380
+ background-color: color-mix(in oklab, var(--theme-panel, #000) 10%, transparent);
381
+ }
382
+
383
+ /* The composer is one surface lifted off the page: a hairline of the ink for
384
+ * its edge, and a short, soft shadow that deepens while it has focus. The
385
+ * shadow is black on every scheme; an ink-coloured one would glow on dark. */
386
+ .chat-composer {
387
+ box-shadow:
388
+ 0 0 0 1px color-mix(in oklab, var(--theme-panel, #000) 12%, transparent),
389
+ 0 1px 2px rgb(0 0 0 / 0.04),
390
+ 0 6px 16px -8px rgb(0 0 0 / 0.12);
391
+ transition: box-shadow 200ms cubic-bezier(0.16, 1, 0.3, 1);
392
+ }
393
+ .chat-composer:focus-within {
394
+ box-shadow:
395
+ 0 0 0 1px color-mix(in oklab, var(--theme-panel, #000) 22%, transparent),
396
+ 0 1px 2px rgb(0 0 0 / 0.05),
397
+ 0 10px 24px -10px rgb(0 0 0 / 0.18);
398
+ }
399
+
400
+ /* The thread runs under the docked composer and fades out above it, so the
401
+ * last lines dissolve rather than cut off at the dock's edge. */
402
+ .chat-composer-dock::before {
403
+ content: "";
404
+ position: absolute;
405
+ inset-inline: 0;
406
+ bottom: 100%;
407
+ height: 1.5rem;
408
+ background: linear-gradient(to top, var(--theme-panel-bg, #fff), transparent);
409
+ pointer-events: none;
410
+ }
411
+
412
+ @media (prefers-reduced-motion: reduce) {
413
+ .chat-composer {
414
+ transition: none;
415
+ }
416
+ }
417
+
418
+ /* The tail of the reasoning while it streams fades in from the top, so the
419
+ * lines scrolling out read as more above rather than as cut off. */
420
+ .chat-reasoning-tail {
421
+ mask-image: linear-gradient(to bottom, transparent, black 1.25rem);
422
+ -webkit-mask-image: linear-gradient(to bottom, transparent, black 1.25rem);
423
+ }
424
+
425
+ /* An edge drawn from the ink rather than the theme's border, so a card reads
426
+ * the same on the panel and on a tinted bubble. */
427
+ .chat-hairline {
428
+ box-shadow: 0 0 0 1px color-mix(in oklab, var(--theme-panel, #000) 10%, transparent);
429
+ }
430
+
431
+ /* A bubble carrying pastes would shrink to its shortest line and squeeze
432
+ * its cards and blocks into a column; give it room for them to read. */
433
+ .chat-bubble-roomy {
434
+ min-width: min(100%, 24rem);
435
+ }
436
+
437
+ /* ---- Accent ---------------------------------------------------------------
438
+ * The host's accent is the chat's signature: the send button, the caret, the
439
+ * focus ring, the thinking shimmer and the tint on what the reader wrote. Everything else stays ink, so the accent always means "this is the
440
+ * conversation's own voice or the reader's next move". */
441
+ .chat-thread,
442
+ .chat-composer,
443
+ .chat-empty,
444
+ .chat-reasoning-body {
445
+ --chat-accent: var(--theme-accent, var(--theme-element, #06354f));
446
+ }
447
+
448
+ /* What the reader wrote carries a wash of the accent: theirs, not the
449
+ * assistant's, at a glance. */
450
+ .chat-bubble-user {
451
+ background-color: color-mix(
452
+ in oklab,
453
+ var(--theme-accent, #06354f) 9%,
454
+ var(--theme-panel-bg, #fff)
455
+ );
456
+ }
457
+
458
+ /* The reader's next move is the accent: the caret in the box, its ring on
459
+ * focus, and (in ChatInput) the filled send button. */
460
+ .chat-composer textarea {
461
+ caret-color: var(--chat-accent);
462
+ }
463
+ .chat-composer:focus-within {
464
+ box-shadow:
465
+ 0 0 0 1px color-mix(in oklab, var(--chat-accent) 55%, transparent),
466
+ 0 0 0 4px color-mix(in oklab, var(--chat-accent) 12%, transparent),
467
+ 0 10px 24px -10px rgb(0 0 0 / 0.18);
468
+ }
469
+ @utility chat-send-idle {
470
+ background-color: color-mix(in oklab, var(--theme-accent, #06354f) 14%, transparent);
471
+ color: color-mix(in oklab, var(--theme-accent, #06354f) 70%, var(--theme-panel-bg, #fff));
472
+ }
473
+
474
+ /* The reply being written ends in the accent's caret. */
475
+ .chat-thread [style*="--streamdown-caret"] > :last-child::after {
476
+ color: var(--chat-accent);
477
+ }
478
+
479
+ /* Selected text inside the chat takes the accent too. */
480
+ .chat-thread ::selection,
481
+ .chat-composer ::selection {
482
+ background-color: color-mix(in oklab, var(--chat-accent) 22%, transparent);
483
+ }
484
+
485
+ /* A thought hangs off a line of the accent rather than a gray rule. */
486
+ .chat-reasoning-body {
487
+ border-left: 1px solid color-mix(in oklab, var(--theme-accent, #06354f) 35%, transparent);
488
+ }
489
+
490
+ /* A paste in the reader's bubble is a code block like the assistant's, set on
491
+ * the panel so it stands off the bubble's tint, with the bubble's spacing
492
+ * rather than the answer's. */
493
+ .chat-paste-code .markdown [data-streamdown="code-block"] {
494
+ margin: 0;
495
+ background: var(--theme-panel-bg, #fff);
496
+ white-space: normal;
497
+ }
498
+
499
+ /* A queued message is written but not yet sent: its own outline, dashed,
500
+ * where a sent one has its fill, so it reads as pending without the text
501
+ * losing contrast. */
502
+ .chat-queued .chat-bubble-user {
503
+ background: transparent;
504
+ outline: 1px dashed color-mix(in oklab, var(--theme-panel, #000) 30%, transparent);
505
+ outline-offset: -1px;
506
+ }
@@ -40,10 +40,10 @@ function AllocationSelector({ variant = "default" } = {}) {
40
40
  value: selectedAllocation ?? "",
41
41
  onChange: (v) => setSelectedAllocation(v || null),
42
42
  variant,
43
- textBoxClassName: bare ? "rounded-md px-1.5 py-0.5 text-[11px] hover:theme-muted-panel" : "h-8 rounded-lg border theme-border text-xs px-2",
43
+ textBoxClassName: bare ? "rounded-full h-8 px-2.5 text-[13px] hover:chat-tint" : "h-8 rounded-lg border theme-border text-xs px-2",
44
44
  placeholder: "Select allocation"
45
45
  }), selected && /* @__PURE__ */ jsxs("span", {
46
- className: "text-[10px] theme-muted-text whitespace-nowrap",
46
+ className: "text-xs tabular-nums theme-muted-text whitespace-nowrap",
47
47
  children: [
48
48
  "$",
49
49
  (selected.used ?? 0).toFixed(2),
@@ -25,19 +25,19 @@ function BranchNavigator({ messages, currentMessageId, onNavigate }) {
25
25
  if (hasNext && next) onNavigate(next.id);
26
26
  };
27
27
  return /* @__PURE__ */ jsxs("div", {
28
- className: "inline-flex items-center gap-1 text-xs theme-muted-text",
28
+ className: "inline-flex items-center text-xs theme-muted-text",
29
29
  children: [
30
30
  /* @__PURE__ */ jsx("button", {
31
31
  type: "button",
32
32
  onClick: handlePrev,
33
33
  disabled: !hasPrev,
34
- className: "p-0.5 rounded theme-hover disabled:opacity-30 disabled:cursor-not-allowed",
34
+ className: "p-1.5 rounded-md transition-colors enabled:hover:chat-tint enabled:hover:theme-text disabled:opacity-30 disabled:cursor-not-allowed",
35
35
  title: t.previous,
36
36
  "aria-label": t.previous,
37
- children: /* @__PURE__ */ jsx(FaAngleLeft, { className: "w-3 h-3" })
37
+ children: /* @__PURE__ */ jsx(FaAngleLeft, { className: "w-3.5 h-3.5" })
38
38
  }),
39
39
  /* @__PURE__ */ jsxs("span", {
40
- className: "min-w-[3ch] text-center",
40
+ className: "min-w-[3ch] text-center tabular-nums",
41
41
  children: [
42
42
  currentIndex + 1,
43
43
  "/",
@@ -48,10 +48,10 @@ function BranchNavigator({ messages, currentMessageId, onNavigate }) {
48
48
  type: "button",
49
49
  onClick: handleNext,
50
50
  disabled: !hasNext,
51
- className: "p-0.5 rounded theme-hover disabled:opacity-30 disabled:cursor-not-allowed",
51
+ className: "p-1.5 rounded-md transition-colors enabled:hover:chat-tint enabled:hover:theme-text disabled:opacity-30 disabled:cursor-not-allowed",
52
52
  title: t.next,
53
53
  "aria-label": t.next,
54
- children: /* @__PURE__ */ jsx(FaAngleRight, { className: "w-3 h-3" })
54
+ children: /* @__PURE__ */ jsx(FaAngleRight, { className: "w-3.5 h-3.5" })
55
55
  })
56
56
  ]
57
57
  });
@@ -12,7 +12,6 @@ import { ComposerControls, ConnectToolsLink } from "./ComposerChrome.js";
12
12
  import DragOverlay from "./DragOverlay.js";
13
13
  import ChatInput from "./ChatInput.js";
14
14
  import ProviderIssueBanner from "./ProviderIssueBanner.js";
15
- import cx from "classnames";
16
15
  import { useCallback, useEffect, useMemo, useRef, useState } from "react";
17
16
  import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
18
17
  //#region src/ai/components/ChatEmptyState.tsx
@@ -71,7 +70,7 @@ function ChatEmptyState({ targetSession }) {
71
70
  const { isDragging, dragHandlers } = useDragDrop({ onFilesDropped: handleFilesDropped });
72
71
  const attachmentsAvailable = !!adapter.attachments;
73
72
  return /* @__PURE__ */ jsx("div", {
74
- className: "flex h-full bg-(--theme-panel-bg)",
73
+ className: "chat-empty flex h-full bg-(--theme-panel-bg)",
75
74
  children: /* @__PURE__ */ jsxs("div", {
76
75
  className: "flex-1 flex flex-col min-w-0",
77
76
  children: [
@@ -110,31 +109,31 @@ function ChatEmptyState({ targetSession }) {
110
109
  /* @__PURE__ */ jsxs("div", {
111
110
  className: "flex-1 flex flex-col relative bg-(--theme-panel-bg)",
112
111
  ...attachmentsAvailable ? dragHandlers : {},
113
- children: [
114
- isDragging && attachmentsAvailable && /* @__PURE__ */ jsx(DragOverlay, { className: "rounded-2xl m-4" }),
115
- /* @__PURE__ */ jsx("div", {
116
- className: "flex-1 flex flex-col items-center justify-center p-8",
117
- children: /* @__PURE__ */ jsxs("div", {
118
- className: "max-w-2xl w-full",
119
- children: [!(hasLoadedModels && models.length === 0) && /* @__PURE__ */ jsxs("div", {
120
- className: "text-center mb-10",
112
+ children: [isDragging && attachmentsAvailable && /* @__PURE__ */ jsx(DragOverlay, { className: "rounded-2xl m-4" }), /* @__PURE__ */ jsx("div", {
113
+ className: "flex-1 flex flex-col items-center justify-center pt-8 pb-[12vh]",
114
+ children: /* @__PURE__ */ jsxs("div", {
115
+ className: "w-full",
116
+ children: [
117
+ !(hasLoadedModels && models.length === 0) && /* @__PURE__ */ jsxs("div", {
118
+ className: "mx-auto mb-6 max-w-[50rem] px-8 text-center",
121
119
  children: [/* @__PURE__ */ jsx("h1", {
122
- className: cx("text-2xl font-semibold text-(--theme-panel)", "tracking-tight"),
120
+ className: "text-[1.75rem] leading-tight font-medium tracking-[-0.02em] text-balance text-(--theme-panel)",
123
121
  children: greeting
124
122
  }), /* @__PURE__ */ jsx("p", {
125
123
  className: "mt-2 text-sm theme-muted-text",
126
124
  children: t.modernSubtitle
127
125
  })]
128
- }), hasLoadedModels && models.length === 0 ? /* @__PURE__ */ jsxs("div", {
129
- className: "flex flex-col items-center justify-center gap-4 py-8",
126
+ }),
127
+ hasLoadedModels && models.length === 0 && /* @__PURE__ */ jsxs("div", {
128
+ className: "mx-auto mb-6 flex max-w-md flex-col items-center gap-3 px-8 text-center",
130
129
  children: [
131
- /* @__PURE__ */ jsx(FaCloud, { className: "h-16 w-16 theme-muted-text opacity-50" }),
130
+ /* @__PURE__ */ jsx(FaCloud, { className: "h-10 w-10 theme-muted-text opacity-60" }),
132
131
  /* @__PURE__ */ jsx("p", {
133
- className: "text-lg theme-muted-text",
132
+ className: "text-lg font-medium theme-text",
134
133
  children: t.noProvidersTitle
135
134
  }),
136
135
  /* @__PURE__ */ jsx("p", {
137
- className: "text-sm theme-muted-text text-center max-w-md",
136
+ className: "text-sm theme-muted-text",
138
137
  children: t.noProvidersBody
139
138
  }),
140
139
  extraLinks.addProvider && /* @__PURE__ */ jsxs(LinkComponent, {
@@ -142,31 +141,35 @@ function ChatEmptyState({ targetSession }) {
142
141
  kind: "external",
143
142
  href: extraLinks.addProvider
144
143
  },
145
- className: "inline-flex items-center gap-2 px-4 py-2 rounded-lg transition-colors text-sm font-medium theme-element hover:opacity-90",
144
+ className: "mt-1 inline-flex items-center gap-2 px-4 py-2 rounded-full transition-opacity text-sm font-medium theme-element hover:opacity-90",
146
145
  children: [/* @__PURE__ */ jsx(FaPlus, { className: "h-3 w-3" }), t.addProvider]
147
146
  })
148
147
  ]
149
- }) : /* @__PURE__ */ jsx("div", {
150
- className: "grid grid-cols-1 md:grid-cols-2 gap-3",
151
- children: keyedByContent(prompts, (p) => p).map(({ key, item: prompt }) => /* @__PURE__ */ jsx("button", {
152
- type: "button",
153
- onClick: () => handleSuggestedPrompt(prompt),
154
- disabled: isLoading || isLoadingModels || models.length === 0,
155
- className: "text-left p-4 rounded-2xl border theme-border\n hover:opacity-80 transition-all duration-200 cursor-pointer\n disabled:opacity-50 disabled:cursor-not-allowed\n text-sm shadow-sm bg-(--theme-panel-bg) text-(--theme-panel)",
156
- children: prompt
148
+ }),
149
+ /* @__PURE__ */ jsx(ChatInput, {
150
+ ref: inputRef,
151
+ onSend: handleSendMessage,
152
+ disabled: isLoading || isLoadingModels || models.length === 0,
153
+ placeholder: isLoadingModels ? tInput.placeholderLoading : hasLoadedModels && models.length === 0 ? tInput.placeholderNoProviders : tInput.placeholder,
154
+ settingsLeft: /* @__PURE__ */ jsx(ConnectToolsLink, {}),
155
+ settingsRight: /* @__PURE__ */ jsxs(Fragment$1, { children: [slots.composerUsage, /* @__PURE__ */ jsx(ComposerControls, { targetSession })] })
156
+ }),
157
+ prompts.length > 0 && !(hasLoadedModels && models.length === 0) && /* @__PURE__ */ jsx("ul", {
158
+ className: "mx-auto max-w-[50rem] px-8",
159
+ children: keyedByContent(prompts, (p) => p).map(({ key, item: prompt }) => /* @__PURE__ */ jsx("li", {
160
+ className: "border-b theme-border last:border-b-0",
161
+ children: /* @__PURE__ */ jsx("button", {
162
+ type: "button",
163
+ onClick: () => handleSuggestedPrompt(prompt),
164
+ disabled: isLoading || isLoadingModels || models.length === 0,
165
+ className: "-mx-3 my-1 block w-[calc(100%+1.5rem)] text-pretty rounded-lg px-3 py-2 text-left text-sm theme-muted-text transition-colors hover:chat-tint hover:theme-text disabled:opacity-50 disabled:cursor-not-allowed",
166
+ children: prompt
167
+ })
157
168
  }, key))
158
- })]
159
- })
160
- }),
161
- /* @__PURE__ */ jsx(ChatInput, {
162
- ref: inputRef,
163
- onSend: handleSendMessage,
164
- disabled: isLoading || isLoadingModels || models.length === 0,
165
- placeholder: isLoadingModels ? tInput.placeholderLoading : hasLoadedModels && models.length === 0 ? tInput.placeholderNoProviders : tInput.placeholder,
166
- settingsLeft: /* @__PURE__ */ jsx(ConnectToolsLink, {}),
167
- settingsRight: /* @__PURE__ */ jsxs(Fragment$1, { children: [slots.composerUsage, /* @__PURE__ */ jsx(ComposerControls, { targetSession })] })
169
+ })
170
+ ]
168
171
  })
169
- ]
172
+ })]
170
173
  })
171
174
  ]
172
175
  })
@@ -1,6 +1,6 @@
1
1
  import { FaArrowUp } from "../../icons/fa/FaArrowUp.js";
2
- import { FaPaperclip } from "../../icons/fa/FaPaperclip.js";
3
2
  import { FaStop } from "../../icons/fa/FaStop.js";
3
+ import { GoPlus } from "../../icons/go/GoPlus.js";
4
4
  import { useChatConfig } from "../core/config.js";
5
5
  import { useChat } from "../core/ChatProvider.js";
6
6
  import AttachmentUpload from "./AttachmentUpload.js";
@@ -56,9 +56,11 @@ function AttachButton({ open, disabled, label, onClick }) {
56
56
  type: "button",
57
57
  onClick,
58
58
  disabled,
59
- className: cx("flex-shrink-0 p-3 ml-1 mb-1 rounded-xl transition-colors", open ? "text-blue-600" : "theme-muted-text hover:theme-text", "disabled:opacity-50 disabled:cursor-not-allowed"),
59
+ "aria-expanded": open,
60
+ className: cx("flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-full transition-colors", open ? "theme-text chat-tint-strong" : "theme-muted-text hover:theme-text hover:chat-tint", "disabled:opacity-50 disabled:cursor-not-allowed"),
60
61
  "aria-label": label,
61
- children: /* @__PURE__ */ jsx(FaPaperclip, { className: "h-5 w-5" })
62
+ title: label,
63
+ children: /* @__PURE__ */ jsx(GoPlus, { className: cx("h-5 w-5 transition-transform duration-200", open && "rotate-45") })
62
64
  });
63
65
  }
64
66
  function SendButton({ onClick, enabled, emphasised, label, testId }) {
@@ -67,7 +69,7 @@ function SendButton({ onClick, enabled, emphasised, label, testId }) {
67
69
  onClick,
68
70
  disabled: !enabled,
69
71
  "data-testid": testId,
70
- className: cx("p-2.5 rounded-xl transition-all duration-200", emphasised ? "theme-element hover:opacity-90" : "theme-muted-panel theme-muted-text", !enabled && "cursor-not-allowed"),
72
+ className: cx("flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-full transition-[background-color,color,transform] duration-150 active:scale-95", emphasised ? "theme-element hover:opacity-90" : "chat-send-idle", !enabled && "cursor-not-allowed active:scale-100"),
71
73
  "aria-label": label,
72
74
  title: label,
73
75
  children: /* @__PURE__ */ jsx(FaArrowUp, { className: "h-4 w-4" })
@@ -198,22 +200,28 @@ var ChatInput = forwardRef(function ChatInput({ onSend, disabled = false, placeh
198
200
  if (pastes && handleTextPaste(e.clipboardData.getData("text/plain"))) e.preventDefault();
199
201
  };
200
202
  const { isDragging, dragHandlers } = useDragDrop({ onFilesDropped: handleFilesDropped });
203
+ const toolbar = attachmentsAvailable || !flush && !!settingsRight;
204
+ const action = isStreaming && !input.trim() ? /* @__PURE__ */ jsx("button", {
205
+ type: "button",
206
+ onClick: stopStreaming,
207
+ disabled: turn?.stopping,
208
+ className: "flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-full bg-(--theme-panel) text-(--theme-panel-bg) transition-opacity hover:opacity-85 disabled:opacity-50",
209
+ "aria-label": t.stopGenerating,
210
+ title: t.stopGenerating,
211
+ children: /* @__PURE__ */ jsx(FaStop, { className: "h-3 w-3" })
212
+ }) : /* @__PURE__ */ jsx(SendButton, {
213
+ onClick: handleSubmit,
214
+ enabled: canSend,
215
+ emphasised,
216
+ label: sendLabel ?? t.sendMessage,
217
+ testId: sendTestId
218
+ });
201
219
  return /* @__PURE__ */ jsxs("div", {
202
- className: cx("relative", !flush && "px-4 pb-6 pt-2 bg-(--theme-panel-bg)"),
220
+ className: cx("relative", !flush && "chat-composer-dock px-4 pb-5 pt-2 bg-(--theme-panel-bg)"),
203
221
  ...attachmentsAvailable ? dragHandlers : {},
204
222
  children: [isDragging && /* @__PURE__ */ jsx(DragOverlay, { className: "rounded-2xl" }), /* @__PURE__ */ jsxs("div", {
205
223
  className: cx(!flush && "w-full max-w-[50rem] mx-auto px-4"),
206
224
  children: [
207
- showAttachments && attachmentsAvailable && /* @__PURE__ */ jsx("div", {
208
- className: "mb-3 p-3 rounded-2xl border theme-border shadow-sm bg-(--theme-panel-bg)",
209
- children: /* @__PURE__ */ jsx(AttachmentUpload, {
210
- ref: attachmentUploadRef,
211
- conversationId,
212
- attachments,
213
- onAttachmentsChange: setAttachments,
214
- maxFiles: 10
215
- })
216
- }),
217
225
  context && /* @__PURE__ */ jsx(ComposerContext, { children: context }),
218
226
  cards.length > 0 && /* @__PURE__ */ jsx("div", {
219
227
  className: "mb-2 flex flex-wrap gap-2",
@@ -238,56 +246,55 @@ var ChatInput = forwardRef(function ChatInput({ onSend, disabled = false, placeh
238
246
  textareaRef.current?.focus();
239
247
  }
240
248
  }), /* @__PURE__ */ jsxs("div", {
241
- className: cx("relative flex items-end gap-2", "rounded-3xl border shadow-sm", "px-2 overflow-hidden", "transition-all duration-200", "focus-within:shadow-md", "theme-border", "bg-(--theme-panel-bg)"),
242
- children: [attachmentsAvailable && /* @__PURE__ */ jsx(AttachButton, {
243
- open: showAttachments,
244
- disabled,
245
- label: t.attachFiles,
246
- onClick: () => setShowAttachments(!showAttachments)
247
- }), /* @__PURE__ */ jsx("div", {
248
- className: "flex-1 relative max-h-[200px] overflow-y-auto",
249
- children: /* @__PURE__ */ jsxs("div", {
250
- className: "flex items-end",
251
- children: [/* @__PURE__ */ jsx("textarea", {
252
- ref: textareaRef,
253
- value: input,
254
- onChange: (e) => setInputAndPersist(e.target.value),
255
- onKeyDown: handleKeyDown,
256
- onPaste: handlePaste,
257
- placeholder: effectivePlaceholder,
258
- "aria-label": inputLabel,
259
- disabled,
260
- rows: 1,
261
- className: cx("theme-text", "flex-1 resize-none py-3.5 px-2 pr-3", "bg-transparent border-0", "placeholder:theme-muted-text", "focus:outline-none focus:ring-0", "disabled:opacity-50 disabled:cursor-not-allowed", "text-base leading-relaxed", "overflow-hidden")
262
- }), /* @__PURE__ */ jsx("div", {
263
- className: "flex-shrink-0 p-1.5 mr-1 sticky bottom-1 flex items-center",
264
- children: isStreaming && !input.trim() ? /* @__PURE__ */ jsx("button", {
265
- type: "button",
266
- onClick: stopStreaming,
267
- disabled: turn?.stopping,
268
- className: cx("p-2.5 rounded-xl", "theme-muted-panel", "hover:theme-hover", "transition-colors"),
269
- "aria-label": t.stopGenerating,
270
- title: t.stopGenerating,
271
- children: /* @__PURE__ */ jsx(FaStop, { className: "h-4 w-4" })
272
- }) : /* @__PURE__ */ jsx(SendButton, {
273
- onClick: handleSubmit,
274
- enabled: canSend,
275
- emphasised,
276
- label: sendLabel ?? t.sendMessage,
277
- testId: sendTestId
249
+ className: "chat-composer flex flex-col rounded-[1.25rem] bg-(--theme-panel-bg)",
250
+ children: [
251
+ showAttachments && attachmentsAvailable && /* @__PURE__ */ jsx("div", {
252
+ className: "px-3 pt-3",
253
+ children: /* @__PURE__ */ jsx(AttachmentUpload, {
254
+ ref: attachmentUploadRef,
255
+ conversationId,
256
+ attachments,
257
+ onAttachmentsChange: setAttachments,
258
+ maxFiles: 10
259
+ })
260
+ }),
261
+ /* @__PURE__ */ jsxs("div", {
262
+ className: cx(!toolbar && "flex items-end gap-2 pr-2 pb-2"),
263
+ children: [/* @__PURE__ */ jsx("div", {
264
+ className: "max-h-[200px] min-w-0 flex-1 overflow-y-auto",
265
+ children: /* @__PURE__ */ jsx("textarea", {
266
+ ref: textareaRef,
267
+ value: input,
268
+ onChange: (e) => setInputAndPersist(e.target.value),
269
+ onKeyDown: handleKeyDown,
270
+ onPaste: handlePaste,
271
+ placeholder: effectivePlaceholder,
272
+ "aria-label": inputLabel ?? effectivePlaceholder,
273
+ disabled,
274
+ rows: 1,
275
+ className: cx("theme-text block w-full resize-none overflow-hidden", "bg-transparent border-0 px-4", toolbar ? "pt-3.5 pb-1" : "pt-3 pb-1", "placeholder:theme-muted-text", "focus:outline-none focus:ring-0", "disabled:opacity-50 disabled:cursor-not-allowed", "text-[1rem] leading-relaxed")
278
276
  })
277
+ }), !toolbar && action]
278
+ }),
279
+ toolbar && /* @__PURE__ */ jsxs("div", {
280
+ className: "flex items-center gap-1 px-2 pb-2",
281
+ children: [attachmentsAvailable && /* @__PURE__ */ jsx(AttachButton, {
282
+ open: showAttachments,
283
+ disabled,
284
+ label: t.attachFiles,
285
+ onClick: () => setShowAttachments(!showAttachments)
286
+ }), /* @__PURE__ */ jsxs("div", {
287
+ className: "ml-auto flex min-w-0 items-center gap-1",
288
+ children: [!flush && settingsRight, action]
279
289
  })]
280
290
  })
281
- })]
291
+ ]
282
292
  })]
283
293
  }),
284
- !flush && /* @__PURE__ */ jsx(ComposerSettings, {
285
- left: /* @__PURE__ */ jsxs(Fragment$1, { children: [settingsLeft, uploadedCount > 0 && /* @__PURE__ */ jsx("span", {
286
- className: "text-[11px] theme-muted-text",
287
- children: t.filesAttached(uploadedCount)
288
- })] }),
289
- children: settingsRight
290
- })
294
+ !flush && (settingsLeft || uploadedCount > 0) && /* @__PURE__ */ jsx(ComposerSettings, { left: /* @__PURE__ */ jsxs(Fragment$1, { children: [settingsLeft, uploadedCount > 0 && /* @__PURE__ */ jsx("span", {
295
+ className: "text-[11px] theme-muted-text",
296
+ children: t.filesAttached(uploadedCount)
297
+ })] }) })
291
298
  ]
292
299
  })]
293
300
  });