feat(ui): Phase D.4/D.5 — keyboard nav + density toggle

D.4 useListKeyboardNav:
- 신규: frontend/src/lib/composables/useListKeyboardNav.svelte.ts
  j/k/Arrow/Enter/Esc, isTypingTarget 가드 (input/textarea/select/
  contenteditable 포커스 시 비활성)
- documents/+page.svelte: kbIndex $state, kbSelectedId $derived,
  items 변경 시 clamp, URL 변경 시 0 리셋
- DocumentTable/DocumentCard: kbSelectedId prop → data-kb-selected
  속성 + ring-accent-ring 시각 표시
- scrollSelectedIntoView: queueMicrotask + querySelector로 현재
  커서를 뷰포트 내로 스크롤 (block: nearest)

D.5 Table density:
- DocumentTable: density prop (compact/comfortable), rowPaddingClass
  ($derived: py-1 | py-2.5), rowTextClass (text-[10px] | text-xs)
- documents/+page.svelte: tableDensity $state, toggleDensity 헬퍼,
  localStorage.tableDensity persistent, 테이블 뷰에서만 토글 버튼
  노출 (Rows2/Rows3 아이콘)
- 뷰 모드 버튼도 token 기반으로 리팩토링

검증:
- npm run build 통과
- npm run lint:tokens 231 → 229 (뷰 모드 버튼 token swap으로 -2)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Hyungi Ahn
2026-04-08 12:42:03 +09:00
parent 3375a5f1b1
commit d83842ccd8
3 changed files with 158 additions and 14 deletions

View File

@@ -10,8 +10,15 @@
selectable = false,
selectedIds = new Set(),
onselectionchange = null,
// D.4 키보드 네비게이션 커서
kbSelectedId = null,
// D.5 행 밀도
density = 'comfortable', // 'compact' | 'comfortable'
} = $props();
let rowPaddingClass = $derived(density === 'compact' ? 'py-1' : 'py-2.5');
let rowTextClass = $derived(density === 'compact' ? 'text-[10px]' : 'text-xs');
function toggleSelection(id, e) {
e?.stopPropagation?.();
const next = new Set(selectedIds);
@@ -130,10 +137,13 @@
<!-- 행 -->
{#each sortedItems() as doc}
{@const isChecked = selectedIds.has(doc.id)}
{@const isKbCursor = doc.id === kbSelectedId}
<div
class="flex items-center gap-1 px-2 py-1.5 w-full border-b border-default/30 hover:bg-surface transition-colors group
data-kb-selected={isKbCursor}
class="flex items-center gap-1 px-2 {rowPaddingClass} w-full border-b border-default/30 hover:bg-surface transition-colors group
{selectedId === doc.id ? 'bg-accent/5 border-l-2 border-l-accent' : ''}
{isChecked ? 'bg-accent/10' : ''}"
{isChecked ? 'bg-accent/10' : ''}
{isKbCursor ? 'ring-1 ring-accent-ring ring-inset' : ''}"
>
{#if selectable}
<span class="w-6 shrink-0 flex items-center justify-center">
@@ -156,7 +166,7 @@
<div class="flex-1 flex items-center gap-2 min-w-0">
<span class="w-1 h-4 rounded-full shrink-0" style="background: {getDomainColor(doc.ai_domain)}"></span>
<FormatIcon format={doc.file_format} size={14} />
<span class="text-xs truncate group-hover:text-accent">{doc.title || '제목 없음'}</span>
<span class="{rowTextClass} truncate group-hover:text-accent">{doc.title || '제목 없음'}</span>
</div>
<!-- 분류 -->
<div class="w-48 text-[10px] text-dim truncate">