refactor(communication): polish FilterBar + thread time + composer shortcut

Communication page audit items from the second-round pass landed as
one commit because they sit on overlapping render paths:

- FilterBar had two native <select> elements (channel type / priority)
  using raw option tags. After C9 normalized the Select primitive with
  an explicit CaretDown, these two were the last stragglers. Migrated
  to Select compact. Chevron affordance now matches the rest of the app.
- FilterBar search input carried a `focusRingColor: "var(--primary)"`
  inline style that is NOT a valid CSS property; it had zero rendering
  effect. Replaced with Tailwind `focus:border-[color:var(--primary)]
  focus:ring-1 focus:ring-[color:var(--primary)]` which actually draws
  the focus ring the author intended.
- FilterBar primary button + unread-only pill used inline
  `style={{ backgroundColor: "var(--primary)" }}` to work around the
  (formerly missing) Tailwind color token. `bg-primary` is now defined
  via the theme tokens — switched both to Tailwind utilities so we
  don't mix CSS-in-JS with class-based styling in a single component.
- ChannelItem unread badge: 9px tabular-nums. 9px was unreadable on
  1x displays; bumped to 10px and added `tabular-nums` so multi-digit
  counts (10, 99, 99+) don't shift alignment.
- ThreadPanel event timeline time was formatted with
  `toLocaleTimeString([], …)` which ignores the app's zh/en locale
  setting. Switched to the shared `formatMessageTime` helper so the
  thread respects locale and also handles yesterday/earlier dates.
