Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .Jules/palette.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
## 2026-07-06 - [μ ‘κ·Όμ„±] μ•„μ½”λ””μ–Έ μ»΄ν¬λ„ŒνŠΈ ARIA 속성 및 ν‚€λ³΄λ“œ 포컀슀 μŠ€νƒ€μΌ λΆ€μž¬
**Learning:** μ• ν”Œλ¦¬μΌ€μ΄μ…˜ λ‚΄μ˜ μ»€μŠ€ν…€ μΈν„°λž™ν‹°λΈŒ μ•„μ½”λ””μ–Έ μ»΄ν¬λ„ŒνŠΈ(`ContextSection` λ“±)κ°€ μ μ ˆν•œ ARIA 속성을 ν†΅ν•œ 연결이 λˆ„λ½λ˜μ–΄ 있음(`aria-controls`, `role="region"`, `aria-labelledby` λˆ„λ½). λ˜ν•œ, UI μ›μ‹œ μ»΄ν¬λ„ŒνŠΈλ₯Ό μ‚¬μš©ν•˜μ§€ μ•Šμ€ ν† κΈ€μš© λ‹¨μˆœ `<button>` μš”μ†Œμ— λͺ…ν™•ν•œ ν‚€λ³΄λ“œ 포컀슀 인디케이터가 μ μš©λ˜μ–΄ μžˆμ§€ μ•Šμ€ 점 발견.
**Action:** μ•žμœΌλ‘œ React의 `useId()`λ₯Ό μ‚¬μš©ν•˜μ—¬ κ³ μœ ν•œ IDλ₯Ό λ™μ μœΌλ‘œ μƒμ„±ν•˜κ³ , 이λ₯Ό 톡해 μ•„μ½”λ””μ–Έ ν† κΈ€ λ²„νŠΌκ³Ό μ½˜ν…μΈ  μ˜μ—­μ„ μ—°κ²°ν•΄μ•Ό 함. ν‚€λ³΄λ“œ λ„€λΉ„κ²Œμ΄μ…˜ μ‹œ ν¬μ»€μŠ€κ°€ 잘 보이도둝 λͺ¨λ“  μ»€μŠ€ν…€ μΈν„°λž™ν‹°λΈŒ μš”μ†Œμ— `focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset` λ“± Tailwind 포컀슀 클래슀λ₯Ό λͺ…μ‹œμ μœΌλ‘œ μΆ”κ°€ν•΄μ•Ό 함.
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Changelog

## [Unreleased]

### 🎨 λ³€κ²½ 사항 (UX / μ ‘κ·Όμ„±)

