| @@ -253,21 +253,100 @@ | ||
| 253 | 253 | gap: 8px; |
| 254 | 254 | margin-top: 2px; |
| 255 | 255 | } |
| 256 | 256 | |
| 257 | +/* The composer is one field. The textarea is still the kit's, but the | |
| 258 | + wrapper carries the chrome (field surface, border, focus bloom) and | |
| 259 | + the textarea's own box is flattened through its exported part, so | |
| 260 | + the send button sits inside the same border rather than beside it. */ | |
| 257 | 261 | .dm-agent-chat__composer { |
| 258 | 262 | display: flex; |
| 259 | 263 | align-items: flex-end; |
| 260 | - gap: 8px; | |
| 261 | - padding: 10px 12px; | |
| 262 | - border-top: 1px solid var( --os-border, rgba( 0, 0, 0, 0.08 ) ); | |
| 264 | + gap: 6px; | |
| 265 | + margin: 10px 12px 12px; | |
| 266 | + padding: 4px 8px 4px 10px; | |
| 267 | + border: 1px solid var( --os-ui-field-border, var( --os-ui-border, #dcdcde ) ); | |
| 268 | + border-radius: 12px; | |
| 269 | + background: var( --os-ui-field-bg, var( --os-window-bg, #fff ) ); | |
| 270 | + transition: border-color 0.15s ease, box-shadow 0.15s ease; | |
| 263 | 271 | } |
| 264 | 272 | |
| 273 | +.dm-agent-chat__composer:focus-within { | |
| 274 | + border-color: var( --os-ui-accent, #f252fc ); | |
| 275 | + box-shadow: 0 0 0 3px | |
| 276 | + color-mix( in srgb, var( --os-ui-accent-dim, #f252fc ) 28%, transparent ); | |
| 277 | +} | |
| 278 | + | |
| 265 | 279 | .dm-agent-chat__composer os-textarea { |
| 266 | 280 | flex: 1; |
| 281 | + min-width: 0; | |
| 267 | 282 | } |
| 268 | 283 | |
| 284 | +/* One line of 13px text at 1.45 is 18.85px; 6.5px above and below | |
| 285 | + makes the single-line textarea 31.85px, the send button's height, | |
| 286 | + so with the row bottom-aligned the two centres coincide on one line | |
| 287 | + and the button stays anchored at the bottom as the text grows. */ | |
| 288 | +.dm-agent-chat__composer os-textarea::part( textarea ), | |
| 289 | +.dm-agent-chat__composer os-textarea::part( textarea ):hover, | |
| 290 | +.dm-agent-chat__composer os-textarea::part( textarea ):focus, | |
| 291 | +.dm-agent-chat__composer os-textarea::part( textarea ):focus-visible { | |
| 292 | + border: 0; | |
| 293 | + border-radius: 0; | |
| 294 | + background: transparent; | |
| 295 | + box-shadow: none; | |
| 296 | + outline: none; | |
| 297 | + padding: 6.5px 0; | |
| 298 | +} | |
| 269 | 299 | |
| 300 | +/* Round and icon-only, a size down from a toolbar button so it reads | |
| 301 | + as part of the field rather than a control beside it. Sized through | |
| 302 | + the exported part: the host's width does not reach the inner button, | |
| 303 | + and a zero padding token on its own shrank it to the glyph. The | |
| 304 | + bottom margin is half the difference to the single-line textarea | |
| 305 | + (31.85px), so the two centres meet on one line and the button stays | |
| 306 | + anchored to the last line as the text grows. */ | |
| 307 | +.dm-agent-chat__send { | |
| 308 | + flex: none; | |
| 309 | + margin-block-end: 4px; | |
| 310 | +} | |
| 311 | + | |
| 312 | +.dm-agent-chat__send::part( button ) { | |
| 313 | + display: inline-flex; | |
| 314 | + align-items: center; | |
| 315 | + justify-content: center; | |
| 316 | + width: 24px; | |
| 317 | + height: 24px; | |
| 318 | + min-height: 0; | |
| 319 | + padding: 0; | |
| 320 | + border-radius: 999px; | |
| 321 | +} | |
| 322 | + | |
| 323 | +.dm-agent-chat__send-icon { | |
| 324 | + display: block; | |
| 325 | + width: 14px; | |
| 326 | + height: 14px; | |
| 327 | +} | |
| 328 | + | |
| 329 | +/* The accessible name, off-screen. */ | |
| 330 | +.dm-agent-chat__send-label { | |
| 331 | + position: absolute; | |
| 332 | + width: 1px; | |
| 333 | + height: 1px; | |
| 334 | + padding: 0; | |
| 335 | + margin: -1px; | |
| 336 | + overflow: hidden; | |
| 337 | + clip: rect( 0, 0, 0, 0 ); | |
| 338 | + white-space: nowrap; | |
| 339 | + border: 0; | |
| 340 | +} | |
| 341 | + | |
| 342 | +@media ( prefers-reduced-motion: reduce ) { | |
| 343 | + .dm-agent-chat__composer { | |
| 344 | + transition: none; | |
| 345 | + } | |
| 346 | +} | |
| 347 | + | |
| 348 | + | |
| 270 | 349 | .dm-agent-chat__line { |
| 271 | 350 | display: flex; |
| 272 | 351 | align-items: flex-end; |
| 273 | 352 | gap: 8px; |
| @@ -376,19 +455,78 @@ | ||
| 376 | 455 | .dm-agent-chat__line--pending { |
| 377 | 456 | min-width: min( 176px, 100% ); |
| 378 | 457 | } |
| 379 | 458 | |
| 459 | +/* Spinner and label on one line, centred. The bubble is a flex column | |
| 460 | + everywhere else; the working row is the one place it reads as an | |
| 461 | + instrument line, arc first, then the word. */ | |
| 380 | 462 | .dm-agent-chat__line--pending .dm-agent-chat__msg { |
| 381 | 463 | flex: 1; |
| 382 | - /* Center the label + spinner inside the wide bubble. The bubble | |
| 383 | - is a flex column, so the spinner needs align-items (text-align | |
| 384 | - alone only centers the label's inline text). */ | |
| 464 | + flex-direction: row; | |
| 385 | 465 | align-items: center; |
| 386 | - text-align: center; | |
| 466 | + justify-content: center; | |
| 467 | + gap: 8px; | |
| 387 | 468 | } |
| 388 | 469 | |
| 389 | 470 | .dm-agent-chat__msg--pending os-spinner { |
| 390 | - --os-ui-spinner-size: 24px; | |
| 471 | + flex: none; | |
| 472 | + --os-ui-spinner-size: 16px; | |
| 473 | +} | |
| 474 | + | |
| 475 | +/* Three motions say "working", each quiet on its own: the arc turns | |
| 476 | + (the component's), a sheen sweeps the label, and the agent's face | |
| 477 | + bobs beside the bubble. The sheen is the skeleton shimmer clipped | |
| 478 | + to text, so the label stays a real, selectable word rather than a | |
| 479 | + drawn one. Every one of them stops under reduced motion; the label | |
| 480 | + falls back to its plain colour, not to invisible text. */ | |
| 481 | +.dm-agent-chat__msg--pending .dm-agent-chat__msg-text { | |
| 482 | + background: linear-gradient( | |
| 483 | + 90deg, | |
| 484 | + currentColor 0%, | |
| 485 | + currentColor 35%, | |
| 486 | + var( --os-ui-accent, #f252fc ) 50%, | |
| 487 | + currentColor 65%, | |
| 488 | + currentColor 100% | |
| 489 | + ); | |
| 490 | + background-size: 250% 100%; | |
| 491 | + -webkit-background-clip: text; | |
| 492 | + background-clip: text; | |
| 493 | + -webkit-text-fill-color: transparent; | |
| 494 | + animation: dm-agent-chat-sheen 1.8s ease-in-out infinite; | |
| 495 | +} | |
| 496 | + | |
| 497 | +.dm-agent-chat__line--pending .dm-agent-chat__msg-avatar { | |
| 498 | + animation: dm-agent-chat-bob 1.4s ease-in-out infinite; | |
| 499 | +} | |
| 500 | + | |
| 501 | +@keyframes dm-agent-chat-sheen { | |
| 502 | + from { | |
| 503 | + background-position: 125% 0; | |
| 504 | + } | |
| 505 | + to { | |
| 506 | + background-position: -125% 0; | |
| 507 | + } | |
| 508 | +} | |
| 509 | + | |
| 510 | +@keyframes dm-agent-chat-bob { | |
| 511 | + 0%, | |
| 512 | + 100% { | |
| 513 | + transform: translateY( 0 ); | |
| 514 | + } | |
| 515 | + 50% { | |
| 516 | + transform: translateY( -3px ); | |
| 517 | + } | |
| 518 | +} | |
| 519 | + | |
| 520 | +@media ( prefers-reduced-motion: reduce ) { | |
| 521 | + .dm-agent-chat__msg--pending .dm-agent-chat__msg-text { | |
| 522 | + animation: none; | |
| 523 | + background: none; | |
| 524 | + -webkit-text-fill-color: currentColor; | |
| 525 | + } | |
| 526 | + .dm-agent-chat__line--pending .dm-agent-chat__msg-avatar { | |
| 527 | + animation: none; | |
| 528 | + } | |
| 391 | 529 | } |
| 392 | 530 | |
| 393 | 531 | /* Markdown content inside agent bubbles. */ |
| 394 | 532 | .dm-agent-chat__msg-text p { |