- MessageComposer shortcut hint was a hardcoded English string
  "Press {cmd}+Enter to send". Replaced with the existing
  `comm.sendShortcut` locale key ("按 {0}+Enter 发送" / "Press {0}+
  Enter to send") + cmdOrCtrlLabel() interpolation.

Covers J-C2 / J-C3 / J-C4 / J-C5 / J-C7 / J-C8 from the Communication
audit. The remaining J-C1 (hardcoded 260px sidebar) and J-C6 (380px
thread panel) are intentional — resizable panels would be a separate
UX feature, not a bug fix.
This commit is contained in:
ZacharyZhang-NY committed 2026-04-17 20:12:19 -04:00
1 parent e87170f105
commit 1f3484ac19
3 files changed
+30 -37

No files matched your search

@@ -5,7 +5,7 @@ import type { AgentRecord, MessageChannel } from "@shared/types"
import { AgentAvatar } from "./MessageItem"
import type { ChannelDescriptor, TaskDiscussionDescriptor } from "./ChannelHeader"
import { useT } from "../../i18n"
import { Eyebrow } from "../ui"
import { Eyebrow, Select } from "../ui"
type ChannelConfig = { labelKey: string; icon: React.ElementType }
@@ -68,10 +68,7 @@ function ChannelItem({
{hasUrgent ? (
<span className="h-2 w-2 shrink-0 rounded-full bg-destructive" />
) : unreadCount > 0 ? (
<span
className="flex h-[18px] min-w-[18px] shrink-0 items-center justify-center rounded-full px-1 text-[9px] font-bold text-primary-foreground"
style={{ backgroundColor: "var(--primary)" }}
>
<span className="flex h-[18px] min-w-[18px] shrink-0 items-center justify-center rounded-full bg-primary px-1 text-[10px] font-bold tabular-nums text-primary-foreground">
{unreadCount > 99 ? "99+" : unreadCount}
</span>
) : null}
@@ -204,8 +201,7 @@ export function FilterBar({
onChange={(e) => onChangeFilters({ ...filters, search: e.target.value })}
onKeyDown={onSearchKeyDown}
placeholder={t("comm.searchMessages")}
className="w-full rounded-lg border border-border bg-card py-1.5 pl-8 pr-3 text-[11px] text-foreground placeholder:text-muted-foreground focus:outline-none focus:ring-1"
style={{ focusRingColor: "var(--primary)" } as React.CSSProperties}
className="w-full rounded-lg border border-border bg-card py-1.5 pl-8 pr-3 text-[11px] text-foreground placeholder:text-muted-foreground focus:border-[color:var(--primary)] focus:outline-none focus:ring-1 focus:ring-[color:var(--primary)]"
/>
</div>
{scopeToggle}
@@ -214,14 +210,9 @@ export function FilterBar({
onClick={() => setExpanded(!expanded)}
className={`flex items-center gap-1.5 rounded-lg border px-2.5 py-1.5 text-[11px] font-medium transition ${
expanded || filters.channelType !== "all" || filters.priority !== "all" || filters.unreadOnly
? "border-transparent text-primary-foreground"
? "border-transparent bg-primary text-primary-foreground"
: "border-border text-muted-foreground hover:border-border"
}`}
style={
expanded || filters.channelType !== "all" || filters.priority !== "all" || filters.unreadOnly
? { backgroundColor: "var(--primary)" }
: undefined
}
>
<Funnel size={12} />
{t("comm.filters")}
@@ -236,35 +227,36 @@ export function FilterBar({
exit={{ opacity: 0, height: 0 }}
className="flex flex-wrap items-center gap-2 overflow-hidden"
>
<select
<Select
compact
value={filters.channelType}
onChange={(e) => onChangeFilters({ ...filters, channelType: e.target.value as MessageChannel | "all" })}
className="rounded-lg border border-border bg-card px-2 py-1 text-[11px] font-medium text-foreground"
>
<option value="all">{t("comm.allChannels")}</option>
<option value="direct">{t("comm.direct")}</option>
<option value="department">{t("comm.department")}</option>
<option value="company">{t("comm.company")}</option>
<option value="project">{t("nav.projects")}</option>
<option value="incident">{t("comm.incident")}</option>
</select>
<select
onChange={(v) => onChangeFilters({ ...filters, channelType: v as MessageChannel | "all" })}
options={[
{ value: "all", label: t("comm.allChannels") },
{ value: "direct", label: t("comm.direct") },
{ value: "department", label: t("comm.department") },
{ value: "company", label: t("comm.company") },
{ value: "project", label: t("nav.projects") },
{ value: "incident", label: t("comm.incident") },
]}
/>
<Select
compact
value={filters.priority}
onChange={(e) => onChangeFilters({ ...filters, priority: e.target.value })}
className="rounded-lg border border-border bg-card px-2 py-1 text-[11px] font-medium text-foreground"
>
<option value="all">{t("comm.allPriorities")}</option>
<option value="urgent">{t("comm.urgent")}</option>
<option value="normal">{t("comm.normal")}</option>
<option value="low">{t("comm.low")}</option>
</select>
onChange={(v) => onChangeFilters({ ...filters, priority: v })}
options={[
{ value: "all", label: t("comm.allPriorities") },
{ value: "urgent", label: t("comm.urgent") },
{ value: "normal", label: t("comm.normal") },
{ value: "low", label: t("comm.low") },
]}
/>
<button
type="button"
onClick={() => onChangeFilters({ ...filters, unreadOnly: !filters.unreadOnly })}
className={`rounded-full px-2.5 py-1.5 text-[11px] font-semibold transition ${
filters.unreadOnly ? "text-primary-foreground" : "bg-muted text-muted-foreground"
filters.unreadOnly ? "bg-primary text-primary-foreground" : "bg-muted text-muted-foreground"
}`}
style={filters.unreadOnly ? { backgroundColor: "var(--primary)" } : undefined}
>
{t("comm.unreadOnly")}
</button>
@@ -119,7 +119,7 @@ export function MessageComposer({
placeholder={t("composer.typeMessage")}
sending={sending}
error={sendError}
shortcutHint={`Press ${cmdOrCtrlLabel()}+Enter to send`}
shortcutHint={t("comm.sendShortcut").replace("{0}", cmdOrCtrlLabel())}
sendLabel={t("composer.sendMessage")}
sendDisabled={!body.trim() || (channel === "direct" && !directRecipientId)}
leading={
@@ -12,6 +12,7 @@ import { motion } from "framer-motion"
import type { AgentRecord, ConversationEventRecord, ThreadWaitStateRecord, ConversationThreadRecord, DocumentRecord } from "@shared/types"
import WaitStateIndicator, { type WaitStateInfo } from "./WaitStateIndicator"
import { MessageComposer } from "./MessageComposer"
import { formatMessageTime } from "../../lib/formatters"
import { useT, type TranslationFn } from "../../i18n"
interface ThreadPanelProps {
@@ -28,7 +29,7 @@ function EventItem({ event, agents, t }: { event: ConversationEventRecord; agent
? t("thread.actor.you")
: agents.find((a) => a.id === event.actorId)?.name ?? event.actorId
const time = new Date(event.createdAt).toLocaleTimeString([], { hour: "2-digit", minute: "2-digit" })
const time = formatMessageTime(event.createdAt)
const iconClass = event.eventType === "status_change"
? "text-primary"