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>
This commit is contained in:
@@ -1,6 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useRef, useState } from "react";
|
||||
import { CharacterRemaining } from "@/components/character-remaining";
|
||||
import { characterRemainingVisible } from "@/lib/character-remaining";
|
||||
import {
|
||||
choiceQuestionGroups,
|
||||
choiceSelectionIntent,
|
||||
@@ -94,6 +96,8 @@ export function BirthTimeChoiceQuestion(props: ChoiceQuestionProps) {
|
||||
);
|
||||
}
|
||||
|
||||
const unmatchedRemainingId = "birth-time-unmatched-remaining";
|
||||
|
||||
export function BirthTimeUnmatchedClarification(props: ClarificationProps) {
|
||||
const [note, setNote] = useState("");
|
||||
const reframeRef = useRef<HTMLButtonElement | null>(null);
|
||||
@@ -108,9 +112,13 @@ export function BirthTimeUnmatchedClarification(props: ClarificationProps) {
|
||||
<fieldset className="birth-time-choice-question" disabled={props.pending}>
|
||||
<legend>这道题都不符合你的情况吗?</legend>
|
||||
<label className="birth-time-unmatched-note">
|
||||
<span>补充一句(可选)</span>
|
||||
<span className="character-remaining-row">
|
||||
补充一句(可选)
|
||||
<CharacterRemaining id={unmatchedRemainingId} length={note.length} maxLength={240} />
|
||||
</span>
|
||||
<textarea
|
||||
maxLength={240}
|
||||
aria-describedby={characterRemainingVisible(note.length, 240) ? unmatchedRemainingId : undefined}
|
||||
onChange={(event) => setNote(event.target.value)}
|
||||
placeholder="例如:这段变化发生得更早,或情况不太一样"
|
||||
value={note}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { CharacterRemaining } from "@/components/character-remaining";
|
||||
import { characterRemainingVisible } from "@/lib/character-remaining";
|
||||
import type { JourneyProgress } from "@/lib/birth-time-journey-turn";
|
||||
import { protectOnboardingPhrases } from "@/lib/onboarding-copy";
|
||||
|
||||
@@ -13,11 +15,17 @@ type BirthTimeGuideTurnProps = {
|
||||
readonly onPause: () => void;
|
||||
};
|
||||
|
||||
const guideRemainingId = "birth-time-guide-remaining";
|
||||
|
||||
export function BirthTimeGuideTurn(props: BirthTimeGuideTurnProps) {
|
||||
const [message, setMessage] = useState("");
|
||||
const phase = props.progress.adaptiveRound > 0
|
||||
? `自适应第 ${props.progress.adaptiveRound} / ${props.progress.maxAdaptiveRounds} 轮`
|
||||
: "基础证据";
|
||||
const showRemaining = characterRemainingVisible(message.length, 500);
|
||||
const describedBy = showRemaining
|
||||
? `birth-time-guide-hint ${guideRemainingId}`
|
||||
: "birth-time-guide-hint";
|
||||
|
||||
return (
|
||||
<div className="birth-time-guide-turn" aria-live="polite">
|
||||
@@ -27,23 +35,27 @@ export function BirthTimeGuideTurn(props: BirthTimeGuideTurnProps) {
|
||||
</div>
|
||||
<p className="birth-time-guide-question">{protectOnboardingPhrases(props.question)}</p>
|
||||
<label className="birth-time-guide-composer">
|
||||
<span>说说这段经历</span>
|
||||
<span className="character-remaining-row">
|
||||
说说这段经历
|
||||
<CharacterRemaining id={guideRemainingId} length={message.length} maxLength={500} />
|
||||
</span>
|
||||
<textarea
|
||||
aria-describedby="birth-time-guide-hint"
|
||||
aria-describedby={describedBy}
|
||||
disabled={props.pending}
|
||||
maxLength={500}
|
||||
rows={3}
|
||||
value={message}
|
||||
onChange={(event) => setMessage(event.target.value)}
|
||||
onKeyDown={(event) => {
|
||||
if ((event.metaKey || event.ctrlKey) && event.key === "Enter" && message.trim()) {
|
||||
if (event.nativeEvent.isComposing) return;
|
||||
if (event.key === "Enter" && !event.shiftKey && message.trim()) {
|
||||
event.preventDefault();
|
||||
props.onSubmit(message.trim());
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</label>
|
||||
<p id="birth-time-guide-hint" className="birth-time-guide-hint">说出大概年份也可以,不需要为了校正而猜测。</p>
|
||||
<p id="birth-time-guide-hint" className="birth-time-guide-hint">Enter 发送,Shift+Enter 换行。说出大概年份也可以,不需要为了校正而猜测。</p>
|
||||
<div className="birth-time-guided-actions">
|
||||
<button className="button-text birth-time-guided-action" disabled={props.pending} type="button" onClick={props.onPause}>暂停,稍后继续</button>
|
||||
<button className="button-secondary birth-time-guided-action" disabled={props.pending} type="button" onClick={props.onSkip}>不记得,跳过</button>
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import {
|
||||
characterRemainingAnnouncement,
|
||||
characterRemainingLabel,
|
||||
characterRemainingVisible,
|
||||
} from "@/lib/character-remaining";
|
||||
|
||||
export function CharacterRemaining({
|
||||
id,
|
||||
length,
|
||||
maxLength,
|
||||
}: {
|
||||
id: string;
|
||||
length: number;
|
||||
maxLength: number;
|
||||
}) {
|
||||
if (!characterRemainingVisible(length, maxLength)) return null;
|
||||
const remaining = maxLength - length;
|
||||
const atLimit = remaining <= 0;
|
||||
return (
|
||||
<span
|
||||
id={id}
|
||||
className="character-remaining"
|
||||
data-limit={atLimit ? "true" : undefined}
|
||||
aria-live="polite"
|
||||
aria-atomic="true"
|
||||
>
|
||||
<span aria-hidden="true">{characterRemainingLabel(remaining, maxLength)}</span>
|
||||
<span className="sr-only">{characterRemainingAnnouncement(remaining, maxLength)}</span>
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -5,12 +5,15 @@ 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,
|
||||
@@ -53,12 +56,14 @@ export function ChatComposer({
|
||||
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}
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
"use client";
|
||||
|
||||
import { CharacterRemaining } from "@/components/character-remaining";
|
||||
import { composerRemainingId, useComposerDraft } from "@/components/chat-composer";
|
||||
|
||||
export function ComposerCharacterRemaining({ maxLength }: { maxLength: number }) {
|
||||
const draft = useComposerDraft();
|
||||
return <CharacterRemaining id={composerRemainingId} length={draft.length} maxLength={maxLength} />;
|
||||
}
|
||||
Reference in New Issue
Block a user