- FOCUSED_BG and FOCUSED_BORDER tokens in feedbackStyles - headerSlot prop for draft pin comment input above thread list - focusedThreadId prop highlights active thread row with accent border - ThreadListItem gains focused prop with left border + subtle background
127 lines
3.7 KiB
TypeScript
127 lines
3.7 KiB
TypeScript
import React from 'react';
|
|
import { Thread } from '../../stores/annotationStore';
|
|
import { getAuthorColor, getAuthorInitial } from '../../utils/authorColors';
|
|
import { relativeTime, fullTimestamp } from '../../utils/relativeTime';
|
|
import {
|
|
TEXT_PRIMARY,
|
|
TEXT_SECONDARY,
|
|
TEXT_MUTED,
|
|
STATUS_OPEN,
|
|
STATUS_RESOLVED,
|
|
BORDER,
|
|
HOVER_BG,
|
|
FOCUSED_BG,
|
|
ACCENT,
|
|
} from './feedbackStyles';
|
|
|
|
interface ThreadListItemProps {
|
|
thread: Thread;
|
|
pinNumber: number;
|
|
onClick: () => void;
|
|
focused?: boolean;
|
|
}
|
|
|
|
export default function ThreadListItem({ thread, pinNumber, onClick, focused }: ThreadListItemProps) {
|
|
const firstComment = thread.comments[0];
|
|
const isResolved = thread.status === 'resolved';
|
|
|
|
return (
|
|
<div
|
|
onClick={onClick}
|
|
style={{
|
|
padding: '12px 16px',
|
|
borderBottom: `1px solid ${BORDER}`,
|
|
cursor: 'pointer',
|
|
transition: 'background 0.1s ease',
|
|
background: focused ? FOCUSED_BG : 'transparent',
|
|
borderLeft: focused ? `2px solid ${ACCENT}` : '2px solid transparent',
|
|
}}
|
|
onMouseEnter={(e) => { if (!focused) e.currentTarget.style.background = HOVER_BG; }}
|
|
onMouseLeave={(e) => { if (!focused) e.currentTarget.style.background = 'transparent'; }}
|
|
>
|
|
{/* Row 1: author circle + name + timestamp */}
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '6px' }}>
|
|
<span
|
|
style={{
|
|
width: '24px',
|
|
height: '24px',
|
|
borderRadius: '50%',
|
|
background: isResolved ? '#333' : getAuthorColor(thread.created_by),
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
justifyContent: 'center',
|
|
fontSize: '11px',
|
|
fontWeight: 700,
|
|
color: '#fff',
|
|
flexShrink: 0,
|
|
opacity: isResolved ? 0.6 : 1,
|
|
}}
|
|
>
|
|
{getAuthorInitial(firstComment?.author_name || '?')}
|
|
</span>
|
|
<span
|
|
style={{
|
|
color: isResolved ? TEXT_MUTED : TEXT_PRIMARY,
|
|
fontSize: '12px',
|
|
fontWeight: 600,
|
|
flex: 1,
|
|
overflow: 'hidden',
|
|
textOverflow: 'ellipsis',
|
|
whiteSpace: 'nowrap',
|
|
}}
|
|
>
|
|
{firstComment?.author_name || 'Unknown'}
|
|
</span>
|
|
<span style={{ color: TEXT_MUTED, fontSize: '10px', flexShrink: 0 }} title={fullTimestamp(thread.last_commented_at || thread.created_at)}>
|
|
{relativeTime(thread.last_commented_at || thread.created_at)}
|
|
</span>
|
|
</div>
|
|
|
|
{/* Row 2: comment preview */}
|
|
{firstComment && (
|
|
<div
|
|
style={{
|
|
color: isResolved ? TEXT_MUTED : TEXT_SECONDARY,
|
|
fontSize: '12px',
|
|
lineHeight: 1.4,
|
|
overflow: 'hidden',
|
|
textOverflow: 'ellipsis',
|
|
whiteSpace: 'nowrap',
|
|
marginLeft: '32px',
|
|
}}
|
|
>
|
|
{firstComment.content}
|
|
</div>
|
|
)}
|
|
|
|
{/* Row 3: meta — replies, status */}
|
|
<div
|
|
style={{
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
gap: '8px',
|
|
marginTop: '6px',
|
|
marginLeft: '32px',
|
|
}}
|
|
>
|
|
{thread.comment_count > 1 && (
|
|
<span style={{ color: TEXT_MUTED, fontSize: '10px' }}>
|
|
{thread.comment_count - 1} {thread.comment_count === 2 ? 'reply' : 'replies'}
|
|
</span>
|
|
)}
|
|
<span
|
|
style={{
|
|
fontSize: '10px',
|
|
fontWeight: 500,
|
|
color: isResolved ? STATUS_RESOLVED : STATUS_OPEN,
|
|
marginLeft: 'auto',
|
|
textTransform: 'capitalize',
|
|
}}
|
|
>
|
|
{thread.status}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|