feat(ui): polish messages timeline and graph toolbar
This commit is contained in:
parent
e5d6911025
commit
674c1d8b13
11 changed files with 535 additions and 339 deletions
|
|
@ -1,4 +1,4 @@
|
||||||
import { useState } from 'react';
|
import { useState, type ReactNode } from 'react';
|
||||||
|
|
||||||
import { CARD_BG, CARD_BORDER_STYLE, CARD_ICON_MUTED } from '@renderer/constants/cssVariables';
|
import { CARD_BG, CARD_BORDER_STYLE, CARD_ICON_MUTED } from '@renderer/constants/cssVariables';
|
||||||
import { getTeamColorSet, getThemedBadge } from '@renderer/constants/teamColors';
|
import { getTeamColorSet, getThemedBadge } from '@renderer/constants/teamColors';
|
||||||
|
|
@ -19,6 +19,7 @@ interface ActiveTasksBlockProps {
|
||||||
tasks: TeamTaskWithKanban[];
|
tasks: TeamTaskWithKanban[];
|
||||||
/** Start collapsed (e.g. when rendered inside the sidebar where MemberList already shows status). */
|
/** Start collapsed (e.g. when rendered inside the sidebar where MemberList already shows status). */
|
||||||
defaultCollapsed?: boolean;
|
defaultCollapsed?: boolean;
|
||||||
|
headerRight?: ReactNode;
|
||||||
onMemberClick?: (member: ResolvedTeamMember) => void;
|
onMemberClick?: (member: ResolvedTeamMember) => void;
|
||||||
onTaskClick?: (task: TeamTaskWithKanban) => void;
|
onTaskClick?: (task: TeamTaskWithKanban) => void;
|
||||||
}
|
}
|
||||||
|
|
@ -34,6 +35,7 @@ export const ActiveTasksBlock = ({
|
||||||
members,
|
members,
|
||||||
tasks,
|
tasks,
|
||||||
defaultCollapsed = false,
|
defaultCollapsed = false,
|
||||||
|
headerRight,
|
||||||
onMemberClick,
|
onMemberClick,
|
||||||
onTaskClick,
|
onTaskClick,
|
||||||
}: ActiveTasksBlockProps): React.JSX.Element | null => {
|
}: ActiveTasksBlockProps): React.JSX.Element | null => {
|
||||||
|
|
@ -70,23 +72,26 @@ export const ActiveTasksBlock = ({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mb-3 space-y-1.5">
|
<div className="mb-3 space-y-1.5">
|
||||||
<button
|
<div className="flex items-center justify-between gap-3">
|
||||||
type="button"
|
<button
|
||||||
className="flex w-full items-center gap-1.5 text-[10px] font-medium uppercase tracking-wide text-[var(--color-text-muted)] transition-colors hover:text-[var(--color-text-secondary)]"
|
type="button"
|
||||||
onClick={() => setCollapsed((v) => !v)}
|
className="flex min-w-0 items-center gap-1.5 text-[10px] font-medium uppercase tracking-wide text-[var(--color-text-muted)] transition-colors hover:text-[var(--color-text-secondary)]"
|
||||||
aria-label={collapsed ? 'Expand in progress' : 'Collapse in progress'}
|
onClick={() => setCollapsed((v) => !v)}
|
||||||
>
|
aria-label={collapsed ? 'Expand in progress' : 'Collapse in progress'}
|
||||||
<ChevronRight
|
>
|
||||||
size={10}
|
<ChevronRight
|
||||||
className={`shrink-0 transition-transform duration-150 ${collapsed ? '' : 'rotate-90'}`}
|
size={10}
|
||||||
/>
|
className={`shrink-0 transition-transform duration-150 ${collapsed ? '' : 'rotate-90'}`}
|
||||||
<span>In progress</span>
|
/>
|
||||||
{collapsed && (
|
<span>In progress</span>
|
||||||
<span className="rounded-full bg-[var(--color-surface-raised)] px-1.5 py-0.5 text-[10px] font-medium tabular-nums leading-none text-[var(--color-text-muted)]">
|
{collapsed && (
|
||||||
{entries.length}
|
<span className="rounded-full bg-[var(--color-surface-raised)] px-1.5 py-0.5 text-[10px] font-medium tabular-nums leading-none text-[var(--color-text-muted)]">
|
||||||
</span>
|
{entries.length}
|
||||||
)}
|
</span>
|
||||||
</button>
|
)}
|
||||||
|
</button>
|
||||||
|
{headerRight ? <div className="shrink-0">{headerRight}</div> : null}
|
||||||
|
</div>
|
||||||
{!collapsed &&
|
{!collapsed &&
|
||||||
entries.map(({ member, task, taskId, kind }) => {
|
entries.map(({ member, task, taskId, kind }) => {
|
||||||
const colors = getTeamColorSet(colorMap.get(member.name) ?? '');
|
const colors = getTeamColorSet(colorMap.get(member.name) ?? '');
|
||||||
|
|
|
||||||
|
|
@ -881,6 +881,132 @@ export const ActivityItem = memo(
|
||||||
onToggleCollapse?.(collapseToggleKey);
|
onToggleCollapse?.(collapseToggleKey);
|
||||||
}
|
}
|
||||||
}, [collapseToggleKey, isHeaderClickable, onToggleCollapse]);
|
}, [collapseToggleKey, isHeaderClickable, onToggleCollapse]);
|
||||||
|
const useCompactCollapsedHeader = compactHeader && !isExpanded;
|
||||||
|
|
||||||
|
const senderBadge = isSlashCommandResult ? (
|
||||||
|
<span className="inline-flex items-center rounded-full bg-amber-500/15 px-2 py-0.5 text-[10px] font-medium uppercase tracking-wide text-amber-300">
|
||||||
|
result
|
||||||
|
</span>
|
||||||
|
) : (
|
||||||
|
<MemberBadge
|
||||||
|
name={senderName}
|
||||||
|
color={senderColor}
|
||||||
|
teamName={teamName}
|
||||||
|
hideAvatar={senderHideAvatar || compactHeader}
|
||||||
|
onClick={onMemberNameClick}
|
||||||
|
disableHoverCard={crossTeamOrigin != null}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
|
||||||
|
const messageTypeBadge = systemLabel ? (
|
||||||
|
<span className="text-[10px] uppercase tracking-wide" style={{ color: CARD_ICON_MUTED }}>
|
||||||
|
{systemLabel}
|
||||||
|
</span>
|
||||||
|
) : isSlashCommandResult && message.commandOutput ? (
|
||||||
|
<span
|
||||||
|
className={[
|
||||||
|
'inline-flex items-center rounded-full px-1.5 py-0.5 text-[10px] uppercase tracking-wide',
|
||||||
|
isCommandOutputError ? 'bg-rose-500/15 text-rose-300' : 'bg-amber-500/15 text-amber-300',
|
||||||
|
].join(' ')}
|
||||||
|
>
|
||||||
|
{message.commandOutput.stream}
|
||||||
|
</span>
|
||||||
|
) : isSlashCommandMessage ? (
|
||||||
|
<span className="text-[10px] uppercase tracking-wide text-amber-400">command</span>
|
||||||
|
) : messageType ? (
|
||||||
|
<span className="text-[10px] uppercase tracking-wide" style={{ color: CARD_ICON_MUTED }}>
|
||||||
|
{messageType}
|
||||||
|
</span>
|
||||||
|
) : null;
|
||||||
|
|
||||||
|
const leadSourceBadge =
|
||||||
|
message.source === 'lead_session' && !isSlashCommandResult ? (
|
||||||
|
<span className="text-[10px] uppercase tracking-wide" style={{ color: CARD_ICON_MUTED }}>
|
||||||
|
session
|
||||||
|
</span>
|
||||||
|
) : message.source === 'lead_process' && !isSlashCommandResult ? (
|
||||||
|
<span className="text-[10px] uppercase tracking-wide" style={{ color: CARD_ICON_MUTED }}>
|
||||||
|
live
|
||||||
|
</span>
|
||||||
|
) : null;
|
||||||
|
|
||||||
|
const statusBadge = rateLimited ? (
|
||||||
|
<span className="inline-flex items-center gap-1 rounded-full bg-red-500/20 px-1.5 py-0.5 text-[10px] font-medium text-red-400">
|
||||||
|
<AlertTriangle size={10} />
|
||||||
|
Rate Limited
|
||||||
|
</span>
|
||||||
|
) : isApiError ? (
|
||||||
|
<span className="inline-flex items-center gap-1 rounded-full bg-red-500/20 px-1.5 py-0.5 text-[10px] font-medium text-red-400">
|
||||||
|
<AlertTriangle size={10} />
|
||||||
|
API Error
|
||||||
|
</span>
|
||||||
|
) : null;
|
||||||
|
|
||||||
|
const recipientBadge =
|
||||||
|
message.to && message.to !== message.from ? (
|
||||||
|
<>
|
||||||
|
<MoveRight size={10} style={{ color: CARD_ICON_MUTED }} className="shrink-0" />
|
||||||
|
{crossTeamTarget ? (
|
||||||
|
<CrossTeamTeamBadge teamName={crossTeamTarget} onClick={onTeamClick} />
|
||||||
|
) : null}
|
||||||
|
{crossTeamSentMemberName || !crossTeamTarget ? (
|
||||||
|
<MemberBadge
|
||||||
|
name={crossTeamSentMemberName ?? qualifiedRecipient?.memberName ?? message.to}
|
||||||
|
color={crossTeamTarget ? undefined : recipientColor}
|
||||||
|
teamName={crossTeamTarget ? undefined : teamName}
|
||||||
|
hideAvatar={
|
||||||
|
compactHeader ||
|
||||||
|
(crossTeamSentMemberName ?? qualifiedRecipient?.memberName ?? message.to) === 'user'
|
||||||
|
}
|
||||||
|
onClick={onMemberNameClick}
|
||||||
|
disableHoverCard={crossTeamTarget != null}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
</>
|
||||||
|
) : null;
|
||||||
|
|
||||||
|
const summaryContent =
|
||||||
|
isSlashCommandResult && message.commandOutput ? (
|
||||||
|
<span className="inline-flex min-w-0 items-center gap-1.5">
|
||||||
|
<Command
|
||||||
|
size={12}
|
||||||
|
className={['shrink-0', isCommandOutputError ? 'text-rose-400' : 'text-amber-400'].join(
|
||||||
|
' '
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<span
|
||||||
|
className={[
|
||||||
|
'shrink-0 font-mono text-[11px]',
|
||||||
|
isCommandOutputError ? 'text-rose-300' : 'text-amber-300',
|
||||||
|
].join(' ')}
|
||||||
|
>
|
||||||
|
{message.commandOutput.commandLabel}
|
||||||
|
</span>
|
||||||
|
<span className="min-w-0 truncate text-[11px] text-[var(--color-text-secondary)]">
|
||||||
|
{message.summary || getCommandOutputSummary(message.text) || rawSummary}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
) : isSlashCommandMessage && slashCommandMeta ? (
|
||||||
|
<span className="inline-flex min-w-0 items-center gap-1.5">
|
||||||
|
<Command size={12} className="shrink-0 text-amber-400" />
|
||||||
|
<span className="shrink-0 font-mono text-[11px] text-amber-300">
|
||||||
|
{slashCommandMeta.command}
|
||||||
|
</span>
|
||||||
|
{slashCommandMeta.args ? (
|
||||||
|
<span className="min-w-0 truncate text-[11px] text-[var(--color-text-secondary)]">
|
||||||
|
{slashCommandMeta.args.replace(/\n+/g, ' ')}
|
||||||
|
</span>
|
||||||
|
) : (slashCommandMeta.knownDescription ?? knownSlashCommand?.description) ? (
|
||||||
|
<span className="min-w-0 truncate text-[11px] text-[var(--color-text-secondary)]">
|
||||||
|
{slashCommandMeta.knownDescription ?? knownSlashCommand?.description}
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
</span>
|
||||||
|
) : onTaskIdClick ? (
|
||||||
|
renderInlineBoldSummary(rawSummary, onTaskIdClick)
|
||||||
|
) : (
|
||||||
|
renderInlineBoldSummary(rawSummary)
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<article
|
<article
|
||||||
|
|
@ -933,7 +1059,9 @@ export const ActivityItem = memo(
|
||||||
role={isHeaderClickable ? 'button' : undefined}
|
role={isHeaderClickable ? 'button' : undefined}
|
||||||
tabIndex={isHeaderClickable ? 0 : undefined}
|
tabIndex={isHeaderClickable ? 0 : undefined}
|
||||||
className={[
|
className={[
|
||||||
'flex min-w-0 items-center gap-2 px-3 py-2',
|
useCompactCollapsedHeader
|
||||||
|
? 'min-w-0 px-3 py-2'
|
||||||
|
: 'flex min-w-0 items-center gap-2 px-3 py-2',
|
||||||
isHeaderClickable ? 'cursor-pointer select-none' : '',
|
isHeaderClickable ? 'cursor-pointer select-none' : '',
|
||||||
].join(' ')}
|
].join(' ')}
|
||||||
onClick={handleHeaderToggle}
|
onClick={handleHeaderToggle}
|
||||||
|
|
@ -948,207 +1076,125 @@ export const ActivityItem = memo(
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{isUnread ? (
|
{useCompactCollapsedHeader ? (
|
||||||
<span className="size-2 shrink-0 rounded-full bg-blue-500" title="Unread" aria-hidden />
|
<div className="min-w-0">
|
||||||
) : null}
|
<div className="flex min-w-0 items-start gap-3">
|
||||||
{/* Chevron for collapsible messages */}
|
<div className="flex min-w-0 flex-1 items-center gap-2 overflow-hidden">
|
||||||
{showChevron ? (
|
{isUnread ? (
|
||||||
<ChevronRight
|
<span
|
||||||
className="size-3 shrink-0 transition-transform duration-150"
|
className="size-2 shrink-0 rounded-full bg-blue-500"
|
||||||
style={{
|
title="Unread"
|
||||||
color: CARD_ICON_MUTED,
|
aria-hidden
|
||||||
transform: isExpanded ? 'rotate(90deg)' : undefined,
|
/>
|
||||||
}}
|
) : null}
|
||||||
/>
|
{crossTeamOrigin ? (
|
||||||
) : null}
|
<CrossTeamTeamBadge teamName={crossTeamOrigin.teamName} onClick={onTeamClick} />
|
||||||
|
) : null}
|
||||||
{/* Sender avatar + name badge */}
|
{senderBadge}
|
||||||
{crossTeamOrigin ? (
|
{messageTypeBadge}
|
||||||
<CrossTeamTeamBadge teamName={crossTeamOrigin.teamName} onClick={onTeamClick} />
|
{leadSourceBadge}
|
||||||
) : null}
|
{statusBadge}
|
||||||
{isSlashCommandResult ? (
|
{recipientBadge}
|
||||||
<span className="inline-flex items-center rounded-full bg-amber-500/15 px-2 py-0.5 text-[10px] font-medium uppercase tracking-wide text-amber-300">
|
</div>
|
||||||
result
|
<div className="relative flex shrink-0 items-center">
|
||||||
</span>
|
<span
|
||||||
) : (
|
className={
|
||||||
<MemberBadge
|
onExpand && expandItemKey
|
||||||
name={senderName}
|
? 'text-[10px] transition-opacity group-hover:opacity-0'
|
||||||
color={senderColor}
|
: 'text-[10px]'
|
||||||
teamName={teamName}
|
}
|
||||||
hideAvatar={senderHideAvatar || compactHeader}
|
style={{ color: CARD_ICON_MUTED }}
|
||||||
onClick={onMemberNameClick}
|
>
|
||||||
disableHoverCard={crossTeamOrigin != null}
|
{timestamp}
|
||||||
/>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Role */}
|
|
||||||
{!compactHeader && formattedRole && !isSlashCommandResult ? (
|
|
||||||
<span className="text-[10px]" style={{ color: CARD_ICON_MUTED }}>
|
|
||||||
{formattedRole}
|
|
||||||
</span>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
{/* Message type label or system label */}
|
|
||||||
{systemLabel ? (
|
|
||||||
<span
|
|
||||||
className="text-[10px] uppercase tracking-wide"
|
|
||||||
style={{ color: CARD_ICON_MUTED }}
|
|
||||||
>
|
|
||||||
{systemLabel}
|
|
||||||
</span>
|
|
||||||
) : isSlashCommandResult && message.commandOutput ? (
|
|
||||||
<span
|
|
||||||
className={[
|
|
||||||
'inline-flex items-center rounded-full px-1.5 py-0.5 text-[10px] uppercase tracking-wide',
|
|
||||||
isCommandOutputError
|
|
||||||
? 'bg-rose-500/15 text-rose-300'
|
|
||||||
: 'bg-amber-500/15 text-amber-300',
|
|
||||||
].join(' ')}
|
|
||||||
>
|
|
||||||
{message.commandOutput.stream}
|
|
||||||
</span>
|
|
||||||
) : isSlashCommandMessage ? (
|
|
||||||
<span className="text-[10px] uppercase tracking-wide text-amber-400">command</span>
|
|
||||||
) : messageType ? (
|
|
||||||
<span
|
|
||||||
className="text-[10px] uppercase tracking-wide"
|
|
||||||
style={{ color: CARD_ICON_MUTED }}
|
|
||||||
>
|
|
||||||
{messageType}
|
|
||||||
</span>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
{/* Lead session marker */}
|
|
||||||
{message.source === 'lead_session' && !isSlashCommandResult ? (
|
|
||||||
<span
|
|
||||||
className="text-[10px] uppercase tracking-wide"
|
|
||||||
style={{ color: CARD_ICON_MUTED }}
|
|
||||||
>
|
|
||||||
session
|
|
||||||
</span>
|
|
||||||
) : message.source === 'lead_process' && !isSlashCommandResult ? (
|
|
||||||
<span
|
|
||||||
className="text-[10px] uppercase tracking-wide"
|
|
||||||
style={{ color: CARD_ICON_MUTED }}
|
|
||||||
>
|
|
||||||
live
|
|
||||||
</span>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
{/* Rate limit warning badge */}
|
|
||||||
{rateLimited ? (
|
|
||||||
<span className="inline-flex items-center gap-1 rounded-full bg-red-500/20 px-1.5 py-0.5 text-[10px] font-medium text-red-400">
|
|
||||||
<AlertTriangle size={10} />
|
|
||||||
Rate Limited
|
|
||||||
</span>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
{/* API Error warning badge */}
|
|
||||||
{isApiError && !rateLimited ? (
|
|
||||||
<span className="inline-flex items-center gap-1 rounded-full bg-red-500/20 px-1.5 py-0.5 text-[10px] font-medium text-red-400">
|
|
||||||
<AlertTriangle size={10} />
|
|
||||||
API Error
|
|
||||||
</span>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
{/* Recipient — arrow + avatar + badge */}
|
|
||||||
{message.to && message.to !== message.from ? (
|
|
||||||
<>
|
|
||||||
<MoveRight size={10} style={{ color: CARD_ICON_MUTED }} className="shrink-0" />
|
|
||||||
{crossTeamTarget ? (
|
|
||||||
<CrossTeamTeamBadge teamName={crossTeamTarget} onClick={onTeamClick} />
|
|
||||||
) : null}
|
|
||||||
{crossTeamSentMemberName || !crossTeamTarget ? (
|
|
||||||
<MemberBadge
|
|
||||||
name={crossTeamSentMemberName ?? qualifiedRecipient?.memberName ?? message.to}
|
|
||||||
color={crossTeamTarget ? undefined : recipientColor}
|
|
||||||
teamName={crossTeamTarget ? undefined : teamName}
|
|
||||||
hideAvatar={
|
|
||||||
compactHeader ||
|
|
||||||
(crossTeamSentMemberName ?? qualifiedRecipient?.memberName ?? message.to) ===
|
|
||||||
'user'
|
|
||||||
}
|
|
||||||
onClick={onMemberNameClick}
|
|
||||||
disableHoverCard={crossTeamTarget != null}
|
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
</>
|
|
||||||
) : null}
|
|
||||||
|
|
||||||
{/* Summary */}
|
|
||||||
<span className="min-w-0 flex-1 truncate text-xs" style={{ color: CARD_TEXT_LIGHT }}>
|
|
||||||
{isSlashCommandResult && message.commandOutput ? (
|
|
||||||
<span className="inline-flex items-center gap-1.5">
|
|
||||||
<Command
|
|
||||||
size={12}
|
|
||||||
className={[
|
|
||||||
'shrink-0',
|
|
||||||
isCommandOutputError ? 'text-rose-400' : 'text-amber-400',
|
|
||||||
].join(' ')}
|
|
||||||
/>
|
|
||||||
<span
|
|
||||||
className={[
|
|
||||||
'font-mono text-[11px]',
|
|
||||||
isCommandOutputError ? 'text-rose-300' : 'text-amber-300',
|
|
||||||
].join(' ')}
|
|
||||||
>
|
|
||||||
{message.commandOutput.commandLabel}
|
|
||||||
</span>
|
|
||||||
<span className="truncate text-[11px] text-[var(--color-text-secondary)]">
|
|
||||||
{message.summary || getCommandOutputSummary(message.text) || rawSummary}
|
|
||||||
</span>
|
|
||||||
</span>
|
|
||||||
) : isSlashCommandMessage && slashCommandMeta ? (
|
|
||||||
<span className="inline-flex items-center gap-1.5">
|
|
||||||
<Command size={12} className="shrink-0 text-amber-400" />
|
|
||||||
<span className="font-mono text-[11px] text-amber-300">
|
|
||||||
{slashCommandMeta.command}
|
|
||||||
</span>
|
|
||||||
{slashCommandMeta.args ? (
|
|
||||||
<span className="truncate text-[11px] text-[var(--color-text-secondary)]">
|
|
||||||
{slashCommandMeta.args.replace(/\n+/g, ' ')}
|
|
||||||
</span>
|
</span>
|
||||||
) : (slashCommandMeta.knownDescription ?? knownSlashCommand?.description) ? (
|
{onExpand && expandItemKey && (
|
||||||
<span className="truncate text-[11px] text-[var(--color-text-secondary)]">
|
<button
|
||||||
{slashCommandMeta.knownDescription ?? knownSlashCommand?.description}
|
type="button"
|
||||||
</span>
|
aria-label="Expand message"
|
||||||
) : null}
|
className="absolute right-0 top-1/2 -translate-y-1/2 rounded p-0.5 opacity-0 transition-opacity focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-blue-500/50 group-hover:opacity-100"
|
||||||
</span>
|
style={{ color: CARD_ICON_MUTED }}
|
||||||
) : onTaskIdClick ? (
|
onClick={(e) => {
|
||||||
renderInlineBoldSummary(rawSummary, onTaskIdClick)
|
e.stopPropagation();
|
||||||
) : (
|
onExpand(expandItemKey);
|
||||||
renderInlineBoldSummary(rawSummary)
|
}}
|
||||||
)}
|
onKeyDown={(e) => e.stopPropagation()}
|
||||||
</span>
|
>
|
||||||
|
<Maximize2 size={12} />
|
||||||
{/* Timestamp / expand */}
|
</button>
|
||||||
<div className="relative flex shrink-0 items-center">
|
)}
|
||||||
<span
|
</div>
|
||||||
className={
|
</div>
|
||||||
onExpand && expandItemKey
|
<div
|
||||||
? 'text-[10px] transition-opacity group-hover:opacity-0'
|
className="mt-1 min-w-0 truncate text-[11px]"
|
||||||
: 'text-[10px]'
|
style={{ color: CARD_TEXT_LIGHT }}
|
||||||
}
|
title={summaryText || rawSummary}
|
||||||
style={{ color: CARD_ICON_MUTED }}
|
|
||||||
>
|
|
||||||
{timestamp}
|
|
||||||
</span>
|
|
||||||
{onExpand && expandItemKey && (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
aria-label="Expand message"
|
|
||||||
className="absolute right-0 top-1/2 -translate-y-1/2 rounded p-0.5 opacity-0 transition-opacity focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-blue-500/50 group-hover:opacity-100"
|
|
||||||
style={{ color: CARD_ICON_MUTED }}
|
|
||||||
onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
onExpand(expandItemKey);
|
|
||||||
}}
|
|
||||||
onKeyDown={(e) => e.stopPropagation()}
|
|
||||||
>
|
>
|
||||||
<Maximize2 size={12} />
|
{summaryContent}
|
||||||
</button>
|
</div>
|
||||||
)}
|
</div>
|
||||||
</div>
|
) : (
|
||||||
|
<>
|
||||||
|
{isUnread ? (
|
||||||
|
<span
|
||||||
|
className="size-2 shrink-0 rounded-full bg-blue-500"
|
||||||
|
title="Unread"
|
||||||
|
aria-hidden
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
{showChevron ? (
|
||||||
|
<ChevronRight
|
||||||
|
className="size-3 shrink-0 transition-transform duration-150"
|
||||||
|
style={{
|
||||||
|
color: CARD_ICON_MUTED,
|
||||||
|
transform: isExpanded ? 'rotate(90deg)' : undefined,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
|
{crossTeamOrigin ? (
|
||||||
|
<CrossTeamTeamBadge teamName={crossTeamOrigin.teamName} onClick={onTeamClick} />
|
||||||
|
) : null}
|
||||||
|
{senderBadge}
|
||||||
|
{!compactHeader && formattedRole && !isSlashCommandResult ? (
|
||||||
|
<span className="text-[10px]" style={{ color: CARD_ICON_MUTED }}>
|
||||||
|
{formattedRole}
|
||||||
|
</span>
|
||||||
|
) : null}
|
||||||
|
{messageTypeBadge}
|
||||||
|
{leadSourceBadge}
|
||||||
|
{statusBadge}
|
||||||
|
{recipientBadge}
|
||||||
|
<span className="min-w-0 flex-1 truncate text-xs" style={{ color: CARD_TEXT_LIGHT }}>
|
||||||
|
{summaryContent}
|
||||||
|
</span>
|
||||||
|
<div className="relative flex shrink-0 items-center">
|
||||||
|
<span
|
||||||
|
className={
|
||||||
|
onExpand && expandItemKey
|
||||||
|
? 'text-[10px] transition-opacity group-hover:opacity-0'
|
||||||
|
: 'text-[10px]'
|
||||||
|
}
|
||||||
|
style={{ color: CARD_ICON_MUTED }}
|
||||||
|
>
|
||||||
|
{timestamp}
|
||||||
|
</span>
|
||||||
|
{onExpand && expandItemKey && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
aria-label="Expand message"
|
||||||
|
className="absolute right-0 top-1/2 -translate-y-1/2 rounded p-0.5 opacity-0 transition-opacity focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-blue-500/50 group-hover:opacity-100"
|
||||||
|
style={{ color: CARD_ICON_MUTED }}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onExpand(expandItemKey);
|
||||||
|
}}
|
||||||
|
onKeyDown={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
<Maximize2 size={12} />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Content — collapsed for system messages, expanded for others */}
|
{/* Content — collapsed for system messages, expanded for others */}
|
||||||
|
|
|
||||||
|
|
@ -751,6 +751,13 @@ const LeadThoughtsGroupRowComponent = ({
|
||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const timestampLabel =
|
||||||
|
formatTime(oldest.timestamp) === formatTime(newest.timestamp)
|
||||||
|
? formatTime(oldest.timestamp)
|
||||||
|
: `${formatTime(oldest.timestamp)}–${formatTime(newest.timestamp)}`;
|
||||||
|
const useCompactCollapsedHeader = compactHeader && !isBodyVisible;
|
||||||
|
const compactPreviewText = headerTextPreview ?? totalToolSummary;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AnimatedHeightReveal animate={isNew} containerRef={ref} style={{ overflowAnchor: 'none' }}>
|
<AnimatedHeightReveal animate={isNew} containerRef={ref} style={{ overflowAnchor: 'none' }}>
|
||||||
<article
|
<article
|
||||||
|
|
@ -767,7 +774,9 @@ const LeadThoughtsGroupRowComponent = ({
|
||||||
role={canToggleBodyVisibility ? 'button' : undefined}
|
role={canToggleBodyVisibility ? 'button' : undefined}
|
||||||
tabIndex={canToggleBodyVisibility ? 0 : undefined}
|
tabIndex={canToggleBodyVisibility ? 0 : undefined}
|
||||||
className={[
|
className={[
|
||||||
'flex select-none items-center gap-2 px-3 py-1.5',
|
useCompactCollapsedHeader
|
||||||
|
? 'select-none px-3 py-2'
|
||||||
|
: 'flex select-none items-center gap-2 px-3 py-1.5',
|
||||||
canToggleBodyVisibility ? 'cursor-pointer' : '',
|
canToggleBodyVisibility ? 'cursor-pointer' : '',
|
||||||
].join(' ')}
|
].join(' ')}
|
||||||
style={hasApiError ? { backgroundColor: 'rgba(248, 113, 113, 0.08)' } : undefined}
|
style={hasApiError ? { backgroundColor: 'rgba(248, 113, 113, 0.08)' } : undefined}
|
||||||
|
|
@ -783,101 +792,148 @@ const LeadThoughtsGroupRowComponent = ({
|
||||||
: undefined
|
: undefined
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{/* Chevron for collapse mode */}
|
{useCompactCollapsedHeader ? (
|
||||||
{canToggleBodyVisibility && !compactHeader ? (
|
<div className="min-w-0">
|
||||||
<ChevronRight
|
<div className="flex min-w-0 items-start gap-3">
|
||||||
className="size-3 shrink-0 transition-transform duration-150"
|
<div className="flex min-w-0 flex-1 items-center gap-2 overflow-hidden">
|
||||||
style={{
|
<MemberBadge name={leadName} color={memberColor} hideAvatar />
|
||||||
color: CARD_ICON_MUTED,
|
<span className="shrink-0 text-[10px]" style={{ color: CARD_ICON_MUTED }}>
|
||||||
transform: isBodyVisible ? 'rotate(90deg)' : undefined,
|
{thoughts.length} thoughts
|
||||||
}}
|
</span>
|
||||||
/>
|
</div>
|
||||||
) : null}
|
<div className="relative flex shrink-0 items-center">
|
||||||
{/* Lead avatar with optional live indicator */}
|
<span
|
||||||
{!compactHeader ? (
|
className={
|
||||||
<div className="relative shrink-0">
|
onExpand && expandItemKey
|
||||||
<img
|
? 'text-[10px] transition-opacity group-hover:opacity-0'
|
||||||
src={agentAvatarUrl(leadName, 24)}
|
: 'text-[10px]'
|
||||||
alt=""
|
}
|
||||||
className="size-5 rounded-full bg-[var(--color-surface-raised)]"
|
style={{ color: CARD_ICON_MUTED }}
|
||||||
loading="lazy"
|
>
|
||||||
/>
|
{timestampLabel}
|
||||||
<LiveThoughtStatusBadge
|
</span>
|
||||||
canBeLive={canBeLive}
|
{onExpand && expandItemKey && (
|
||||||
isTeamAlive={isTeamAlive}
|
<button
|
||||||
leadActivity={leadActivity}
|
type="button"
|
||||||
leadContextUpdatedAt={leadContextUpdatedAt}
|
aria-label="Expand thoughts"
|
||||||
newestTimestamp={newest.timestamp}
|
className="absolute right-0 top-1/2 -translate-y-1/2 rounded p-0.5 opacity-0 transition-opacity focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-blue-500/50 group-hover:opacity-100"
|
||||||
/>
|
style={{ color: CARD_ICON_MUTED }}
|
||||||
</div>
|
onClick={(e) => {
|
||||||
) : null}
|
e.stopPropagation();
|
||||||
<MemberBadge name={leadName} color={memberColor} hideAvatar />
|
onExpand(expandItemKey);
|
||||||
<span className="text-[10px]" style={{ color: CARD_ICON_MUTED }}>
|
}}
|
||||||
{thoughts.length} thoughts
|
onKeyDown={(e) => e.stopPropagation()}
|
||||||
</span>
|
>
|
||||||
{!isBodyVisible && headerTextPreview ? (
|
<Maximize2 size={12} />
|
||||||
<Tooltip>
|
</button>
|
||||||
<TooltipTrigger asChild>
|
)}
|
||||||
<span
|
</div>
|
||||||
className="min-w-0 flex-1 cursor-default truncate text-[10px]"
|
</div>
|
||||||
style={{ color: CARD_TEXT_LIGHT }}
|
{compactPreviewText ? (
|
||||||
|
<div
|
||||||
|
className="mt-1 min-w-0 truncate text-[11px]"
|
||||||
|
style={{ color: headerTextPreview ? CARD_TEXT_LIGHT : CARD_ICON_MUTED }}
|
||||||
|
title={compactPreviewText}
|
||||||
>
|
>
|
||||||
{headerTextPreview}
|
{compactPreviewText}
|
||||||
</span>
|
</div>
|
||||||
</TooltipTrigger>
|
|
||||||
{totalToolSummary ? (
|
|
||||||
<TooltipContent side="bottom" className="max-w-[420px] font-mono text-[11px]">
|
|
||||||
<ToolSummaryTooltipContent
|
|
||||||
toolCalls={allToolCalls}
|
|
||||||
toolSummary={totalToolSummary}
|
|
||||||
/>
|
|
||||||
</TooltipContent>
|
|
||||||
) : null}
|
) : null}
|
||||||
</Tooltip>
|
</div>
|
||||||
) : totalToolSummary ? (
|
) : (
|
||||||
<Tooltip>
|
<>
|
||||||
<TooltipTrigger asChild>
|
{canToggleBodyVisibility && !compactHeader ? (
|
||||||
<span className="cursor-default text-[10px]" style={{ color: CARD_ICON_MUTED }}>
|
<ChevronRight
|
||||||
{totalToolSummary}
|
className="size-3 shrink-0 transition-transform duration-150"
|
||||||
</span>
|
style={{
|
||||||
</TooltipTrigger>
|
color: CARD_ICON_MUTED,
|
||||||
<TooltipContent side="bottom" className="max-w-[420px] font-mono text-[11px]">
|
transform: isBodyVisible ? 'rotate(90deg)' : undefined,
|
||||||
<ToolSummaryTooltipContent
|
}}
|
||||||
toolCalls={allToolCalls}
|
|
||||||
toolSummary={totalToolSummary}
|
|
||||||
/>
|
/>
|
||||||
</TooltipContent>
|
) : null}
|
||||||
</Tooltip>
|
{!compactHeader ? (
|
||||||
) : null}
|
<div className="relative shrink-0">
|
||||||
<div className="relative ml-auto flex shrink-0 items-center">
|
<img
|
||||||
<span
|
src={agentAvatarUrl(leadName, 24)}
|
||||||
className={
|
alt=""
|
||||||
onExpand && expandItemKey
|
className="size-5 rounded-full bg-[var(--color-surface-raised)]"
|
||||||
? 'text-[10px] transition-opacity group-hover:opacity-0'
|
loading="lazy"
|
||||||
: 'text-[10px]'
|
/>
|
||||||
}
|
<LiveThoughtStatusBadge
|
||||||
style={{ color: CARD_ICON_MUTED }}
|
canBeLive={canBeLive}
|
||||||
>
|
isTeamAlive={isTeamAlive}
|
||||||
{formatTime(oldest.timestamp) === formatTime(newest.timestamp)
|
leadActivity={leadActivity}
|
||||||
? formatTime(oldest.timestamp)
|
leadContextUpdatedAt={leadContextUpdatedAt}
|
||||||
: `${formatTime(oldest.timestamp)}–${formatTime(newest.timestamp)}`}
|
newestTimestamp={newest.timestamp}
|
||||||
</span>
|
/>
|
||||||
{onExpand && expandItemKey && (
|
</div>
|
||||||
<button
|
) : null}
|
||||||
type="button"
|
<MemberBadge name={leadName} color={memberColor} hideAvatar />
|
||||||
aria-label="Expand thoughts"
|
<span className="text-[10px]" style={{ color: CARD_ICON_MUTED }}>
|
||||||
className="absolute right-0 top-1/2 -translate-y-1/2 rounded p-0.5 opacity-0 transition-opacity focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-blue-500/50 group-hover:opacity-100"
|
{thoughts.length} thoughts
|
||||||
style={{ color: CARD_ICON_MUTED }}
|
</span>
|
||||||
onClick={(e) => {
|
{!isBodyVisible && headerTextPreview ? (
|
||||||
e.stopPropagation();
|
<Tooltip>
|
||||||
onExpand(expandItemKey);
|
<TooltipTrigger asChild>
|
||||||
}}
|
<span
|
||||||
onKeyDown={(e) => e.stopPropagation()}
|
className="min-w-0 flex-1 cursor-default truncate text-[10px]"
|
||||||
>
|
style={{ color: CARD_TEXT_LIGHT }}
|
||||||
<Maximize2 size={12} />
|
>
|
||||||
</button>
|
{headerTextPreview}
|
||||||
)}
|
</span>
|
||||||
</div>
|
</TooltipTrigger>
|
||||||
|
{totalToolSummary ? (
|
||||||
|
<TooltipContent side="bottom" className="max-w-[420px] font-mono text-[11px]">
|
||||||
|
<ToolSummaryTooltipContent
|
||||||
|
toolCalls={allToolCalls}
|
||||||
|
toolSummary={totalToolSummary}
|
||||||
|
/>
|
||||||
|
</TooltipContent>
|
||||||
|
) : null}
|
||||||
|
</Tooltip>
|
||||||
|
) : totalToolSummary ? (
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<span className="cursor-default text-[10px]" style={{ color: CARD_ICON_MUTED }}>
|
||||||
|
{totalToolSummary}
|
||||||
|
</span>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent side="bottom" className="max-w-[420px] font-mono text-[11px]">
|
||||||
|
<ToolSummaryTooltipContent
|
||||||
|
toolCalls={allToolCalls}
|
||||||
|
toolSummary={totalToolSummary}
|
||||||
|
/>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
) : null}
|
||||||
|
<div className="relative ml-auto flex shrink-0 items-center">
|
||||||
|
<span
|
||||||
|
className={
|
||||||
|
onExpand && expandItemKey
|
||||||
|
? 'text-[10px] transition-opacity group-hover:opacity-0'
|
||||||
|
: 'text-[10px]'
|
||||||
|
}
|
||||||
|
style={{ color: CARD_ICON_MUTED }}
|
||||||
|
>
|
||||||
|
{timestampLabel}
|
||||||
|
</span>
|
||||||
|
{onExpand && expandItemKey && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
aria-label="Expand thoughts"
|
||||||
|
className="absolute right-0 top-1/2 -translate-y-1/2 rounded p-0.5 opacity-0 transition-opacity focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-blue-500/50 group-hover:opacity-100"
|
||||||
|
style={{ color: CARD_ICON_MUTED }}
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation();
|
||||||
|
onExpand(expandItemKey);
|
||||||
|
}}
|
||||||
|
onKeyDown={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
<Maximize2 size={12} />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Scrollable body — live thoughts follow bottom unless user scrolls up */}
|
{/* Scrollable body — live thoughts follow bottom unless user scrolls up */}
|
||||||
|
|
|
||||||
|
|
@ -16,6 +16,7 @@ import { Loader2, ShieldQuestion, Users } from 'lucide-react';
|
||||||
import { useShallow } from 'zustand/react/shallow';
|
import { useShallow } from 'zustand/react/shallow';
|
||||||
|
|
||||||
import type { ResolvedTeamMember } from '@shared/types';
|
import type { ResolvedTeamMember } from '@shared/types';
|
||||||
|
import type { ReactNode } from 'react';
|
||||||
|
|
||||||
export interface PendingCrossTeamReply {
|
export interface PendingCrossTeamReply {
|
||||||
teamName: string;
|
teamName: string;
|
||||||
|
|
@ -26,6 +27,7 @@ interface PendingRepliesBlockProps {
|
||||||
members: ResolvedTeamMember[];
|
members: ResolvedTeamMember[];
|
||||||
pendingRepliesByMember: Record<string, number>;
|
pendingRepliesByMember: Record<string, number>;
|
||||||
pendingCrossTeamReplies?: PendingCrossTeamReply[];
|
pendingCrossTeamReplies?: PendingCrossTeamReply[];
|
||||||
|
headerRight?: ReactNode;
|
||||||
onMemberClick?: (member: ResolvedTeamMember) => void;
|
onMemberClick?: (member: ResolvedTeamMember) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -33,6 +35,7 @@ export const PendingRepliesBlock = ({
|
||||||
members,
|
members,
|
||||||
pendingRepliesByMember,
|
pendingRepliesByMember,
|
||||||
pendingCrossTeamReplies = [],
|
pendingCrossTeamReplies = [],
|
||||||
|
headerRight,
|
||||||
onMemberClick,
|
onMemberClick,
|
||||||
}: PendingRepliesBlockProps): React.JSX.Element | null => {
|
}: PendingRepliesBlockProps): React.JSX.Element | null => {
|
||||||
const { isLight } = useTheme();
|
const { isLight } = useTheme();
|
||||||
|
|
@ -70,9 +73,12 @@ export const PendingRepliesBlock = ({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="mb-3 space-y-1.5">
|
<div className="mb-3 space-y-1.5">
|
||||||
<p className="text-[10px] font-medium uppercase tracking-wide text-[var(--color-text-muted)]">
|
<div className="flex items-center justify-between gap-3">
|
||||||
Awaiting replies
|
<p className="text-[10px] font-medium uppercase tracking-wide text-[var(--color-text-muted)]">
|
||||||
</p>
|
Awaiting replies
|
||||||
|
</p>
|
||||||
|
{headerRight ? <div className="shrink-0">{headerRight}</div> : null}
|
||||||
|
</div>
|
||||||
{pending.map((entry) => {
|
{pending.map((entry) => {
|
||||||
const since = formatDistanceToNowStrict(entry.sentAtMs, { addSuffix: true });
|
const since = formatDistanceToNowStrict(entry.sentAtMs, { addSuffix: true });
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -465,7 +465,7 @@ export const MessageComposer = ({
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn('relative', isCompactLayout ? 'pb-1' : 'mb-3 pb-3')}
|
className={cn('relative', isCompactLayout ? 'pb-1' : 'mb-1.5 pb-1.5')}
|
||||||
role="group"
|
role="group"
|
||||||
onDragEnter={handleDragEnter}
|
onDragEnter={handleDragEnter}
|
||||||
onDragLeave={handleDragLeave}
|
onDragLeave={handleDragLeave}
|
||||||
|
|
|
||||||
|
|
@ -648,6 +648,7 @@ export const MessagesPanel = memo(function MessagesPanel({
|
||||||
tasks={tasks}
|
tasks={tasks}
|
||||||
messages={effectiveMessages}
|
messages={effectiveMessages}
|
||||||
pendingRepliesByMember={pendingRepliesByMember}
|
pendingRepliesByMember={pendingRepliesByMember}
|
||||||
|
layout="flow"
|
||||||
position="inline"
|
position="inline"
|
||||||
onMemberClick={onMemberClick}
|
onMemberClick={onMemberClick}
|
||||||
onTaskClick={onTaskClick}
|
onTaskClick={onTaskClick}
|
||||||
|
|
@ -831,6 +832,7 @@ export const MessagesPanel = memo(function MessagesPanel({
|
||||||
tasks={tasks}
|
tasks={tasks}
|
||||||
messages={effectiveMessages}
|
messages={effectiveMessages}
|
||||||
pendingRepliesByMember={pendingRepliesByMember}
|
pendingRepliesByMember={pendingRepliesByMember}
|
||||||
|
layout="flow"
|
||||||
position="sidebar"
|
position="sidebar"
|
||||||
onMemberClick={onMemberClick}
|
onMemberClick={onMemberClick}
|
||||||
onTaskClick={onTaskClick}
|
onTaskClick={onTaskClick}
|
||||||
|
|
|
||||||
|
|
@ -44,15 +44,13 @@ export const StatusBlock = ({
|
||||||
() => computePendingCrossTeamReplies(messages, nowMs),
|
() => computePendingCrossTeamReplies(messages, nowMs),
|
||||||
[messages, nowMs]
|
[messages, nowMs]
|
||||||
);
|
);
|
||||||
|
const hasPendingReplies = useMemo(() => {
|
||||||
/** Whether the Status block has any visible items. */
|
const hasMemberPendingReplies = Object.keys(pendingRepliesByMember).some((name) =>
|
||||||
const hasItems = useMemo(() => {
|
|
||||||
const hasPendingReplies = Object.keys(pendingRepliesByMember).some((name) =>
|
|
||||||
members.some((m) => m.name === name)
|
members.some((m) => m.name === name)
|
||||||
);
|
);
|
||||||
if (hasPendingReplies) return true;
|
return hasMemberPendingReplies || pendingCrossTeamReplies.length > 0;
|
||||||
if (pendingCrossTeamReplies.length > 0) return true;
|
}, [members, pendingRepliesByMember, pendingCrossTeamReplies.length]);
|
||||||
|
const hasActiveTasks = useMemo(() => {
|
||||||
const tMap = new Map(tasks.map((t) => [t.id, t]));
|
const tMap = new Map(tasks.map((t) => [t.id, t]));
|
||||||
return members.some((m) => {
|
return members.some((m) => {
|
||||||
if (!m.currentTaskId) return false;
|
if (!m.currentTaskId) return false;
|
||||||
|
|
@ -60,7 +58,13 @@ export const StatusBlock = ({
|
||||||
if (task && (task.reviewState === 'approved' || task.status === 'completed')) return false;
|
if (task && (task.reviewState === 'approved' || task.status === 'completed')) return false;
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
}, [members, tasks, pendingRepliesByMember, pendingCrossTeamReplies.length]);
|
}, [members, tasks]);
|
||||||
|
|
||||||
|
/** Whether the Status block has any visible items. */
|
||||||
|
const hasItems = useMemo(() => {
|
||||||
|
if (hasPendingReplies) return true;
|
||||||
|
return hasActiveTasks;
|
||||||
|
}, [hasActiveTasks, hasPendingReplies]);
|
||||||
|
|
||||||
// Only run the 1-second timer when the block actually has content to show.
|
// Only run the 1-second timer when the block actually has content to show.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -85,6 +89,7 @@ export const StatusBlock = ({
|
||||||
Status
|
Status
|
||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
|
const flowInlineToggle = layout === 'flow' && !collapsed ? toggleButton : null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
@ -92,21 +97,25 @@ export const StatusBlock = ({
|
||||||
<div className="relative h-0">
|
<div className="relative h-0">
|
||||||
<div className="absolute -top-[19px] right-0 z-10">{toggleButton}</div>
|
<div className="absolute -top-[19px] right-0 z-10">{toggleButton}</div>
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : collapsed ? (
|
||||||
<div className="mb-2 flex justify-end">{toggleButton}</div>
|
<div className="mb-2 flex justify-end">{toggleButton}</div>
|
||||||
)}
|
) : null}
|
||||||
{!collapsed && (
|
{!collapsed && (
|
||||||
<div className={layout === 'overlay' ? 'mt-5' : ''}>
|
<div className={layout === 'overlay' ? 'mt-5' : ''}>
|
||||||
<PendingRepliesBlock
|
{hasPendingReplies ? (
|
||||||
members={members}
|
<PendingRepliesBlock
|
||||||
pendingRepliesByMember={pendingRepliesByMember}
|
members={members}
|
||||||
pendingCrossTeamReplies={pendingCrossTeamReplies}
|
pendingRepliesByMember={pendingRepliesByMember}
|
||||||
onMemberClick={onMemberClick}
|
pendingCrossTeamReplies={pendingCrossTeamReplies}
|
||||||
/>
|
headerRight={flowInlineToggle}
|
||||||
|
onMemberClick={onMemberClick}
|
||||||
|
/>
|
||||||
|
) : null}
|
||||||
<ActiveTasksBlock
|
<ActiveTasksBlock
|
||||||
members={members}
|
members={members}
|
||||||
tasks={tasks}
|
tasks={tasks}
|
||||||
defaultCollapsed={position === 'sidebar'}
|
defaultCollapsed={position === 'sidebar'}
|
||||||
|
headerRight={!hasPendingReplies ? flowInlineToggle : undefined}
|
||||||
onMemberClick={onMemberClick}
|
onMemberClick={onMemberClick}
|
||||||
onTaskClick={onTaskClick}
|
onTaskClick={onTaskClick}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -1102,10 +1102,10 @@ export const MentionableTextarea = React.forwardRef<HTMLTextAreaElement, Mention
|
||||||
const orbitTrackWidth = 1;
|
const orbitTrackWidth = 1;
|
||||||
const orbitStrokeWidth = 1.35;
|
const orbitStrokeWidth = 1.35;
|
||||||
const orbitGlowWidth = 3;
|
const orbitGlowWidth = 3;
|
||||||
const orbitInset = orbitTrackWidth / 2;
|
const orbitInset = 0;
|
||||||
const orbitWidth = Math.max(surfaceShellMetrics.width - orbitTrackWidth, 0);
|
const orbitWidth = Math.max(surfaceShellMetrics.width, 0);
|
||||||
const orbitHeight = Math.max(surfaceShellMetrics.height - orbitTrackWidth, 0);
|
const orbitHeight = Math.max(surfaceShellMetrics.height, 0);
|
||||||
const orbitRadius = Math.max(surfaceShellMetrics.borderRadius - orbitInset, 0);
|
const orbitRadius = Math.max(surfaceShellMetrics.borderRadius, 0);
|
||||||
const orbitRight = orbitInset + orbitWidth;
|
const orbitRight = orbitInset + orbitWidth;
|
||||||
const orbitBottom = orbitInset + orbitHeight;
|
const orbitBottom = orbitInset + orbitHeight;
|
||||||
const orbitMidX = orbitInset + orbitWidth / 2;
|
const orbitMidX = orbitInset + orbitWidth / 2;
|
||||||
|
|
@ -1134,7 +1134,7 @@ export const MentionableTextarea = React.forwardRef<HTMLTextAreaElement, Mention
|
||||||
surfaceShellMetrics.width > 0 &&
|
surfaceShellMetrics.width > 0 &&
|
||||||
surfaceShellMetrics.height > 0 ? (
|
surfaceShellMetrics.height > 0 ? (
|
||||||
<svg
|
<svg
|
||||||
className="message-composer-orbit-svg pointer-events-none absolute inset-0 z-[1] h-full w-full"
|
className="message-composer-orbit-svg pointer-events-none absolute inset-0 z-[16] h-full w-full"
|
||||||
viewBox={`0 0 ${surfaceShellMetrics.width} ${surfaceShellMetrics.height}`}
|
viewBox={`0 0 ${surfaceShellMetrics.width} ${surfaceShellMetrics.height}`}
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
>
|
>
|
||||||
|
|
|
||||||
|
|
@ -84,7 +84,7 @@ export const TeamGraphOverlay = ({
|
||||||
events={events}
|
events={events}
|
||||||
onRequestClose={onClose}
|
onRequestClose={onClose}
|
||||||
onRequestPinAsTab={onPinAsTab}
|
onRequestPinAsTab={onPinAsTab}
|
||||||
className="min-w-0 flex-1"
|
className="team-graph-view min-w-0 flex-1"
|
||||||
renderEdgeOverlay={({ edge, sourceNode, targetNode, onClose: closeEdge, onSelectNode }) => (
|
renderEdgeOverlay={({ edge, sourceNode, targetNode, onClose: closeEdge, onSelectNode }) => (
|
||||||
<GraphBlockingEdgePopover
|
<GraphBlockingEdgePopover
|
||||||
teamName={teamName}
|
teamName={teamName}
|
||||||
|
|
|
||||||
|
|
@ -114,7 +114,7 @@ export const TeamGraphTab = ({
|
||||||
<GraphView
|
<GraphView
|
||||||
data={graphData}
|
data={graphData}
|
||||||
events={events}
|
events={events}
|
||||||
className="size-full"
|
className="team-graph-view size-full"
|
||||||
suspendAnimation={!isActive}
|
suspendAnimation={!isActive}
|
||||||
onRequestFullscreen={() => setFullscreen(true)}
|
onRequestFullscreen={() => setFullscreen(true)}
|
||||||
renderEdgeOverlay={({ edge, sourceNode, targetNode, onClose, onSelectNode }) => (
|
renderEdgeOverlay={({ edge, sourceNode, targetNode, onClose, onSelectNode }) => (
|
||||||
|
|
|
||||||
|
|
@ -1367,7 +1367,7 @@ body.theme-transitioning {
|
||||||
.message-composer-orbit-glow {
|
.message-composer-orbit-glow {
|
||||||
stroke-dasharray: 9 91;
|
stroke-dasharray: 9 91;
|
||||||
animation: composer-orbit-border 19s linear infinite;
|
animation: composer-orbit-border 19s linear infinite;
|
||||||
stroke-linecap: round;
|
stroke-linecap: butt;
|
||||||
stroke-linejoin: round;
|
stroke-linejoin: round;
|
||||||
will-change: stroke-dashoffset;
|
will-change: stroke-dashoffset;
|
||||||
}
|
}
|
||||||
|
|
@ -1418,6 +1418,78 @@ body.theme-transitioning {
|
||||||
stroke: rgba(138, 130, 118, 0.2);
|
stroke: rgba(138, 130, 118, 0.2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
:root.light .team-graph-view > .absolute.left-20.top-3 {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root.light .team-graph-view > .absolute.right-3.top-3 > div:not(.relative),
|
||||||
|
:root.light .team-graph-view > .absolute.right-3.top-3 > .relative > div:first-child,
|
||||||
|
:root.light .team-graph-view > .absolute.bottom-3.right-3 > div,
|
||||||
|
:root.light .team-graph-view > .absolute.bottom-3.left-3 > div {
|
||||||
|
background: rgba(248, 244, 237, 0.9) !important;
|
||||||
|
border-color: rgba(120, 113, 108, 0.18) !important;
|
||||||
|
box-shadow: 0 10px 24px rgba(120, 113, 108, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
:root.light .team-graph-view > .absolute.right-3.top-3 > .relative > .absolute {
|
||||||
|
background: rgba(248, 244, 237, 0.96) !important;
|
||||||
|
border-color: rgba(120, 113, 108, 0.18) !important;
|
||||||
|
box-shadow: 0 16px 32px rgba(120, 113, 108, 0.12);
|
||||||
|
}
|
||||||
|
|
||||||
|
.team-graph-view > .absolute.right-3.top-3 > div:not(.relative),
|
||||||
|
.team-graph-view > .absolute.right-3.top-3 > .relative > div:first-child {
|
||||||
|
width: 32px !important;
|
||||||
|
min-width: 32px !important;
|
||||||
|
height: 32px !important;
|
||||||
|
min-height: 32px !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
justify-content: center !important;
|
||||||
|
border-radius: 10px !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.team-graph-view > .absolute.right-3.top-3 > div:not(.relative) > button,
|
||||||
|
.team-graph-view > .absolute.right-3.top-3 > .relative > div:first-child > button {
|
||||||
|
width: 100% !important;
|
||||||
|
height: 100% !important;
|
||||||
|
min-width: 100% !important;
|
||||||
|
min-height: 100% !important;
|
||||||
|
padding: 6px !important;
|
||||||
|
justify-content: center !important;
|
||||||
|
gap: 0 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.team-graph-view > .absolute.right-3.top-3 > div:not(.relative) > button svg,
|
||||||
|
.team-graph-view > .absolute.right-3.top-3 > .relative > div:first-child > button svg {
|
||||||
|
width: 100% !important;
|
||||||
|
height: 100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.team-graph-view > .absolute.right-3.top-3 > div:not(.relative) > button span,
|
||||||
|
.team-graph-view > .absolute.right-3.top-3 > .relative > div:first-child > button span {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root.light .team-graph-view button[class*='font-mono'] {
|
||||||
|
color: rgba(70, 85, 125, 0.9) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root.light .team-graph-view button[class*='font-mono']:hover {
|
||||||
|
color: #334155 !important;
|
||||||
|
background: rgba(79, 70, 229, 0.08) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root.light
|
||||||
|
.team-graph-view
|
||||||
|
button[class*='font-mono'][class*='bg-\\[rgba\\(100\\,200\\,255\\,0\\.14\\)\\]'] {
|
||||||
|
color: #4338ca !important;
|
||||||
|
background: rgba(79, 70, 229, 0.12) !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:root.light .team-graph-view button[class*='font-mono'] svg {
|
||||||
|
opacity: 0.96;
|
||||||
|
}
|
||||||
|
|
||||||
@media (prefers-reduced-motion: reduce) {
|
@media (prefers-reduced-motion: reduce) {
|
||||||
.message-composer-orbit-path,
|
.message-composer-orbit-path,
|
||||||
.message-composer-orbit-glow {
|
.message-composer-orbit-glow {
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue