feat: annotation UX overhaul, animated packing, toolbar redesign
- PinOverlay: zero-allocation rendering with in-place Graphics/Text updates, fix WebGL texture crash (addressModeU null) from orphaned Text objects - Pins always visible on media, track during drag via updatePositions() fast path - Replace eraser with Review mode in toolbar tool group (orange gradient) - Add tool instruction hints bar below toolbar (context-sensitive per tool) - Feedback panel: glassmorphism, improved ThreadDetail/CommentItem/ThreadListItem - Timestamps: relative with "ago" suffix, full date on hover tooltip - De-emphasize pin numbers, show status text in thread list - Animated arrangement transitions (easeOutCubic, 320ms) for pack/grid/row/column - Disable snap guides (user requested) - Paste now selects newly created items - Remove eraser keyboard shortcuts, remap tool shortcuts - Orphaned thread cleanup: safe destroy check for deleted media pins
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
import React from 'react';
|
||||
import { Comment } from '../../stores/annotationStore';
|
||||
import { getAuthorColor, getAuthorInitial } from '../../utils/authorColors';
|
||||
import { relativeTime } from '../../utils/relativeTime';
|
||||
import { TEXT_PRIMARY, TEXT_MUTED, BORDER } from './feedbackStyles';
|
||||
import { relativeTime, fullTimestamp } from '../../utils/relativeTime';
|
||||
import { TEXT_PRIMARY, TEXT_SECONDARY, TEXT_MUTED, BORDER, HOVER_BG } from './feedbackStyles';
|
||||
|
||||
interface CommentItemProps {
|
||||
comment: Comment;
|
||||
@@ -11,13 +11,23 @@ interface CommentItemProps {
|
||||
}
|
||||
|
||||
export default function CommentItem({ comment, isOwn, onDelete }: CommentItemProps) {
|
||||
const [hovered, setHovered] = React.useState(false);
|
||||
|
||||
return (
|
||||
<div style={{ padding: '10px 0', borderBottom: `1px solid ${BORDER}` }}>
|
||||
<div
|
||||
style={{
|
||||
padding: '12px 0',
|
||||
borderBottom: `1px solid ${BORDER}`,
|
||||
transition: 'background 0.1s ease',
|
||||
}}
|
||||
onMouseEnter={() => setHovered(true)}
|
||||
onMouseLeave={() => setHovered(false)}
|
||||
>
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: '8px', marginBottom: '6px' }}>
|
||||
<span
|
||||
style={{
|
||||
width: '22px',
|
||||
height: '22px',
|
||||
width: '24px',
|
||||
height: '24px',
|
||||
borderRadius: '50%',
|
||||
background: getAuthorColor(comment.user_id),
|
||||
display: 'flex',
|
||||
@@ -31,30 +41,31 @@ export default function CommentItem({ comment, isOwn, onDelete }: CommentItemPro
|
||||
>
|
||||
{getAuthorInitial(comment.author_name)}
|
||||
</span>
|
||||
<span style={{ color: TEXT_PRIMARY, fontSize: '12px', fontWeight: 600 }}>
|
||||
<span style={{ color: TEXT_PRIMARY, fontSize: '12px', fontWeight: 600, letterSpacing: '0.2px' }}>
|
||||
{comment.author_name}
|
||||
</span>
|
||||
<span style={{ color: TEXT_MUTED, fontSize: '10px' }}>
|
||||
<span style={{ color: TEXT_MUTED, fontSize: '10px' }} title={fullTimestamp(comment.created_at)}>
|
||||
{relativeTime(comment.created_at)}
|
||||
</span>
|
||||
{comment.edited_at && (
|
||||
<span style={{ color: TEXT_MUTED, fontSize: '10px' }}>(edited)</span>
|
||||
<span style={{ color: TEXT_MUTED, fontSize: '10px', fontStyle: 'italic' }}>edited</span>
|
||||
)}
|
||||
<div style={{ flex: 1 }} />
|
||||
{isOwn && onDelete && (
|
||||
{isOwn && onDelete && hovered && (
|
||||
<button
|
||||
onClick={onDelete}
|
||||
style={{
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
color: '#633',
|
||||
background: 'rgba(240, 72, 72, 0.08)',
|
||||
border: '1px solid rgba(240, 72, 72, 0.12)',
|
||||
borderRadius: '4px',
|
||||
color: TEXT_MUTED,
|
||||
cursor: 'pointer',
|
||||
fontSize: '10px',
|
||||
opacity: 0.6,
|
||||
transition: 'opacity 0.1s',
|
||||
padding: '2px 6px',
|
||||
transition: 'all 0.1s',
|
||||
}}
|
||||
onMouseEnter={(e) => (e.currentTarget.style.opacity = '1')}
|
||||
onMouseLeave={(e) => (e.currentTarget.style.opacity = '0.6')}
|
||||
onMouseEnter={(e) => { e.currentTarget.style.color = '#f04848'; e.currentTarget.style.background = 'rgba(240, 72, 72, 0.15)'; }}
|
||||
onMouseLeave={(e) => { e.currentTarget.style.color = TEXT_MUTED; e.currentTarget.style.background = 'rgba(240, 72, 72, 0.08)'; }}
|
||||
>
|
||||
Delete
|
||||
</button>
|
||||
@@ -62,10 +73,10 @@ export default function CommentItem({ comment, isOwn, onDelete }: CommentItemPro
|
||||
</div>
|
||||
<div
|
||||
style={{
|
||||
color: '#ccc',
|
||||
color: TEXT_SECONDARY,
|
||||
fontSize: '13px',
|
||||
lineHeight: 1.5,
|
||||
marginLeft: '30px',
|
||||
marginLeft: '32px',
|
||||
whiteSpace: 'pre-wrap',
|
||||
wordBreak: 'break-word',
|
||||
}}
|
||||
|
||||
@@ -3,12 +3,13 @@ import { Thread, AnnotationStore } from '../../stores/annotationStore';
|
||||
import CommentItem from './CommentItem';
|
||||
import CommentInput from './CommentInput';
|
||||
import {
|
||||
PANEL_BG,
|
||||
PANEL_WIDTH,
|
||||
panelContainerStyle,
|
||||
BORDER,
|
||||
TEXT_PRIMARY,
|
||||
TEXT_SECONDARY,
|
||||
TEXT_MUTED,
|
||||
ACCENT,
|
||||
HOVER_BG,
|
||||
STATUS_OPEN,
|
||||
STATUS_RESOLVED,
|
||||
} from './feedbackStyles';
|
||||
@@ -58,25 +59,11 @@ export default function ThreadDetail({
|
||||
};
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
right: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
width: `${PANEL_WIDTH}px`,
|
||||
background: PANEL_BG,
|
||||
borderLeft: `1px solid ${BORDER}`,
|
||||
zIndex: 100,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
userSelect: 'none',
|
||||
}}
|
||||
>
|
||||
<div style={panelContainerStyle}>
|
||||
{/* Header */}
|
||||
<div
|
||||
style={{
|
||||
padding: '10px 14px',
|
||||
padding: '14px 16px',
|
||||
borderBottom: `1px solid ${BORDER}`,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
@@ -90,13 +77,17 @@ export default function ThreadDetail({
|
||||
border: 'none',
|
||||
color: TEXT_MUTED,
|
||||
cursor: 'pointer',
|
||||
fontSize: '14px',
|
||||
padding: '2px',
|
||||
fontSize: '16px',
|
||||
padding: '2px 4px',
|
||||
borderRadius: '4px',
|
||||
transition: 'color 0.1s',
|
||||
}}
|
||||
onMouseEnter={(e) => (e.currentTarget.style.color = TEXT_PRIMARY)}
|
||||
onMouseLeave={(e) => (e.currentTarget.style.color = TEXT_MUTED)}
|
||||
>
|
||||
←
|
||||
</button>
|
||||
<span style={{ color: TEXT_MUTED, fontSize: '11px', fontFamily: 'monospace' }}>
|
||||
<span style={{ color: TEXT_SECONDARY, fontSize: '11px', fontFamily: 'monospace', fontWeight: 500 }}>
|
||||
#{pinNumber}
|
||||
</span>
|
||||
<span
|
||||
@@ -105,8 +96,9 @@ export default function ThreadDetail({
|
||||
fontWeight: 600,
|
||||
padding: '2px 8px',
|
||||
borderRadius: '10px',
|
||||
background: isOpen ? '#2a1515' : '#152a15',
|
||||
background: isOpen ? 'rgba(240, 72, 72, 0.1)' : 'rgba(52, 210, 123, 0.1)',
|
||||
color: isOpen ? STATUS_OPEN : STATUS_RESOLVED,
|
||||
letterSpacing: '0.3px',
|
||||
}}
|
||||
>
|
||||
{isOpen ? 'Open' : 'Resolved'}
|
||||
@@ -116,12 +108,17 @@ export default function ThreadDetail({
|
||||
<button
|
||||
onClick={() => onJumpToObject(thread.object_id)}
|
||||
style={{
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
background: 'rgba(74, 158, 255, 0.08)',
|
||||
border: `1px solid rgba(74, 158, 255, 0.15)`,
|
||||
borderRadius: '6px',
|
||||
color: ACCENT,
|
||||
cursor: 'pointer',
|
||||
fontSize: '11px',
|
||||
padding: '4px 10px',
|
||||
transition: 'all 0.15s ease',
|
||||
}}
|
||||
onMouseEnter={(e) => (e.currentTarget.style.background = 'rgba(74, 158, 255, 0.15)')}
|
||||
onMouseLeave={(e) => (e.currentTarget.style.background = 'rgba(74, 158, 255, 0.08)')}
|
||||
>
|
||||
Jump
|
||||
</button>
|
||||
@@ -130,14 +127,18 @@ export default function ThreadDetail({
|
||||
<button
|
||||
onClick={() => onResolve(thread.id, 'resolved')}
|
||||
style={{
|
||||
background: '#152a15',
|
||||
border: 'none',
|
||||
background: 'rgba(52, 210, 123, 0.1)',
|
||||
border: `1px solid rgba(52, 210, 123, 0.15)`,
|
||||
borderRadius: '6px',
|
||||
color: STATUS_RESOLVED,
|
||||
padding: '4px 10px',
|
||||
cursor: 'pointer',
|
||||
fontSize: '11px',
|
||||
fontWeight: 500,
|
||||
transition: 'all 0.15s ease',
|
||||
}}
|
||||
onMouseEnter={(e) => (e.currentTarget.style.background = 'rgba(52, 210, 123, 0.18)')}
|
||||
onMouseLeave={(e) => (e.currentTarget.style.background = 'rgba(52, 210, 123, 0.1)')}
|
||||
>
|
||||
Resolve
|
||||
</button>
|
||||
@@ -145,14 +146,18 @@ export default function ThreadDetail({
|
||||
<button
|
||||
onClick={() => onResolve(thread.id, 'open')}
|
||||
style={{
|
||||
background: '#1a1a2a',
|
||||
border: 'none',
|
||||
background: 'rgba(74, 158, 255, 0.08)',
|
||||
border: `1px solid rgba(74, 158, 255, 0.15)`,
|
||||
borderRadius: '6px',
|
||||
color: '#88f',
|
||||
color: ACCENT,
|
||||
padding: '4px 10px',
|
||||
cursor: 'pointer',
|
||||
fontSize: '11px',
|
||||
fontWeight: 500,
|
||||
transition: 'all 0.15s ease',
|
||||
}}
|
||||
onMouseEnter={(e) => (e.currentTarget.style.background = 'rgba(74, 158, 255, 0.15)')}
|
||||
onMouseLeave={(e) => (e.currentTarget.style.background = 'rgba(74, 158, 255, 0.08)')}
|
||||
>
|
||||
Reopen
|
||||
</button>
|
||||
@@ -194,11 +199,16 @@ export default function ThreadDetail({
|
||||
style={{
|
||||
background: 'none',
|
||||
border: 'none',
|
||||
color: '#633',
|
||||
color: TEXT_MUTED,
|
||||
cursor: 'pointer',
|
||||
fontSize: '13px',
|
||||
padding: '2px',
|
||||
fontSize: '14px',
|
||||
padding: '2px 4px',
|
||||
borderRadius: '4px',
|
||||
opacity: 0.6,
|
||||
transition: 'all 0.1s',
|
||||
}}
|
||||
onMouseEnter={(e) => { e.currentTarget.style.opacity = '1'; e.currentTarget.style.color = STATUS_OPEN; }}
|
||||
onMouseLeave={(e) => { e.currentTarget.style.opacity = '0.6'; e.currentTarget.style.color = TEXT_MUTED; }}
|
||||
title="Delete thread"
|
||||
>
|
||||
×
|
||||
@@ -207,7 +217,7 @@ export default function ThreadDetail({
|
||||
</div>
|
||||
|
||||
{/* Comments */}
|
||||
<div style={{ flex: 1, overflowY: 'auto', padding: '0 14px' }}>
|
||||
<div style={{ flex: 1, overflowY: 'auto', padding: '0 16px', scrollbarWidth: 'thin', scrollbarColor: '#2a2a2e transparent' }}>
|
||||
{thread.comments.map((c) => (
|
||||
<CommentItem
|
||||
key={c.id}
|
||||
@@ -219,7 +229,7 @@ export default function ThreadDetail({
|
||||
</div>
|
||||
|
||||
{/* Reply input */}
|
||||
<div style={{ padding: '10px 14px', borderTop: `1px solid ${BORDER}` }}>
|
||||
<div style={{ padding: '12px 16px', borderTop: `1px solid ${BORDER}` }}>
|
||||
<CommentInput
|
||||
value={replyText}
|
||||
onChange={setReplyText}
|
||||
|
||||
@@ -3,13 +3,14 @@ import { Thread, AnnotationStore } from '../../stores/annotationStore';
|
||||
import ThreadListItem from './ThreadListItem';
|
||||
import CommentInput from './CommentInput';
|
||||
import {
|
||||
PANEL_BG,
|
||||
PANEL_WIDTH,
|
||||
panelContainerStyle,
|
||||
BORDER,
|
||||
TEXT_PRIMARY,
|
||||
TEXT_SECONDARY,
|
||||
TEXT_MUTED,
|
||||
STATUS_OPEN,
|
||||
FILTER_ACTIVE_BG,
|
||||
ACCENT,
|
||||
} from './feedbackStyles';
|
||||
|
||||
export type FilterType = 'open' | 'resolved' | 'all' | 'mine';
|
||||
@@ -23,7 +24,6 @@ interface ThreadListProps {
|
||||
onFilterChange: (f: FilterType) => void;
|
||||
onSelectThread: (id: string) => void;
|
||||
onCollapse: () => void;
|
||||
// New comment
|
||||
selectedObjectId: string | null;
|
||||
selectedObjectLabel: string;
|
||||
newCommentText: string;
|
||||
@@ -49,32 +49,21 @@ export default function ThreadList({
|
||||
const [showOrphans, setShowOrphans] = React.useState(false);
|
||||
|
||||
return (
|
||||
<div
|
||||
style={{
|
||||
position: 'absolute',
|
||||
right: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
width: `${PANEL_WIDTH}px`,
|
||||
background: PANEL_BG,
|
||||
borderLeft: `1px solid ${BORDER}`,
|
||||
zIndex: 100,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
userSelect: 'none',
|
||||
}}
|
||||
>
|
||||
<div style={panelContainerStyle}>
|
||||
{/* Header */}
|
||||
<div
|
||||
style={{
|
||||
padding: '12px 14px',
|
||||
padding: '14px 16px',
|
||||
borderBottom: `1px solid ${BORDER}`,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px',
|
||||
gap: '10px',
|
||||
}}
|
||||
>
|
||||
<span style={{ color: TEXT_PRIMARY, fontSize: '14px', fontWeight: 600, flex: 1 }}>
|
||||
<svg width="16" height="16" viewBox="0 0 14 14" fill="none" stroke={TEXT_SECONDARY} strokeWidth="1.2" style={{ flexShrink: 0 }}>
|
||||
<path d="M2 2.5A1.5 1.5 0 013.5 1h7A1.5 1.5 0 0112 2.5v6A1.5 1.5 0 0110.5 10H6l-3 3v-3H3.5A1.5 1.5 0 012 8.5v-6z" />
|
||||
</svg>
|
||||
<span style={{ color: TEXT_PRIMARY, fontSize: '13px', fontWeight: 600, flex: 1, letterSpacing: '0.3px' }}>
|
||||
Feedback
|
||||
</span>
|
||||
{openCount > 0 && (
|
||||
@@ -82,12 +71,13 @@ export default function ThreadList({
|
||||
style={{
|
||||
background: STATUS_OPEN,
|
||||
color: '#fff',
|
||||
fontSize: '11px',
|
||||
fontWeight: 600,
|
||||
padding: '1px 7px',
|
||||
fontSize: '10px',
|
||||
fontWeight: 700,
|
||||
padding: '2px 7px',
|
||||
borderRadius: '10px',
|
||||
minWidth: '18px',
|
||||
textAlign: 'center',
|
||||
lineHeight: '14px',
|
||||
}}
|
||||
>
|
||||
{openCount}
|
||||
@@ -102,8 +92,12 @@ export default function ThreadList({
|
||||
cursor: 'pointer',
|
||||
fontSize: '16px',
|
||||
lineHeight: 1,
|
||||
padding: '2px',
|
||||
padding: '2px 4px',
|
||||
borderRadius: '4px',
|
||||
transition: 'color 0.1s',
|
||||
}}
|
||||
onMouseEnter={(e) => (e.currentTarget.style.color = TEXT_PRIMARY)}
|
||||
onMouseLeave={(e) => (e.currentTarget.style.color = TEXT_MUTED)}
|
||||
>
|
||||
×
|
||||
</button>
|
||||
@@ -112,10 +106,10 @@ export default function ThreadList({
|
||||
{/* Filter bar */}
|
||||
<div
|
||||
style={{
|
||||
padding: '8px 14px',
|
||||
padding: '8px 16px',
|
||||
borderBottom: `1px solid ${BORDER}`,
|
||||
display: 'flex',
|
||||
gap: '6px',
|
||||
gap: '4px',
|
||||
}}
|
||||
>
|
||||
{(['open', 'resolved', 'all', 'mine'] as const).map((f) => (
|
||||
@@ -124,15 +118,16 @@ export default function ThreadList({
|
||||
onClick={() => onFilterChange(f)}
|
||||
style={{
|
||||
background: filter === f ? FILTER_ACTIVE_BG : 'transparent',
|
||||
border: 'none',
|
||||
border: filter === f ? `1px solid ${BORDER}` : '1px solid transparent',
|
||||
borderRadius: '6px',
|
||||
color: filter === f ? '#fff' : TEXT_MUTED,
|
||||
color: filter === f ? TEXT_PRIMARY : TEXT_MUTED,
|
||||
padding: '4px 10px',
|
||||
cursor: 'pointer',
|
||||
fontSize: '11px',
|
||||
fontWeight: filter === f ? 600 : 400,
|
||||
textTransform: 'capitalize',
|
||||
transition: 'all 0.1s ease',
|
||||
transition: 'all 0.15s ease',
|
||||
lineHeight: '16px',
|
||||
}}
|
||||
>
|
||||
{f}
|
||||
@@ -142,9 +137,12 @@ export default function ThreadList({
|
||||
|
||||
{/* New comment input (when object selected) */}
|
||||
{selectedObjectId && (
|
||||
<div style={{ padding: '10px 14px', borderBottom: `1px solid ${BORDER}` }}>
|
||||
<div style={{ color: TEXT_MUTED, fontSize: '10px', marginBottom: '6px' }}>
|
||||
Comment on: <span style={{ color: TEXT_PRIMARY }}>{selectedObjectLabel}</span>
|
||||
<div style={{ padding: '12px 16px', borderBottom: `1px solid ${BORDER}`, background: 'rgba(74, 158, 255, 0.03)' }}>
|
||||
<div style={{ color: TEXT_MUTED, fontSize: '10px', marginBottom: '8px', textTransform: 'uppercase', letterSpacing: '0.5px' }}>
|
||||
Comment on
|
||||
</div>
|
||||
<div style={{ color: TEXT_PRIMARY, fontSize: '12px', marginBottom: '10px', fontWeight: 500 }}>
|
||||
{selectedObjectLabel}
|
||||
</div>
|
||||
<CommentInput
|
||||
value={newCommentText}
|
||||
@@ -157,24 +155,23 @@ export default function ThreadList({
|
||||
)}
|
||||
|
||||
{/* Thread list */}
|
||||
<div style={{ flex: 1, overflowY: 'auto' }}>
|
||||
<div style={{ flex: 1, overflowY: 'auto', scrollbarWidth: 'thin', scrollbarColor: '#2a2a2e transparent' }}>
|
||||
{threads.length === 0 && (
|
||||
<div style={{ padding: '32px 20px', textAlign: 'center' }}>
|
||||
<svg
|
||||
width="32"
|
||||
height="32"
|
||||
viewBox="0 0 14 14"
|
||||
fill="none"
|
||||
stroke="#333"
|
||||
strokeWidth="1"
|
||||
style={{ marginBottom: '12px' }}
|
||||
>
|
||||
<path d="M2 2.5A1.5 1.5 0 013.5 1h7A1.5 1.5 0 0112 2.5v6A1.5 1.5 0 0110.5 10H6l-3 3v-3H3.5A1.5 1.5 0 012 8.5v-6z" />
|
||||
</svg>
|
||||
<div style={{ color: TEXT_MUTED, fontSize: '12px', lineHeight: 1.5 }}>
|
||||
<div style={{ padding: '40px 24px', textAlign: 'center' }}>
|
||||
<div style={{
|
||||
width: '48px', height: '48px', borderRadius: '50%',
|
||||
background: 'rgba(74, 158, 255, 0.06)', border: `1px solid rgba(74, 158, 255, 0.1)`,
|
||||
display: 'flex', alignItems: 'center', justifyContent: 'center',
|
||||
margin: '0 auto 16px',
|
||||
}}>
|
||||
<svg width="20" height="20" viewBox="0 0 14 14" fill="none" stroke={TEXT_MUTED} strokeWidth="1">
|
||||
<path d="M2 2.5A1.5 1.5 0 013.5 1h7A1.5 1.5 0 0112 2.5v6A1.5 1.5 0 0110.5 10H6l-3 3v-3H3.5A1.5 1.5 0 012 8.5v-6z" />
|
||||
</svg>
|
||||
</div>
|
||||
<div style={{ color: TEXT_MUTED, fontSize: '12px', lineHeight: 1.6 }}>
|
||||
{selectedObjectId
|
||||
? 'No comments on this item.'
|
||||
: 'No comments yet.\nSelect an image and add a comment.'}
|
||||
: <>No comments yet.<br /><span style={{ color: TEXT_SECONDARY }}>Select an image to leave feedback.</span></>}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
@@ -193,14 +190,14 @@ export default function ThreadList({
|
||||
<div
|
||||
onClick={() => setShowOrphans(!showOrphans)}
|
||||
style={{
|
||||
padding: '10px 14px',
|
||||
padding: '10px 16px',
|
||||
cursor: 'pointer',
|
||||
color: TEXT_MUTED,
|
||||
fontSize: '11px',
|
||||
borderTop: `1px solid ${BORDER}`,
|
||||
transition: 'background 0.1s ease',
|
||||
transition: 'background 0.15s ease',
|
||||
}}
|
||||
onMouseEnter={(e) => (e.currentTarget.style.background = '#151515')}
|
||||
onMouseEnter={(e) => (e.currentTarget.style.background = '#16161a')}
|
||||
onMouseLeave={(e) => (e.currentTarget.style.background = 'transparent')}
|
||||
>
|
||||
{showOrphans ? '\u25BE' : '\u25B8'} Deleted items ({orphanedThreads.length})
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from 'react';
|
||||
import { Thread } from '../../stores/annotationStore';
|
||||
import { getAuthorColor, getAuthorInitial } from '../../utils/authorColors';
|
||||
import { relativeTime } from '../../utils/relativeTime';
|
||||
import { relativeTime, fullTimestamp } from '../../utils/relativeTime';
|
||||
import {
|
||||
TEXT_PRIMARY,
|
||||
TEXT_SECONDARY,
|
||||
@@ -26,7 +26,7 @@ export default function ThreadListItem({ thread, pinNumber, onClick }: ThreadLis
|
||||
<div
|
||||
onClick={onClick}
|
||||
style={{
|
||||
padding: '10px 14px',
|
||||
padding: '12px 16px',
|
||||
borderBottom: `1px solid ${BORDER}`,
|
||||
cursor: 'pointer',
|
||||
transition: 'background 0.1s ease',
|
||||
@@ -67,7 +67,7 @@ export default function ThreadListItem({ thread, pinNumber, onClick }: ThreadLis
|
||||
>
|
||||
{firstComment?.author_name || 'Unknown'}
|
||||
</span>
|
||||
<span style={{ color: TEXT_MUTED, fontSize: '10px', flexShrink: 0 }}>
|
||||
<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>
|
||||
@@ -89,7 +89,7 @@ export default function ThreadListItem({ thread, pinNumber, onClick }: ThreadLis
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Row 3: meta — pin #, replies, status dot */}
|
||||
{/* Row 3: meta — replies, status */}
|
||||
<div
|
||||
style={{
|
||||
display: 'flex',
|
||||
@@ -99,9 +99,6 @@ export default function ThreadListItem({ thread, pinNumber, onClick }: ThreadLis
|
||||
marginLeft: '32px',
|
||||
}}
|
||||
>
|
||||
<span style={{ color: TEXT_MUTED, fontSize: '10px', fontFamily: 'monospace' }}>
|
||||
#{pinNumber}
|
||||
</span>
|
||||
{thread.comment_count > 1 && (
|
||||
<span style={{ color: TEXT_MUTED, fontSize: '10px' }}>
|
||||
{thread.comment_count - 1} {thread.comment_count === 2 ? 'reply' : 'replies'}
|
||||
@@ -109,13 +106,15 @@ export default function ThreadListItem({ thread, pinNumber, onClick }: ThreadLis
|
||||
)}
|
||||
<span
|
||||
style={{
|
||||
width: '6px',
|
||||
height: '6px',
|
||||
borderRadius: '50%',
|
||||
background: isResolved ? STATUS_RESOLVED : STATUS_OPEN,
|
||||
fontSize: '10px',
|
||||
fontWeight: 500,
|
||||
color: isResolved ? STATUS_RESOLVED : STATUS_OPEN,
|
||||
marginLeft: 'auto',
|
||||
textTransform: 'capitalize',
|
||||
}}
|
||||
/>
|
||||
>
|
||||
{thread.status}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,18 +1,40 @@
|
||||
// Design tokens for the feedback/annotation system
|
||||
export const PANEL_WIDTH = 300;
|
||||
export const PANEL_BG = '#0d0d0d';
|
||||
export const BORDER = '#1e1e1e';
|
||||
export const PANEL_WIDTH = 320;
|
||||
export const PANEL_BG = 'rgba(12, 12, 14, 0.95)';
|
||||
export const PANEL_BG_SOLID = '#0c0c0e';
|
||||
export const BORDER = '#1a1a1e';
|
||||
|
||||
export const TEXT_PRIMARY = '#e0e0e0';
|
||||
export const TEXT_SECONDARY = '#999';
|
||||
export const TEXT_MUTED = '#666';
|
||||
export const TEXT_PRIMARY = '#eaeaea';
|
||||
export const TEXT_SECONDARY = '#a0a0a8';
|
||||
export const TEXT_MUTED = '#5a5a64';
|
||||
|
||||
export const ACCENT = '#4a9eff';
|
||||
export const STATUS_OPEN = '#ef4444';
|
||||
export const STATUS_RESOLVED = '#22c55e';
|
||||
export const STATUS_OPEN = '#f04848';
|
||||
export const STATUS_RESOLVED = '#34d27b';
|
||||
|
||||
export const INPUT_BG = '#141414';
|
||||
export const INPUT_BORDER = '#2a2a2a';
|
||||
export const INPUT_BORDER_FOCUS = '#3a3a3a';
|
||||
export const HOVER_BG = '#151515';
|
||||
export const FILTER_ACTIVE_BG = '#2a2a2a';
|
||||
export const INPUT_BG = '#131316';
|
||||
export const INPUT_BORDER = '#26262c';
|
||||
export const INPUT_BORDER_FOCUS = '#4a9eff44';
|
||||
export const HOVER_BG = '#16161a';
|
||||
export const FILTER_ACTIVE_BG = '#1e1e24';
|
||||
|
||||
// Shared panel container style
|
||||
export const panelContainerStyle: React.CSSProperties = {
|
||||
position: 'absolute',
|
||||
right: 0,
|
||||
top: 0,
|
||||
bottom: 0,
|
||||
width: `${PANEL_WIDTH}px`,
|
||||
background: PANEL_BG,
|
||||
backdropFilter: 'blur(16px)',
|
||||
WebkitBackdropFilter: 'blur(16px)',
|
||||
borderLeft: `1px solid ${BORDER}`,
|
||||
zIndex: 100,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
userSelect: 'none',
|
||||
fontFamily: "'Inter', system-ui, -apple-system, sans-serif",
|
||||
};
|
||||
|
||||
// Import this in components that need React.CSSProperties type
|
||||
import type React from 'react';
|
||||
|
||||
Reference in New Issue
Block a user