diff --git a/apps/web/src/app.css b/apps/web/src/app.css index 7180f16d5..60475f38d 100644 --- a/apps/web/src/app.css +++ b/apps/web/src/app.css @@ -3822,67 +3822,87 @@ tr.insights-row-clickable:hover { color: var(--destructive); } -/* Shared composer: transcript-matched text field on the left, a grouped - action rail (Send only — no turn cancellation exists yet) on the right. */ -.chat-composer { +/* Shared composer: one lean full-width row (+, text, voice, send). */ +.chat-composer-box { display: flex; align-items: flex-end; - gap: 0.5rem; - width: 100%; - padding: 0.5rem 0.75rem; - border: 1px solid var(--border); - border-radius: var(--radius-lg, 0.75rem); - background: var(--card, var(--background)); -} -.chat-composer:focus-within { - border-color: var(--ring, var(--primary)); + gap: 4px; + padding: 6px; + background: var(--surface); + border-radius: 22px; + box-shadow: var(--floating); + transition: box-shadow 150ms ease-out; +} +.chat-composer-box:focus-within { + box-shadow: + 0 0 0 1px var(--line-strong), + 0 6px 20px -6px rgb(0 0 0 / 18%); } .chat-composer-input { flex: 1; min-width: 0; + min-height: 32px; + max-height: 180px; + display: block; resize: none; border: 0; background: transparent; - padding: 0.35rem 0; + padding: 5px 4px; font: inherit; - font-size: max(var(--text-base), 16px); - font-weight: var(--font-weight-medium); + font-size: 15px; line-height: 1.5; - color: var(--foreground); + color: var(--ink); } .chat-composer-input:focus { outline: none; } .chat-composer-input::placeholder { - color: var(--muted-foreground); + color: var(--ink-3); } .chat-composer-input:disabled { cursor: not-allowed; opacity: 0.6; } -.chat-composer-rail { - display: flex; - align-items: center; - gap: 0.25rem; - flex-shrink: 0; - padding-bottom: 0.35rem; -} +.chat-composer-btn, .chat-composer-send { - display: inline-flex; - align-items: center; - justify-content: center; - width: 2rem; - height: 2rem; + display: grid; + place-items: center; + flex: 0 0 32px; + width: 32px; + height: 32px; + padding: 0; border: 0; - border-radius: 999px; - background: var(--primary); - color: var(--primary-foreground); + border-radius: 50%; cursor: pointer; } -.chat-composer-send:disabled { +.chat-composer-btn { + background: transparent; + color: var(--ink-2); +} +.chat-composer-voice { + background: var(--hover); + color: var(--ink); +} +.chat-composer-btn:hover:not(:disabled) { + background: var(--press); +} +.chat-composer-btn:disabled { cursor: not-allowed; - background: var(--muted); - color: var(--muted-foreground); + opacity: 0.4; +} +.chat-composer-send { + background: var(--ink); + color: var(--action); + transition: + transform 160ms var(--ease-out), + opacity 150ms ease; +} +.chat-composer-send:disabled { + cursor: default; + opacity: 0.2; +} +.chat-composer-send:not(:disabled):active { + transform: scale(0.92); } .chat-composer-send-spinner { animation: chat-composer-spin 0.8s linear infinite; diff --git a/apps/web/src/chat/composer.tsx b/apps/web/src/chat/composer.tsx index b15030484..13b2b2968 100644 --- a/apps/web/src/chat/composer.tsx +++ b/apps/web/src/chat/composer.tsx @@ -1,8 +1,8 @@ // No Stop action: neither `threads-api.ts` nor the hub API expose a way to // cancel a running turn yet. import { useCommandPaletteNavigation } from "@corbits/react-ui"; -import { ArrowUp, CircleNotch } from "@/lib/icons"; -import { useRef, useState } from "react"; +import { ArrowUp, CircleNotch, Microphone, Plus } from "@/lib/icons"; +import { useLayoutEffect, useRef, useState } from "react"; import { activeMention, applyMention, matchMentionQuery, type ActiveMention } from "./mentions"; @@ -19,6 +19,8 @@ export function Composer({ disabled, mentionables = [], onSend, + onAdd, + onVoice, }: { readonly placeholder: string; readonly busy: boolean; @@ -26,6 +28,10 @@ export function Composer({ /** Agents an `@` token can address; empty disables the popover. */ readonly mentionables?: readonly ComposerMention[]; readonly onSend: (text: string) => void; + /** Opens the attach / add actions; the `+` is hidden without it. */ + readonly onAdd?: () => void; + /** Starts voice mode; the mic is hidden without it. */ + readonly onVoice?: () => void; }) { const [text, setText] = useState(""); const [mention, setMention] = useState(null); @@ -34,6 +40,14 @@ export function Composer({ const matches = mention === null ? [] : matchMentionQuery(mention.query, mentionables); const open = mention !== null && matches.length > 0; + // Grow with the text up to the CSS max-height. + useLayoutEffect(() => { + const input = inputRef.current; + if (input === null) return; + input.style.height = "auto"; + input.style.height = `${input.scrollHeight}px`; + }, [text]); + const send = () => { const trimmed = text.trim(); if (trimmed === "" || busy || disabled) return; @@ -95,48 +109,72 @@ export function Composer({ ))} ) : null} -