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 (
-
{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 && (
+
+ )}
)
})}
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 && (