diff --git a/src/components/ancestor-lines.test.tsx b/src/components/ancestor-lines.test.tsx index 0c3bd99..bb10740 100644 --- a/src/components/ancestor-lines.test.tsx +++ b/src/components/ancestor-lines.test.tsx @@ -10,7 +10,9 @@ describe('AncestorLines', () => { { uri: 'a1', authorName: 'Alice', replyCount: 3, expanded: true }, { uri: 'a2', authorName: 'Bob', replyCount: 5, expanded: true }, ] - render() + render( + + ) const buttons = screen.getAllByRole('button') expect(buttons).toHaveLength(2) }) @@ -22,7 +24,7 @@ describe('AncestorLines', () => { { uri: 'a3', authorName: 'Carol', replyCount: 1, expanded: true }, ] const { container } = render( - + ) const lines = container.querySelectorAll('[aria-hidden="true"]') expect(lines.length).toBeGreaterThan(0) @@ -39,7 +41,9 @@ describe('AncestorLines', () => { { uri: 'a1', authorName: 'Alice', replyCount: 3, expanded: true }, { uri: 'a2', authorName: 'Bob', replyCount: 5, expanded: true }, ] - render() + render( + + ) const buttons = screen.getAllByRole('button') await user.click(buttons[0]!) expect(onToggle).toHaveBeenCalledWith('a1') @@ -47,7 +51,7 @@ describe('AncestorLines', () => { it('renders nothing when ancestors array is empty', () => { const { container } = render( - + ) expect(container.querySelector('button')).not.toBeInTheDocument() }) @@ -55,7 +59,7 @@ describe('AncestorLines', () => { it('passes axe accessibility check', async () => { const ancestors = [{ uri: 'a1', authorName: 'Alice', replyCount: 3, expanded: true }] const { container } = render( - + ) const results = await axe(container) expect(results).toHaveNoViolations() diff --git a/src/components/ancestor-lines.tsx b/src/components/ancestor-lines.tsx index a714da0..ff4ea2d 100644 --- a/src/components/ancestor-lines.tsx +++ b/src/components/ancestor-lines.tsx @@ -20,9 +20,11 @@ interface AncestorLinesProps { ancestors: AncestorInfo[] onToggle: (uri: string) => void showChevron: boolean + /** Width per line in pixels. Should match indent step. */ + lineWidth: number } -export function AncestorLines({ ancestors, onToggle }: AncestorLinesProps) { +export function AncestorLines({ ancestors, onToggle, lineWidth }: AncestorLinesProps) { if (ancestors.length === 0) return null return ( @@ -40,6 +42,7 @@ export function AncestorLines({ ancestors, onToggle }: AncestorLinesProps) { replyCount={ancestor.replyCount} opacity={opacity} showChevron={false} + width={lineWidth} /> ) })} diff --git a/src/components/reply-branch.tsx b/src/components/reply-branch.tsx index 7f5185a..6cf0353 100644 --- a/src/components/reply-branch.tsx +++ b/src/components/reply-branch.tsx @@ -12,7 +12,6 @@ import { useState, useCallback } from 'react' import type { Reply } from '@/lib/api/types' import { type ReplyTreeNode, countDescendants } from '@/lib/build-reply-tree' import { - VISUAL_INDENT_CAP, DEFAULT_EXPANDED_LEVELS, AUTO_COLLAPSE_SIBLING_THRESHOLD, AUTO_COLLAPSE_SHOW_COUNT, @@ -101,8 +100,6 @@ export function ReplyBranch({ const visibleNodes = shouldLimitSiblings ? nodes.slice(0, AUTO_COLLAPSE_SHOW_COUNT) : nodes const hiddenSiblingCount = nodes.length - visibleNodes.length - const atVisualCap = (nodes[0]?.reply.depth ?? 1) >= VISUAL_INDENT_CAP - return (
    {visibleNodes.map((node) => { @@ -133,12 +130,17 @@ export function ReplyBranch({ return (
  1. {needsBadge && parentHandle && parentPostNumber > 0 && ( -
    +
    )}
    - + {hasChildren && ( )}
    @@ -165,47 +168,28 @@ export function ReplyBranch({ type="button" onClick={() => toggleCollapse(node.reply.uri)} className="mt-1 flex items-center gap-1 text-xs text-muted-foreground transition-colors hover:text-foreground" - style={{ marginLeft: (ancestors.length + 1) * 44 }} + style={{ marginLeft: (ancestors.length + 1) * indentStep }} aria-live="polite" > {descendantCount} {descendantCount === 1 ? 'reply' : 'replies'} )} - {hasChildren && - !isCollapsed && - (atVisualCap ? ( - - ) : ( -
    - -
    - ))} + {hasChildren && !isCollapsed && ( + + )}
  2. ) })} diff --git a/src/components/thread-line.test.tsx b/src/components/thread-line.test.tsx index 29c5145..9840b6d 100644 --- a/src/components/thread-line.test.tsx +++ b/src/components/thread-line.test.tsx @@ -61,10 +61,10 @@ describe('ThreadLine', () => { expect(line!.getAttribute('style')).toContain('opacity') }) - it('has adequate tap target (min 44px)', () => { - const { container } = render() + it('applies width from prop', () => { + const { container } = render() const button = container.querySelector('button')! - expect(button.className).toMatch(/min-w-\[44px\]/) + expect(button.style.width).toBe('22px') }) it('passes axe accessibility check', async () => { diff --git a/src/components/thread-line.tsx b/src/components/thread-line.tsx index 83ed711..3499a4b 100644 --- a/src/components/thread-line.tsx +++ b/src/components/thread-line.tsx @@ -14,6 +14,8 @@ interface ThreadLineProps { replyCount: number opacity?: number showChevron?: boolean + /** Width in pixels. Matches the indent step so lines ARE the indentation. */ + width?: number } export function ThreadLine({ @@ -23,6 +25,7 @@ export function ThreadLine({ replyCount, opacity = 1, showChevron = true, + width = 22, }: ThreadLineProps) { const label = expanded ? `Collapse thread by ${authorName}, ${replyCount} ${replyCount === 1 ? 'reply' : 'replies'}` @@ -35,7 +38,8 @@ export function ThreadLine({ aria-expanded={expanded} aria-label={label} title={expanded ? 'Collapse thread' : 'Expand thread'} - className="group relative min-w-[44px] shrink-0 cursor-pointer border-none bg-transparent p-0" + className="group relative shrink-0 cursor-pointer border-none bg-transparent p-0" + style={{ width }} > {showChevron && (