Chat chrome: one gutter, one radius scale, one even glass recipe
The header and composer were full-bleed bands (flush to the top/right/bottom viewport edges, composer corners off-screen) next to a rail that floats with a 12px margin. The new --layout-gutter (12px) spaces the rail, header, empty-state card and composer identically on every side; --radius-panel/-control/-chip (-pill) replace the ad-hoc radii across the shared classes, so every panel is rounded on all four corners and shared controls match in every theme. The desktop-only ::after hairlines and the left-corner-only radius are gone. Aero: the rail and the chat chrome (answers included) share one even recipe - the wallpaper extra-blurred into a hue-free wash (background-glass.webp) under a uniform ~50% white veil and a whisper of gloss. Measured within ~10 RGB units across the rail top->bottom and the card/composer left->right; the blue->green smear and the teal composer are gone. The composer's field is a clean even tile with no wallpaper gradient inside it. Chat fixes: send is a filled primary tile shaped exactly like attach (dimmed while the input is empty), suggestion/context chips use the shared .chip token (40/44px target, visible rim, full-contrast text, raw model id in the tooltip), the AI note reads at 12px --text-muted (.ai-disclaimer), the selected sidebar row carries the accent border + accent icon, the theme toggle is back in Aero (the moon flips to Dark; Settings brings Aero back), and the empty state is centered exactly between header and composer. Outside CSS: the wallpaper's top-left HUD-circle decoration (it peeked through the 12px gutters on wide windows) was healed out of background.webp and background-blur.webp; the orbs/bubbles ride one masked .ice-floats layer that fades them under the chrome bands and the rail strip, so none is sliced by a panel edge (still parked under reduced motion). Version 0.6.0, SW -> freezmo-v82. Tests: chat suite pins the new geometry (12px gutter, 24px corners, even wash on all four panels, send-vs-attach shape, chips/input radii); mobile pins the phone chrome and the Aero toggle; bubbles pins the floats layer + its reduced-motion opt-out.
This commit is contained in:
parent
8153fc8dae
commit
786d6cf1ae
21 changed files with 562 additions and 290 deletions
|
|
@ -578,22 +578,22 @@ export function ChatScreen() {
|
|||
}
|
||||
|
||||
return (
|
||||
<div className="flex h-full min-h-0 w-full">
|
||||
<div className="flex h-full min-h-0 w-full" style={{ paddingBottom: "var(--chat-keyboard, 0px)" }}>
|
||||
{/* Desktop's conversation list lives in the main sidebar (on /chat). */}
|
||||
<div className="relative flex h-full min-h-0 min-w-0 flex-1 flex-col" style={{ paddingBottom: "var(--chat-keyboard, 0px)" }}>
|
||||
<header className="glass chat-header sticky top-0 z-30 flex h-14 shrink-0 items-center gap-2 px-2 sm:px-4">
|
||||
<div className="chat-column relative flex h-full min-h-0 min-w-0 flex-1 flex-col gap-[var(--layout-gutter)] p-[var(--layout-gutter)]">
|
||||
<header className="glass-panel chat-header z-30 flex h-14 shrink-0 items-center gap-2 px-2 sm:px-4">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => (window.history.length > 1 ? router.back() : router.push("/"))}
|
||||
aria-label={t("chat.back")}
|
||||
title={t("chat.back")}
|
||||
className="grid size-11 shrink-0 place-items-center rounded-2xl text-[var(--text-muted)] transition hover:bg-[var(--hover)] hover:text-[var(--text)] lg:hidden"
|
||||
className="grid size-11 shrink-0 place-items-center rounded-[var(--radius-control)] text-[var(--text-muted)] transition hover:bg-[var(--hover)] hover:text-[var(--text)] lg:hidden"
|
||||
>
|
||||
<ChevronLeftIcon className="size-5" />
|
||||
</button>
|
||||
<div className="min-w-0 flex-1">
|
||||
<h1 className="truncate text-[15px] font-semibold text-[var(--text)]">{title || t("chat.title")}</h1>
|
||||
<p className="truncate text-[11.5px] text-[var(--text-subtle)]">
|
||||
<p className="truncate text-[11.5px] text-[var(--text-muted)]">
|
||||
{scopeText}
|
||||
{seed ? ` · ${seed.label}` : ""}
|
||||
</p>
|
||||
|
|
@ -605,20 +605,20 @@ export function ChatScreen() {
|
|||
onClick={() => setHistoryOpen(true)}
|
||||
aria-label={t("chat.history")}
|
||||
title={t("chat.history")}
|
||||
className="grid size-11 place-items-center rounded-2xl border border-[var(--glass-border)] bg-[var(--surface-strong)] text-[var(--text-muted)] transition hover:bg-[var(--hover)] hover:text-[var(--text)] lg:hidden"
|
||||
className="grid size-11 place-items-center rounded-[var(--radius-control)] border border-[var(--glass-border)] bg-[var(--surface-strong)] text-[var(--text-muted)] transition hover:bg-[var(--hover)] hover:text-[var(--text)] lg:hidden"
|
||||
>
|
||||
<HistoryIcon className="size-5" />
|
||||
</button>
|
||||
<ThemeToggle
|
||||
plain
|
||||
className="grid size-11 place-items-center rounded-2xl border border-[var(--glass-border)] bg-[var(--surface-strong)]"
|
||||
className="grid size-11 place-items-center rounded-[var(--radius-control)] border border-[var(--glass-border)] bg-[var(--surface-strong)]"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
onClick={newChat}
|
||||
aria-label={t("chat.new")}
|
||||
title={t("chat.new")}
|
||||
className="grid size-11 place-items-center rounded-2xl border border-[var(--glass-border)] bg-[var(--surface-strong)] text-[var(--text-muted)] transition hover:bg-[var(--hover)] hover:text-[var(--text)]"
|
||||
className="grid size-11 place-items-center rounded-[var(--radius-control)] border border-[var(--glass-border)] bg-[var(--surface-strong)] text-[var(--text-muted)] transition hover:bg-[var(--hover)] hover:text-[var(--text)]"
|
||||
>
|
||||
<ChatPlusIcon className="size-5" />
|
||||
</button>
|
||||
|
|
@ -638,16 +638,16 @@ export function ChatScreen() {
|
|||
{messages.length === 0 ? (
|
||||
<div className="chat-empty space-y-5 py-2">
|
||||
<div className="text-center">
|
||||
<span className="mx-auto mb-4 grid size-12 place-items-center rounded-2xl border border-[var(--hairline)] bg-[var(--surface-strong)] text-accent-600 dark:text-accent-200">
|
||||
<span className="mx-auto mb-4 grid size-12 place-items-center rounded-[var(--radius-control)] border border-[var(--glass-border)] bg-[var(--surface-strong)] text-accent-600 dark:text-accent-200">
|
||||
<ChatIcon className="size-6" />
|
||||
</span>
|
||||
<h2 className="text-[20px] font-semibold tracking-[-0.01em] text-[var(--text)]">{t("chat.emptyTitle")}</h2>
|
||||
<p className="mx-auto mt-1.5 max-w-sm text-[13.5px] leading-relaxed text-[var(--text-subtle)]">
|
||||
<p className="mx-auto mt-1.5 max-w-sm text-[13.5px] leading-relaxed text-[var(--text-muted)]">
|
||||
{t("chat.emptyHint")}
|
||||
</p>
|
||||
</div>
|
||||
{missingNotes ? (
|
||||
<p className="rounded-2xl border border-amber-300/50 bg-amber-50/70 px-3.5 py-3 text-[12.5px] leading-relaxed text-amber-800 dark:border-amber-400/25 dark:bg-amber-400/10 dark:text-amber-200">
|
||||
<p className="rounded-[var(--radius-control)] border border-amber-300/50 bg-amber-50/70 px-3.5 py-3 text-[12.5px] leading-relaxed text-amber-800 dark:border-amber-400/25 dark:bg-amber-400/10 dark:text-amber-200">
|
||||
{t("chat.oldDecksNote")}
|
||||
</p>
|
||||
) : null}
|
||||
|
|
@ -658,7 +658,7 @@ export function ChatScreen() {
|
|||
type="button"
|
||||
disabled={!online || busy}
|
||||
onClick={() => send(suggestion)}
|
||||
className="max-w-full rounded-2xl border border-[var(--glass-border)] bg-[var(--surface-strong)] px-3.5 py-2.5 text-left text-[13px] text-[var(--text-muted)] transition hover:bg-[var(--hover)] hover:text-[var(--text)] disabled:opacity-55"
|
||||
className="chip max-w-full px-3.5 py-2 text-left text-[13px] disabled:opacity-55"
|
||||
>
|
||||
{suggestion}
|
||||
</button>
|
||||
|
|
@ -697,10 +697,9 @@ export function ChatScreen() {
|
|||
))}
|
||||
</>
|
||||
)}
|
||||
<div className="h-2" aria-hidden />
|
||||
{messages.length > 0 ? <div className="h-2" aria-hidden /> : null}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!pinned && messages.length > 0 ? (
|
||||
<button
|
||||
type="button"
|
||||
|
|
@ -721,7 +720,7 @@ export function ChatScreen() {
|
|||
{error ? (
|
||||
<div
|
||||
role="alert"
|
||||
className="mx-3 mb-2 shrink-0 rounded-2xl border border-rose-300/60 bg-rose-50/80 p-3 dark:border-rose-400/30 dark:bg-rose-500/10"
|
||||
className="shrink-0 rounded-[var(--radius-control)] border border-rose-300/60 bg-rose-50/80 p-3 dark:border-rose-400/30 dark:bg-rose-500/10"
|
||||
>
|
||||
<p className="text-[13px] leading-relaxed text-rose-800 dark:text-rose-200">{error.message}</p>
|
||||
<div className="mt-2 flex flex-wrap items-center gap-2">
|
||||
|
|
|
|||
|
|
@ -70,13 +70,13 @@ export function Composer({
|
|||
const canSend = online && !busy && value.trim().length > 0;
|
||||
|
||||
return (
|
||||
<div className="chat-composer-band border-t border-[var(--glass-border)] bg-[var(--glass-bg)] px-4 pb-safe pt-2 backdrop-blur-xl lg:px-10">
|
||||
<div className="chat-composer-band glass-panel px-4 pb-safe pt-2 lg:px-10">
|
||||
{attachments.length > 0 ? (
|
||||
<ul className="mb-2 flex flex-wrap gap-1.5">
|
||||
{attachments.map((entry, index) => (
|
||||
<li
|
||||
key={`${entry.attachment.name}-${index}`}
|
||||
className="flex items-center gap-1.5 rounded-xl border border-[var(--hairline-soft)] bg-[var(--surface-sunken)] py-1 pl-2.5 pr-1 text-[12px] text-[var(--text-muted)]"
|
||||
className="flex items-center gap-1.5 rounded-[var(--radius-chip)] border border-[var(--glass-border)] bg-[var(--surface-strong)] py-1 pl-2.5 pr-1 text-[12px] text-[var(--text-muted)]"
|
||||
>
|
||||
<span className="max-w-[160px] truncate">{entry.attachment.name}</span>
|
||||
<span className="text-[var(--text-subtle)]">{entry.attachment.kind === "photo" ? t("chat.attachPhoto") : t("chat.attachDoc")}</span>
|
||||
|
|
@ -85,7 +85,7 @@ export function Composer({
|
|||
aria-label={t("chat.attachRemove")}
|
||||
title={t("chat.attachRemove")}
|
||||
onClick={() => onRemoveAttachment(index)}
|
||||
className="grid size-6 place-items-center rounded-lg text-[var(--text-subtle)] transition hover:bg-[var(--hover)] hover:text-[var(--text)]"
|
||||
className="grid size-6 place-items-center rounded-[var(--radius-chip)] text-[var(--text-subtle)] transition hover:bg-[var(--hover)] hover:text-[var(--text)]"
|
||||
>
|
||||
<XIcon className="size-3.5" />
|
||||
</button>
|
||||
|
|
@ -101,7 +101,7 @@ export function Composer({
|
|||
title={t("chat.attach")}
|
||||
disabled={attachBusy}
|
||||
onClick={() => fileInput.current?.click()}
|
||||
className="grid size-10 shrink-0 place-items-center rounded-2xl border border-[var(--glass-border)] bg-[var(--surface-strong)] text-[var(--text-muted)] transition hover:bg-[var(--hover)] hover:text-[var(--text)] disabled:opacity-60"
|
||||
className="touch-target grid size-10 shrink-0 place-items-center rounded-[var(--radius-control)] border border-[var(--glass-border)] bg-[var(--surface-strong)] text-[var(--text-muted)] transition hover:bg-[var(--hover)] hover:text-[var(--text)] disabled:opacity-60"
|
||||
>
|
||||
{attachBusy ? <Spinner className="size-4" /> : <PaperclipIcon className="size-5" />}
|
||||
</button>
|
||||
|
|
@ -132,7 +132,7 @@ export function Composer({
|
|||
rows={1}
|
||||
placeholder={t("chat.placeholder")}
|
||||
aria-label={t("chat.placeholder")}
|
||||
className="max-h-[180px] min-h-[44px] min-w-0 flex-1 resize-none rounded-2xl border border-[var(--glass-border)] bg-[var(--surface-strong)] px-3.5 py-2.5 text-[16px] leading-relaxed text-[var(--text)] outline-none transition-[height,border-color] duration-150 ease-out placeholder:text-[var(--text-subtle)] focus:border-accent-400/60 motion-reduce:transition-none"
|
||||
className="max-h-[180px] min-h-[44px] min-w-0 flex-1 resize-none rounded-[var(--radius-control)] border border-[var(--glass-border)] bg-[var(--surface-strong)] px-3.5 py-2.5 text-[16px] leading-relaxed text-[var(--text)] outline-none transition-[height,border-color] duration-150 ease-out placeholder:text-[var(--text-subtle)] focus:border-accent-400/60 motion-reduce:transition-none"
|
||||
/>
|
||||
|
||||
{busy ? (
|
||||
|
|
@ -141,7 +141,7 @@ export function Composer({
|
|||
onClick={onStop}
|
||||
aria-label={t("chat.stop")}
|
||||
title={t("chat.stop")}
|
||||
className="grid size-10 shrink-0 place-items-center rounded-2xl border border-[var(--glass-border)] bg-[var(--surface-strong)] text-[var(--text)] transition hover:bg-[var(--hover)]"
|
||||
className="touch-target grid size-10 shrink-0 place-items-center rounded-[var(--radius-control)] border border-[var(--glass-border)] bg-[var(--surface-strong)] text-[var(--text)] transition hover:bg-[var(--hover)]"
|
||||
>
|
||||
<StopIcon className="size-5" />
|
||||
</button>
|
||||
|
|
@ -152,10 +152,7 @@ export function Composer({
|
|||
disabled={!canSend}
|
||||
aria-label={t("chat.send")}
|
||||
title={t("chat.send")}
|
||||
className={cn(
|
||||
"grid size-10 shrink-0 place-items-center rounded-2xl transition",
|
||||
canSend ? "grad-ice text-[#06111f]" : "border border-[var(--glass-border)] bg-[var(--surface-strong)] text-[var(--text-subtle)] opacity-60",
|
||||
)}
|
||||
className="touch-target grad-ice grid size-10 shrink-0 place-items-center rounded-[var(--radius-control)] text-[#06111f] transition disabled:opacity-45"
|
||||
>
|
||||
<SendIcon className="size-5" />
|
||||
</button>
|
||||
|
|
@ -199,14 +196,13 @@ export function Composer({
|
|||
}
|
||||
|
||||
/**
|
||||
* Chip style for the two popover triggers (context, model). The open state
|
||||
* reads pressed, because the same chip opens and closes its popover.
|
||||
* Chip style for the two popover triggers (context, model): the shared chip
|
||||
* language, read as pressed while the chip's own popover is open (the same
|
||||
* chip opens and closes it).
|
||||
*/
|
||||
function pickerChipClass(open: boolean): string {
|
||||
return cn(
|
||||
"flex min-w-0 items-center gap-1.5 rounded-xl border px-3 py-2 text-[12.5px] font-medium transition",
|
||||
open
|
||||
? "border-[var(--nav-active-border)] bg-[var(--nav-active-bg)] text-[var(--text)]"
|
||||
: "border-[var(--hairline)] bg-[var(--surface-sunken)] text-[var(--text)] hover:bg-[var(--hover)]",
|
||||
"chip min-w-0 px-3 py-2 text-[12.5px] font-medium",
|
||||
open && "border-[var(--nav-active-border)] bg-[var(--nav-active-bg)]",
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -105,10 +105,10 @@ export function ContextPicker({
|
|||
)
|
||||
}
|
||||
className={cn(
|
||||
"flex w-full items-center gap-3 rounded-2xl border px-3 py-3 text-left transition",
|
||||
"flex w-full items-center gap-3 rounded-[var(--radius-control)] border px-3 py-3 text-left transition",
|
||||
scope.kind === value
|
||||
? "border-[var(--nav-active-border)] bg-[var(--nav-active-bg)]"
|
||||
: "border-[var(--hairline-soft)] bg-[var(--surface-strong)] hover:bg-[var(--hover)]",
|
||||
: "border-[var(--glass-border)] bg-[var(--surface-strong)] hover:bg-[var(--hover)]",
|
||||
)}
|
||||
>
|
||||
<span className="shrink-0 text-[var(--text-muted)]">{icon}</span>
|
||||
|
|
@ -125,7 +125,7 @@ export function ContextPicker({
|
|||
{modeRow("all", t("chat.scope.all"), t("chat.scope.allNote"), <LayersIcon className="size-5" />)}
|
||||
{modeRow("decks", t("chat.scope.pick"), t("chat.scope.pickNote"), <FolderIcon className="size-5" />)}
|
||||
{scope.kind === "decks" ? (
|
||||
<div className="max-h-[38dvh] space-y-1 overflow-y-auto overscroll-contain rounded-2xl border border-[var(--hairline-soft)] bg-[var(--surface-sunken)] p-2">
|
||||
<div className="max-h-[38dvh] space-y-1 overflow-y-auto overscroll-contain rounded-[var(--radius-control)] border border-[var(--glass-border)] bg-[var(--surface-sunken)] p-2">
|
||||
{folders.map((folder) => {
|
||||
const inFolder = decks.filter((deck) => deck.folderId === folder.id);
|
||||
if (inFolder.length === 0) return null;
|
||||
|
|
|
|||
|
|
@ -23,7 +23,7 @@ export function CopyIconButton({ text, className }: { text: string; className?:
|
|||
window.setTimeout(() => setState("idle"), 1800);
|
||||
}}
|
||||
className={cn(
|
||||
"grid size-9 shrink-0 place-items-center rounded-xl border border-[var(--glass-border)] bg-[var(--surface-strong)] text-[var(--text-muted)] transition hover:bg-[var(--hover)] hover:text-[var(--text)]",
|
||||
"grid size-9 shrink-0 place-items-center rounded-[var(--radius-chip)] border border-[var(--glass-border)] bg-[var(--surface-strong)] text-[var(--text-muted)] transition hover:bg-[var(--hover)] hover:text-[var(--text)]",
|
||||
className,
|
||||
)}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ export function ChatHistoryList({
|
|||
onChange={(event) => onQueryChange(event.target.value)}
|
||||
placeholder={t("chat.searchChats")}
|
||||
aria-label={t("chat.searchChats")}
|
||||
className="min-w-0 flex-1 rounded-xl border border-[var(--hairline)] bg-[var(--surface-strong)] px-2.5 py-2 text-[16px] text-[var(--text)] outline-none transition placeholder:text-[var(--text-subtle)] focus:border-accent-400/60 sm:text-[13px]"
|
||||
className="min-w-0 flex-1 rounded-[var(--radius-control)] border border-[var(--glass-border)] bg-[var(--surface-strong)] px-2.5 py-2 text-[16px] text-[var(--text)] outline-none transition placeholder:text-[var(--text-subtle)] focus:border-accent-400/60 sm:text-[13px]"
|
||||
/>
|
||||
<Button size="sm" variant="secondary" className="touch-target shrink-0" onClick={onNew} aria-label={t("chat.new")}>
|
||||
<PlusIcon className="size-4" />
|
||||
|
|
@ -95,12 +95,12 @@ export function ChatHistoryList({
|
|||
if (event.key === "Escape") setRenaming(null);
|
||||
}}
|
||||
aria-label={t("common.rename")}
|
||||
className="min-w-0 flex-1 rounded-lg border border-[var(--hairline)] bg-[var(--surface-strong)] px-2 py-1.5 text-[16px] text-[var(--text)] outline-none sm:text-[13px]"
|
||||
className="min-w-0 flex-1 rounded-[var(--radius-chip)] border border-[var(--glass-border)] bg-[var(--surface-strong)] px-2 py-1.5 text-[16px] text-[var(--text)] outline-none sm:text-[13px]"
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
aria-label={t("common.save")}
|
||||
className="grid size-9 shrink-0 place-items-center rounded-lg text-emerald-600 transition hover:bg-[var(--hover)] dark:text-emerald-300"
|
||||
className="grid size-9 shrink-0 place-items-center rounded-[var(--radius-chip)] text-emerald-600 transition hover:bg-[var(--hover)] dark:text-emerald-300"
|
||||
>
|
||||
<CheckCircleIcon className="size-4" />
|
||||
</button>
|
||||
|
|
@ -108,7 +108,7 @@ export function ChatHistoryList({
|
|||
type="button"
|
||||
onClick={() => setRenaming(null)}
|
||||
aria-label={t("common.cancel")}
|
||||
className="grid size-9 shrink-0 place-items-center rounded-lg text-[var(--text-subtle)] transition hover:bg-[var(--hover)]"
|
||||
className="grid size-9 shrink-0 place-items-center rounded-[var(--radius-chip)] text-[var(--text-subtle)] transition hover:bg-[var(--hover)]"
|
||||
>
|
||||
<XIcon className="size-4" />
|
||||
</button>
|
||||
|
|
@ -120,7 +120,7 @@ export function ChatHistoryList({
|
|||
onClick={() => onOpen(conversation.id)}
|
||||
aria-current={active ? "true" : undefined}
|
||||
className={cn(
|
||||
"min-w-0 flex-1 rounded-xl px-2.5 py-2 text-left transition",
|
||||
"min-w-0 flex-1 rounded-[var(--radius-chip)] px-2.5 py-2 text-left transition",
|
||||
active ? "bg-[var(--nav-active-bg)]" : "hover:bg-[var(--hover)]",
|
||||
)}
|
||||
>
|
||||
|
|
@ -136,7 +136,7 @@ export function ChatHistoryList({
|
|||
onClick={() => startRename(conversation)}
|
||||
aria-label={t("common.rename")}
|
||||
title={t("common.rename")}
|
||||
className="grid size-9 shrink-0 place-items-center rounded-lg text-[var(--text-subtle)] transition hover:bg-[var(--hover)] hover:text-[var(--text)]"
|
||||
className="grid size-9 shrink-0 place-items-center rounded-[var(--radius-chip)] text-[var(--text-subtle)] transition hover:bg-[var(--hover)] hover:text-[var(--text)]"
|
||||
>
|
||||
<PencilIcon className="size-3.5" />
|
||||
</button>
|
||||
|
|
@ -145,7 +145,7 @@ export function ChatHistoryList({
|
|||
onClick={() => onDelete(conversation)}
|
||||
aria-label={t("chat.deleteChat")}
|
||||
title={t("chat.deleteChat")}
|
||||
className="grid size-9 shrink-0 place-items-center rounded-lg text-[var(--text-subtle)] transition hover:bg-[var(--hover)] hover:text-rose-500"
|
||||
className="grid size-9 shrink-0 place-items-center rounded-[var(--radius-chip)] text-[var(--text-subtle)] transition hover:bg-[var(--hover)] hover:text-rose-500"
|
||||
>
|
||||
<XIcon className="size-3.5" />
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -44,7 +44,7 @@ export const MessageRow = memo(function MessageRow({
|
|||
if (message.role === "user") {
|
||||
return (
|
||||
<div className="flex flex-col items-end gap-1">
|
||||
<div className="chat-bubble max-w-[88%] rounded-3xl rounded-br-lg border border-[var(--glass-border)] bg-[var(--surface-strong)] px-3.5 py-2.5">
|
||||
<div className="chat-bubble max-w-[88%] rounded-[var(--radius-panel)] rounded-br-[var(--radius-chip)] border border-[var(--glass-border)] bg-[var(--surface-strong)] px-3.5 py-2.5">
|
||||
<p className="whitespace-pre-wrap break-words text-[15px] leading-relaxed text-[var(--text)]">{message.content}</p>
|
||||
{message.attachments && message.attachments.length > 0 ? (
|
||||
<p className="mt-1.5 text-[11.5px] text-[var(--text-subtle)]">
|
||||
|
|
@ -56,7 +56,8 @@ export const MessageRow = memo(function MessageRow({
|
|||
<button
|
||||
type="button"
|
||||
onClick={onEdit}
|
||||
className="flex items-center gap-1 rounded-lg px-1.5 py-1 text-[11.5px] text-[var(--text-subtle)] transition hover:text-[var(--text)]"
|
||||
title={t("chat.edit")}
|
||||
className="flex items-center gap-1 rounded-[var(--radius-chip)] px-1.5 py-1 text-[11.5px] text-[var(--text-subtle)] transition hover:text-[var(--text)]"
|
||||
>
|
||||
<PencilIcon className="size-3.5" />
|
||||
{t("chat.edit")}
|
||||
|
|
@ -88,7 +89,7 @@ export const MessageRow = memo(function MessageRow({
|
|||
{message.error ? (
|
||||
<div
|
||||
role="alert"
|
||||
className="rounded-2xl border border-rose-300/60 bg-rose-50/70 p-3 dark:border-rose-400/30 dark:bg-rose-500/10"
|
||||
className="rounded-[var(--radius-control)] border border-rose-300/60 bg-rose-50/70 p-3 dark:border-rose-400/30 dark:bg-rose-500/10"
|
||||
>
|
||||
<p className="text-[13px] leading-relaxed text-rose-800 dark:text-rose-200">{message.error.message}</p>
|
||||
<div className="mt-2 flex flex-wrap items-center gap-2">
|
||||
|
|
@ -109,7 +110,7 @@ export const MessageRow = memo(function MessageRow({
|
|||
<button
|
||||
type="button"
|
||||
onClick={onRegenerate}
|
||||
className="flex items-center gap-1.5 rounded-xl border border-[var(--glass-border)] bg-[var(--surface-strong)] px-2.5 py-2 text-[12px] font-medium text-[var(--text-muted)] transition hover:bg-[var(--hover)] hover:text-[var(--text)]"
|
||||
className="flex items-center gap-1.5 rounded-[var(--radius-chip)] border border-[var(--glass-border)] bg-[var(--surface-strong)] px-2.5 py-2 text-[12px] font-medium text-[var(--text)] transition hover:bg-[var(--hover)]"
|
||||
>
|
||||
<RefreshIcon className="size-3.5" />
|
||||
{t("chat.regenerate")}
|
||||
|
|
@ -119,7 +120,7 @@ export const MessageRow = memo(function MessageRow({
|
|||
<button
|
||||
type="button"
|
||||
onClick={() => onToFlashcards(message)}
|
||||
className="flex items-center gap-1.5 rounded-xl border border-[var(--glass-border)] bg-[var(--surface-strong)] px-2.5 py-2 text-[12px] font-medium text-[var(--text-muted)] transition hover:bg-[var(--hover)] hover:text-[var(--text)]"
|
||||
className="flex items-center gap-1.5 rounded-[var(--radius-chip)] border border-[var(--glass-border)] bg-[var(--surface-strong)] px-2.5 py-2 text-[12px] font-medium text-[var(--text)] transition hover:bg-[var(--hover)]"
|
||||
>
|
||||
<SparklesIcon className="size-3.5" />
|
||||
{t("chat.toFlashcards")}
|
||||
|
|
@ -129,7 +130,7 @@ export const MessageRow = memo(function MessageRow({
|
|||
<button
|
||||
type="button"
|
||||
onClick={() => onSaveNotes(message)}
|
||||
className="flex items-center gap-1.5 rounded-xl border border-[var(--glass-border)] bg-[var(--surface-strong)] px-2.5 py-2 text-[12px] font-medium text-[var(--text-muted)] transition hover:bg-[var(--hover)] hover:text-[var(--text)]"
|
||||
className="flex items-center gap-1.5 rounded-[var(--radius-chip)] border border-[var(--glass-border)] bg-[var(--surface-strong)] px-2.5 py-2 text-[12px] font-medium text-[var(--text)] transition hover:bg-[var(--hover)]"
|
||||
>
|
||||
<PlusIcon className="size-3.5" />
|
||||
{t("chat.saveToNotes")}
|
||||
|
|
@ -146,7 +147,7 @@ export const MessageRow = memo(function MessageRow({
|
|||
<Link
|
||||
key={`${source.deckId}-${source.section}-${index}`}
|
||||
href={`/deck/${source.deckId}`}
|
||||
className="max-w-[220px] truncate rounded-lg border border-[var(--hairline-soft)] bg-[var(--surface-sunken)] px-2 py-1 text-[11.5px] text-[var(--text-muted)] transition hover:text-[var(--text)]"
|
||||
className="max-w-[220px] truncate rounded-[var(--radius-chip)] border border-[var(--glass-border)] bg-[var(--surface-strong)] px-2 py-1 text-[11.5px] text-[var(--text-muted)] transition hover:text-[var(--text)]"
|
||||
title={`${source.deckTitle} — ${source.section}`}
|
||||
>
|
||||
{source.deckTitle} · {source.section}
|
||||
|
|
|
|||
|
|
@ -86,7 +86,7 @@ export function ModelPicker({
|
|||
aria-label={t("chat.modelSearch")}
|
||||
autoComplete="off"
|
||||
spellCheck={false}
|
||||
className="mb-2 w-full rounded-xl border border-[var(--hairline)] bg-[var(--surface-sunken)] px-3 py-2 text-[13px] text-[var(--text)] outline-none placeholder:text-[var(--text-subtle)] focus:border-accent-400"
|
||||
className="mb-2 w-full rounded-[var(--radius-control)] border border-[var(--glass-border)] bg-[var(--surface-sunken)] px-3 py-2 text-[13px] text-[var(--text)] outline-none placeholder:text-[var(--text-subtle)] focus:border-accent-400"
|
||||
/>
|
||||
) : null}
|
||||
<ul className="max-h-[60dvh] space-y-1 overflow-y-auto overscroll-contain">
|
||||
|
|
@ -107,17 +107,17 @@ export function ModelPicker({
|
|||
onClose();
|
||||
}}
|
||||
className={cn(
|
||||
"flex w-full items-center gap-3 rounded-2xl border px-3 py-3 text-left transition",
|
||||
"flex w-full items-center gap-3 rounded-[var(--radius-control)] border px-3 py-3 text-left transition",
|
||||
selected
|
||||
? "border-[var(--nav-active-border)] bg-[var(--nav-active-bg)]"
|
||||
: "border-[var(--hairline-soft)] bg-[var(--surface-strong)] hover:bg-[var(--hover)]",
|
||||
: "border-[var(--glass-border)] bg-[var(--surface-strong)] hover:bg-[var(--hover)]",
|
||||
unavailable && "opacity-55",
|
||||
)}
|
||||
>
|
||||
<span className="min-w-0 flex-1">
|
||||
<span className="block truncate text-[13.5px] font-semibold text-[var(--text)]">{model.name}</span>
|
||||
<span className="mt-0.5 flex items-center gap-2 text-[11.5px] text-[var(--text-subtle)]">
|
||||
<span className="rounded-md border border-[var(--hairline-soft)] px-1.5 py-0.5">
|
||||
<span className="rounded-[var(--radius-chip)] border border-[var(--glass-border)] px-1.5 py-0.5">
|
||||
{model.provider ? `${providerLabel(model.provider)} · ${badge(model)}` : badge(model)}
|
||||
</span>
|
||||
{model.available === true ? (
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue