Files
Jyotisha/frontend/src/components/chat-composer.tsx
T
Jesse_Chen e8fa1ce4ea
Independent Staging Quality Gate / validate (push) Successful in 9m19s
Independent Staging Quality Gate / publish (push) Successful in 9m54s
fix(frontend): raise dark contrast, expand hit areas, and surface input limits
Dark muted surfaces missed WCAG AA; action and tertiary tokens plus a 32-pair contract close that. Hit targets, remaining-count, and Enter-to-send follow the interaction audit without changing visual sizes.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-30 11:19:50 +08:00

94 lines
2.5 KiB
TypeScript

"use client";
import { useSyncExternalStore } from "react";
import type { ChangeEvent, FormEvent, KeyboardEvent, RefObject } from "react";
import { ArrowUp, Square } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Textarea } from "@/components/ui/textarea";
import { characterRemainingVisible } from "@/lib/character-remaining";
import {
composerDraftSnapshot,
serverComposerDraftSnapshot,
subscribeComposerDraft,
} from "@/lib/composer-draft";
export const composerRemainingId = "composer-character-remaining";
export function useComposerDraft(): string {
return useSyncExternalStore(
subscribeComposerDraft,
composerDraftSnapshot,
serverComposerDraftSnapshot,
);
}
type ChatComposerProps = {
readonly inputRef: RefObject<HTMLTextAreaElement | null>;
readonly inputLabel: string;
readonly placeholder: string;
readonly maxLength: number;
readonly inputDisabled: boolean;
readonly submitLabel: string;
readonly submitBlocked: boolean;
readonly stopVisible: boolean;
readonly stopLabel: string;
readonly stopTitle: string;
readonly onSubmit: (event: FormEvent<HTMLFormElement>) => void;
readonly onChange: (event: ChangeEvent<HTMLTextAreaElement>) => void;
readonly onKeyDown: (event: KeyboardEvent<HTMLTextAreaElement>) => void;
readonly onStop: () => void;
};
export function ChatComposer({
inputRef,
inputLabel,
placeholder,
maxLength,
inputDisabled,
submitLabel,
submitBlocked,
stopVisible,
stopLabel,
stopTitle,
onSubmit,
onChange,
onKeyDown,
onStop,
}: ChatComposerProps) {
const draft = useComposerDraft();
const showRemaining = characterRemainingVisible(draft.length, maxLength);
return (
<form className="composer" onSubmit={onSubmit}>
<Textarea
ref={inputRef}
aria-label={inputLabel}
aria-describedby={showRemaining ? composerRemainingId : undefined}
placeholder={placeholder}
rows={1}
maxLength={maxLength}
disabled={inputDisabled}
value={draft}
onChange={onChange}
onKeyDown={onKeyDown}
/>
{stopVisible ? (
<Button
className="composer-stop"
aria-label={stopLabel}
title={stopTitle}
size="icon"
type="button"
onClick={onStop}
>
<Square aria-hidden="true" />
</Button>
) : (
<Button aria-label={submitLabel} disabled={!draft.trim() || submitBlocked} size="icon" type="submit">
<ArrowUp aria-hidden="true" />
</Button>
)}
</form>
);
}