- μ›Ή λŒ€μ‹œλ³΄λ“œμ˜ 각쒅 λ‘œκ·Έμ•„μ›ƒ λ²„νŠΌ(`org-sidebar.tsx`, `org-header.tsx`, `no-organization-state.tsx`)에 슀크린 λ¦¬λ”μš© `aria-label="Log out of your account"` (λ˜λŠ” `Sign out of your account`) 속성을 μΆ”κ°€ν•˜μ—¬ 접근성을 κ°œμ„ ν–ˆμŠ΅λ‹ˆλ‹€.
4 changes: 4 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,11 @@
},
"devDependencies": {
"@eslint/eslintrc": "^3",
"@testing-library/dom": "^10.4.1",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.2",
"eslint": "^9",
"jsdom": "^29.1.1",
"turbo": "^2.9.16",
"typescript-eslint": "^8"
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ import {
SessionFilesTab,
} from '@/components/dashboard/session-files'
import { useSessionDetail } from '@/hooks/use-dashboard-sessions'
import { messagesToTimeline, formatSlashCommandText } from '@/lib/timeline-events'
import { messagesToTimeline, buildTimelineGroups, formatSlashCommandText } from '@/lib/timeline-events'
import { extractSessionFiles } from '@/lib/session-files'
import {
formatTokens,
Expand Down Expand Up @@ -46,6 +46,7 @@ export default function OrgSessionDetailPage({
() => (data ? messagesToTimeline(data.messages) : []),
[data],
)
const groups = useMemo(() => buildTimelineGroups(events), [events])
const files = useMemo(() => extractSessionFiles(events), [events])
const [selectedIdx, setSelectedIdx] = useState<number | null>(0)
const safeIdx =
Expand Down Expand Up @@ -221,6 +222,7 @@ export default function OrgSessionDetailPage({
<div className="px-4 pt-3 pb-2">
<SessionActivityRibbon
events={events}
groups={groups}
selectedIdx={safeIdx}
onSelect={setSelectedIdx}
sessionStartedAt={data.startedAt}
Expand All @@ -232,6 +234,7 @@ export default function OrgSessionDetailPage({
<div className="border-r border-border min-h-0 overflow-hidden">
<EventList
events={events}
groups={groups}
selectedIdx={safeIdx ?? -1}
onSelect={setSelectedIdx}
sessionStartedAt={data.startedAt}
Expand Down
11 changes: 6 additions & 5 deletions packages/web/src/components/dashboard/event-list.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,14 @@ import { List, type RowComponentProps } from "react-window";
import { User, Bot, Wrench, ChevronRight } from "lucide-react";
import {
formatSlashCommandText,
buildTimelineGroups,
type TimelineEvent,
type TimelineGroup,
} from "@/lib/timeline-events";
import { cn } from "@/lib/utils";

type EventListProps = {
events: TimelineEvent[];
groups: TimelineGroup[];
selectedIdx: number;
onSelect: (idx: number) => void;
sessionStartedAt: string;
Expand Down Expand Up @@ -50,11 +51,10 @@ function formatElapsed(timestamp: string, sessionStartedAt: string): string {
}

function buildFlatRows(
events: TimelineEvent[],
groups: TimelineGroup[],
expandedGroups: Set<number>,
selectedIdx: number,
): FlatRow[] {
const groups = buildTimelineGroups(events);
const rows: FlatRow[] = [];
for (const group of groups) {
if (group.kind === "single") {
Expand Down Expand Up @@ -261,15 +261,16 @@ function Row({

export function EventList({
events,
groups,
selectedIdx,
onSelect,
sessionStartedAt,
expandedGroups,
onToggleGroup,
}: EventListProps) {
const rows = useMemo(
() => buildFlatRows(events, expandedGroups, selectedIdx),
[events, expandedGroups, selectedIdx],
() => buildFlatRows(groups, expandedGroups, selectedIdx),
[groups, expandedGroups, selectedIdx],
);

if (events.length === 0) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ export function NoOrganizationState({
<button
onClick={() => signOut({ callbackUrl: '/login' })}
className="text-sm text-destructive hover:underline"
aria-label="Log out of your account"
>
Log out
</button>
Expand Down
2 changes: 1 addition & 1 deletion packages/web/src/components/dashboard/overview-stats.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ export function OverviewStats({
id="overview-stats-explanation-toggle"
aria-expanded={expanded}
aria-controls="overview-stats-explanation"
className="mt-4 flex items-center gap-2 text-xs text-muted-foreground hover:text-foreground transition-colors"
className="mt-4 flex items-center gap-2 text-xs text-muted-foreground hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring transition-colors rounded-sm"
>
<span
aria-hidden="true"
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
/** @vitest-environment jsdom */
import React from 'react'
import { render, screen, cleanup, fireEvent } from '@testing-library/react'
import * as matchers from '@testing-library/jest-dom/matchers'
import { describe, it, expect, afterEach } from 'vitest'
import { ContextSection } from './context-section'

expect.extend(matchers)

describe('ContextSection', () => {
afterEach(() => {
cleanup()
})

it('renders title and children correctly', () => {
render(
<ContextSection title="Test Title">
<div data-testid="child">Child Content</div>
</ContextSection>
)

expect(screen.getByText('Test Title')).toBeInTheDocument()
expect(screen.queryByTestId('child')).not.toBeInTheDocument() // default closed
})

it('renders open by default if defaultOpen is true', () => {
render(
<ContextSection title="Test Title" defaultOpen={true}>
<div data-testid="child">Child Content</div>
</ContextSection>
)

expect(screen.getByText('Test Title')).toBeInTheDocument()
expect(screen.getByTestId('child')).toBeInTheDocument()
})

it('toggles open/close on click', () => {
render(
<ContextSection title="Test Title">
<div data-testid="child">Child Content</div>
</ContextSection>
)

const button = screen.getByRole('button', { name: /Test Title/i })

// Open
fireEvent.click(button)
expect(screen.getByTestId('child')).toBeInTheDocument()
expect(button).toHaveAttribute('aria-expanded', 'true')

// Close
fireEvent.click(button)
expect(screen.queryByTestId('child')).not.toBeInTheDocument()
expect(button).toHaveAttribute('aria-expanded', 'false')
})

it('has correct ARIA attributes for accessibility', () => {
render(
<ContextSection title="Accessible Title" defaultOpen={true}>
<div data-testid="child">Child Content</div>
</ContextSection>
)

const button = screen.getByRole('button', { name: /Accessible Title/i })
const region = screen.getByRole('region')

const buttonControls = button.getAttribute('aria-controls')
const regionId = region.getAttribute('id')
expect(buttonControls).toBe(regionId)

const regionLabelledBy = region.getAttribute('aria-labelledby')
const buttonId = button.getAttribute('id')
expect(regionLabelledBy).toBe(buttonId)
})
})
16 changes: 11 additions & 5 deletions packages/web/src/components/dashboard/reports/context-section.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
'use client'

import { useState, type ReactNode } from 'react'
import React, { useState, useId, type ReactNode } from 'react'
import { ChevronDown, ChevronUp } from 'lucide-react'
import { cn } from '@/lib/utils'

Expand All @@ -12,27 +12,33 @@ interface ContextSectionProps {

export function ContextSection({ title, children, defaultOpen = false }: ContextSectionProps) {
const [open, setOpen] = useState(defaultOpen)
const id = useId()

const headerId = `context-header-${id}`
const panelId = `context-panel-${id}`

return (
<div className="rounded-xl bg-card ring-1 ring-foreground/10 overflow-hidden">
<button
type="button"
id={headerId}
aria-controls={panelId}
onClick={() => setOpen((v) => !v)}
className={cn(
'w-full flex items-center justify-between px-4 py-3 text-left',
'hover:bg-card-elevated transition-colors',
'hover:bg-card-elevated transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-inset',
)}
aria-expanded={open}
>
<h2 className="text-base font-medium">{title}</h2>
{open ? (
<ChevronUp className="h-4 w-4 text-muted-foreground" />
<ChevronUp className="h-4 w-4 text-muted-foreground" aria-hidden="true" />
) : (
<ChevronDown className="h-4 w-4 text-muted-foreground" />
<ChevronDown className="h-4 w-4 text-muted-foreground" aria-hidden="true" />
)}
</button>
{open && (
<div className="px-4 pb-4 pt-1">
<div id={panelId} role="region" aria-labelledby={headerId} className="px-4 pb-4 pt-1">
{children}
</div>
)}
Expand Down
27 changes: 16 additions & 11 deletions packages/web/src/components/dashboard/reports/weekly-flow-chart.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
Legend,
type TooltipProps,
} from 'recharts'
import { useMemo } from 'react'
import { parseISO } from 'date-fns'
import { formatTokens } from '@/lib/format'
import type { DailySeriesPoint } from '@/types/reports'
Expand Down Expand Up @@ -57,18 +58,22 @@ function sumTokens(p: DailySeriesPoint): number {
}

export function WeeklyFlowChart({ thisWeek, prevWeek }: WeeklyFlowChartProps) {
// μš”μΌ κΈ°μ€€μœΌλ‘œ 병합 (μ›”~일 7개 슬둯)
const dayNames = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
const data: ChartRow[] = dayNames.map((day) => ({ day, thisWeekTokens: 0, prevWeekTokens: 0 }))
const data = useMemo(() => {
// μš”μΌ κΈ°μ€€μœΌλ‘œ 병합 (μ›”~일 7개 슬둯)
const dayNames = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
const result: ChartRow[] = dayNames.map((day) => ({ day, thisWeekTokens: 0, prevWeekTokens: 0 }))

for (const p of thisWeek) {
const idx = dayIndex(p.date)
if (idx >= 0) data[idx].thisWeekTokens += sumTokens(p)
}
for (const p of prevWeek) {
const idx = dayIndex(p.date)
if (idx >= 0) data[idx].prevWeekTokens += sumTokens(p)
}
for (const p of thisWeek) {
const idx = dayIndex(p.date)
if (idx >= 0) result[idx].thisWeekTokens += sumTokens(p)
}
for (const p of prevWeek) {
const idx = dayIndex(p.date)
if (idx >= 0) result[idx].prevWeekTokens += sumTokens(p)
}

return result
}, [thisWeek, prevWeek])

return (
<ResponsiveContainer width="100%" height={260}>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
'use client'

import { useMemo, useState, useRef } from 'react'
import { useState, useRef } from 'react'
import {
formatSlashCommandText,
buildTimelineGroups,
type TimelineEvent,
type TimelineGroup,
} from '@/lib/timeline-events'
import { formatTokens, formatCost, formatRelativeTime } from '@/lib/format'
import { segmentVisuals } from './session-ribbon-visuals'

type Props = {
events: TimelineEvent[]
groups: TimelineGroup[]
selectedIdx: number | null
onSelect: (idx: number) => void
sessionStartedAt: string
Expand Down Expand Up @@ -147,6 +148,7 @@ function MergedTooltipBody({

export function SessionActivityRibbon({
events,
groups,
selectedIdx,
onSelect,
sessionStartedAt,
Expand All @@ -156,8 +158,6 @@ export function SessionActivityRibbon({
const [hover, setHover] = useState<HoverState | null>(null)
const containerRef = useRef<HTMLDivElement>(null)

const groups = useMemo(() => buildTimelineGroups(events), [events])

if (events.length === 0) return null

const trackMouse = (e: React.MouseEvent) => {
Expand Down
21 changes: 12 additions & 9 deletions packages/web/src/components/dashboard/token-usage-chart.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
'use client'

import { useMemo } from 'react'
import { AreaChart, Area, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer, TooltipProps } from 'recharts'
import { formatTokens, formatCost } from '@/lib/format'
import type { UsageSeries } from '@argos/shared'
Expand Down Expand Up @@ -43,15 +44,17 @@ function CustomTooltip({ active, payload, label }: TooltipProps<number, string>)
}

export function TokenUsageChart({ data }: TokenUsageChartProps) {
const chartData = data.map(d => {
const date = new Date(d.date)
return {
date: format(date, 'MMM d'),
fullDate: format(date, 'MMM d, yyyy'),
input: d.inputTokens,
output: d.outputTokens,
}
})
const chartData = useMemo(() => {
return data.map(d => {
const date = new Date(d.date)
return {
date: format(date, 'MMM d'),
fullDate: format(date, 'MMM d, yyyy'),
input: d.inputTokens,
output: d.outputTokens,
}
})
}, [data])

return (
<ResponsiveContainer width="100%" height={300}>
Expand Down
1 change: 1 addition & 0 deletions packages/web/src/components/layout/org-header.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export function OrgHeader({ orgName }: { orgName?: string }) {
<Button
variant="outline"
onClick={() => signOut({ callbackUrl: '/login' })}
aria-label="Sign out of your account"
>
Sign out
</Button>
Expand Down
2 changes: 2 additions & 0 deletions packages/web/src/components/layout/org-sidebar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ export function OrgSidebar() {
<button
onClick={handleLogout}
className="w-full px-3 py-2 text-sm font-medium text-destructive hover:bg-destructive/10 rounded-md transition-colors"
aria-label="Log out of your account"
>
Log Out
</button>
Expand All @@ -150,6 +151,7 @@ export function OrgSidebar() {
<button
onClick={handleLogout}
className="px-3 py-1 text-sm text-destructive hover:bg-destructive/10 rounded-md transition-colors"
aria-label="Log out of your account"
>
Logout
</button>
Expand Down
Loading
Loading