11"use client" ;
22
3- import { useState } from "react" ;
3+ import { useMemo , useState } from "react" ;
44import { useLocale } from "next-intl" ;
55import { useSimulationStore } from "@/stores/simulation" ;
66import { AgentAvatar } from "@/components/AgentAvatar" ;
@@ -30,44 +30,72 @@ const TYPE_COLOR: Record<string, string> = {
3030
3131function useCopy ( ) {
3232 const [ copiedIdx , setCopiedIdx ] = useState < number | null > ( null ) ;
33- const copy = async ( text : string , idx : number ) => {
33+ const [ copiedLabel , setCopiedLabel ] = useState < string > ( "" ) ;
34+
35+ const copy = async ( text : string , idx : number , label : string ) => {
3436 await navigator . clipboard . writeText ( text ) ;
3537 setCopiedIdx ( idx ) ;
36- setTimeout ( ( ) => setCopiedIdx ( null ) , 1200 ) ;
38+ setCopiedLabel ( label ) ;
39+ setTimeout ( ( ) => {
40+ setCopiedIdx ( null ) ;
41+ setCopiedLabel ( "" ) ;
42+ } , 1200 ) ;
3743 } ;
38- return { copiedIdx, copy } ;
44+
45+ return { copiedIdx, copiedLabel, copy } ;
3946}
4047
4148export function LogTab ( ) {
4249 const locale = useLocale ( ) ;
4350 const { events, agents, factions } = useSimulationStore ( ) ;
4451 const [ filter , setFilter ] = useState < string > ( "all" ) ;
45- const { copiedIdx, copy } = useCopy ( ) ;
52+ const { copiedIdx, copiedLabel , copy } = useCopy ( ) ;
4653
4754 const filtered = filter === "all"
4855 ? events
4956 : events . filter ( ( e ) => e . type === filter ) ;
5057
5158 // Show most recent first
5259 const displayed = [ ...filtered ] . reverse ( ) . slice ( 0 , 100 ) ;
60+ const filterResultLabel = useMemo ( ( ) => {
61+ const filterName = filter === "all" ? "all events" : `${ filter } events` ;
62+ return `Showing ${ displayed . length } of ${ filtered . length } ${ filterName } ` ;
63+ } , [ displayed . length , filter , filtered . length ] ) ;
5364
5465 return (
5566 < div className = "flex flex-col h-full" >
67+ < div className = "sr-only" role = "status" aria-live = "polite" >
68+ { filterResultLabel }
69+ { copiedLabel ? `, copied ${ copiedLabel } ` : "" }
70+ </ div >
71+
5672 { /* Filter bar */ }
57- < div className = "flex flex-wrap gap-1 p-2 border-b border-hud-border" >
58- { EVENT_TYPES . map ( ( type ) => (
59- < button
60- key = { type }
61- onClick = { ( ) => setFilter ( type ) }
62- className = { `font-mono text-sm uppercase px-1.5 py-0.5 border transition-colors ${
63- filter === type
64- ? "border-accent text-accent"
65- : "border-hud-border text-hud-muted hover:text-hud-text"
66- } `}
67- >
68- { type === "all" ? "ALL" : type . split ( "." ) [ 1 ] }
69- </ button >
70- ) ) }
73+ < div className = "flex flex-wrap gap-1 p-2 border-b border-hud-border" role = "tablist" aria-label = "Log event filters" >
74+ { EVENT_TYPES . map ( ( type ) => {
75+ const isActive = filter === type ;
76+ const shortLabel = type === "all" ? "ALL" : type . split ( "." ) [ 1 ] ;
77+ const resultCount = type === "all"
78+ ? events . length
79+ : events . filter ( ( event ) => event . type === type ) . length ;
80+
81+ return (
82+ < button
83+ key = { type }
84+ type = "button"
85+ onClick = { ( ) => setFilter ( type ) }
86+ role = "tab"
87+ aria-selected = { isActive }
88+ aria-label = { `${ shortLabel } filter, ${ resultCount } events` }
89+ className = { `font-mono text-sm uppercase px-1.5 py-0.5 border transition-colors ${
90+ isActive
91+ ? "border-accent text-accent"
92+ : "border-hud-border text-hud-muted hover:text-hud-text"
93+ } `}
94+ >
95+ { shortLabel }
96+ </ button >
97+ ) ;
98+ } ) }
7199 </ div >
72100
73101 { /* Log entries */ }
@@ -85,12 +113,15 @@ export function LogTab() {
85113 second : "2-digit" ,
86114 } ) ;
87115 const summary = getSummary ( event , locale ) ;
116+ const copyLabel = `${ event . type } event at ${ time } ` ;
88117
89118 return (
90- < div
119+ < button
91120 key = { i }
92- className = "font-mono text-sm flex gap-2 group cursor-pointer hover:bg-accent/5"
93- onClick = { ( ) => copy ( `[${ time } ] ${ event . type } : ${ summary } ` , i ) }
121+ type = "button"
122+ className = "w-full text-left font-mono text-sm flex gap-2 group cursor-pointer hover:bg-accent/5 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-accent"
123+ onClick = { ( ) => copy ( `[${ time } ] ${ event . type } : ${ summary } ` , i , copyLabel ) }
124+ aria-label = { `Copy ${ copyLabel } : ${ summary } ` }
94125 >
95126 { event . type === "agent.message" && ( ( ) => {
96127 const agentId = event . payload . agent_id as string ;
@@ -103,10 +134,10 @@ export function LogTab() {
103134 { event . type . split ( "." ) [ 1 ] }
104135 </ span >
105136 < span className = "text-hud-muted truncate flex-1" > { summary } </ span >
106- < span className = "text-sm text-hud-label opacity-0 group-hover:opacity-100 flex-shrink-0" >
137+ < span className = "text-sm text-hud-label opacity-0 group-hover:opacity-100 group-focus-visible:opacity-100 flex-shrink-0" aria-hidden = "true ">
107138 { copiedIdx === i ? "COPIED" : "📋" }
108139 </ span >
109- </ div >
140+ </ button >
110141 ) ;
111142 } ) }
112143 </ div >
0 commit comments