feat: enhance theme support and UI consistency across components

- Added theme-aware accent and info colors in tailwind configuration for improved visual consistency.
- Updated CSS variables for accent and info styles to support light and dark themes.
- Refactored various components to utilize the new themed badge logic, ensuring consistent styling based on the current theme.
- Improved accessibility and visual feedback in components like SidebarTaskItem, TeamListView, and ActivityItem by adjusting color schemes and hover states.
- Enhanced the CreateTeamDialog and other team-related components with updated styling for better user experience.
This commit is contained in:
iliya 2026-03-07 12:02:12 +02:00
parent 919d40b7bc
commit e9b369e667
41 changed files with 342 additions and 143 deletions

View file

@ -9,7 +9,8 @@
import React, { useRef } from 'react';
import { CARD_ICON_MUTED } from '@renderer/constants/cssVariables';
import { getTeamColorSet } from '@renderer/constants/teamColors';
import { getTeamColorSet, getThemedBadge } from '@renderer/constants/teamColors';
import { useTheme } from '@renderer/hooks/useTheme';
import {
getToolContextTokens,
getToolStatus,
@ -70,6 +71,7 @@ export const LinkedToolItem: React.FC<LinkedToolItemProps> = ({
registerRef,
}) => {
const status = getToolStatus(linkedTool);
const { isLight } = useTheme();
const summary = getToolSummary(linkedTool.name, linkedTool.input);
const summaryNode =
searchQueryOverride && searchQueryOverride.trim().length > 0
@ -104,7 +106,7 @@ export const LinkedToolItem: React.FC<LinkedToolItemProps> = ({
<span className="size-2.5 rounded-full" style={{ backgroundColor: colors.border }} />
<span
className="rounded px-1.5 py-0.5 text-[10px] font-medium"
style={{ backgroundColor: colors.badge, color: colors.text }}
style={{ backgroundColor: getThemedBadge(colors, isLight), color: colors.text }}
>
{name}
</span>

View file

@ -12,8 +12,13 @@ import {
COLOR_TEXT_MUTED,
COLOR_TEXT_SECONDARY,
} from '@renderer/constants/cssVariables';
import { getSubagentTypeColorSet, getTeamColorSet } from '@renderer/constants/teamColors';
import {
getSubagentTypeColorSet,
getTeamColorSet,
getThemedBadge,
} from '@renderer/constants/teamColors';
import { useTabUI } from '@renderer/hooks/useTabUI';
import { useTheme } from '@renderer/hooks/useTheme';
import { useStore } from '@renderer/store';
import { buildDisplayItemsFromMessages, buildSummary } from '@renderer/utils/aiGroupEnhancer';
import { computeSubagentPhaseBreakdown } from '@renderer/utils/aiGroupHelpers';
@ -80,6 +85,7 @@ export const SubagentItem: React.FC<SubagentItemProps> = ({
// Team member colors (when this subagent is a team member)
const teamColors = subagent.team ? getTeamColorSet(subagent.team.memberColor) : null;
const { isLight } = useTheme();
// Type-based colors for non-team subagents (from agent config or deterministic hash)
const typeColors = !teamColors ? getSubagentTypeColorSet(subagentType, agentConfigs) : null;
@ -233,7 +239,7 @@ export const SubagentItem: React.FC<SubagentItemProps> = ({
<span
className="rounded px-1.5 py-0.5 text-[10px] font-medium tracking-wide"
style={{
backgroundColor: teamColors.badge,
backgroundColor: getThemedBadge(teamColors, isLight),
color: teamColors.text,
border: `1px solid ${teamColors.border}40`,
}}
@ -305,7 +311,7 @@ export const SubagentItem: React.FC<SubagentItemProps> = ({
<span
className="rounded px-1.5 py-0.5 text-[10px] font-medium tracking-wide"
style={{
backgroundColor: teamColors.badge,
backgroundColor: getThemedBadge(teamColors, isLight),
color: teamColors.text,
border: `1px solid ${teamColors.border}40`,
}}
@ -316,7 +322,7 @@ export const SubagentItem: React.FC<SubagentItemProps> = ({
<span
className="rounded px-1.5 py-0.5 text-[10px] font-medium uppercase tracking-wide"
style={{
backgroundColor: typeColors!.badge,
backgroundColor: getThemedBadge(typeColors!, isLight),
color: typeColors!.text,
border: `1px solid ${typeColors!.border}40`,
}}

View file

@ -7,7 +7,8 @@ import {
CARD_ICON_MUTED,
CARD_TEXT_LIGHT,
} from '@renderer/constants/cssVariables';
import { getTeamColorSet } from '@renderer/constants/teamColors';
import { getTeamColorSet, getThemedBadge } from '@renderer/constants/teamColors';
import { useTheme } from '@renderer/hooks/useTheme';
import { detectOperationalNoise } from '@renderer/utils/agentMessageFormatting';
import { formatTokensCompact } from '@renderer/utils/formatters';
import { stripAgentBlocks } from '@shared/constants/agentBlocks';
@ -77,6 +78,7 @@ export const TeammateMessageItem: React.FC<TeammateMessageItemProps> = ({
highlightStyle,
}) => {
const colors = getTeamColorSet(teammateMessage.color);
const { isLight } = useTheme();
// Detect operational noise
const noiseLabel = useMemo(
@ -162,7 +164,7 @@ export const TeammateMessageItem: React.FC<TeammateMessageItemProps> = ({
<span
className="rounded px-1.5 py-0.5 text-[10px] font-medium tracking-wide"
style={{
backgroundColor: colors.badge,
backgroundColor: getThemedBadge(colors, isLight),
color: colors.text,
border: `1px solid ${colors.border}40`,
}}

View file

@ -23,7 +23,8 @@ import {
PROSE_TABLE_BORDER,
PROSE_TABLE_HEADER_BG,
} from '@renderer/constants/cssVariables';
import { getTeamColorSet } from '@renderer/constants/teamColors';
import { getTeamColorSet, getThemedBadge } from '@renderer/constants/teamColors';
import { useTheme } from '@renderer/hooks/useTheme';
import { useStore } from '@renderer/store';
import { REHYPE_PLUGINS, REHYPE_PLUGINS_NO_HIGHLIGHT } from '@renderer/utils/markdownPlugins';
import { FileText } from 'lucide-react';
@ -153,7 +154,10 @@ function hastToText(node: HastNode): string {
// Component factories
// =============================================================================
function createViewerMarkdownComponents(searchCtx: SearchContext | null): Components {
function createViewerMarkdownComponents(
searchCtx: SearchContext | null,
isLight = false
): Components {
const hl = (children: React.ReactNode): React.ReactNode =>
searchCtx ? highlightSearchInChildren(children, searchCtx) : children;
@ -214,7 +218,7 @@ function createViewerMarkdownComponents(searchCtx: SearchContext | null): Compon
// malformed percent-encoding — use empty color
}
const colorSet = getTeamColorSet(color);
const bg = colorSet.badge;
const bg = getThemedBadge(colorSet, isLight);
return (
<span
style={{
@ -450,6 +454,7 @@ export const MarkdownViewer: React.FC<MarkdownViewerProps> = ({
}) => {
const [showRaw, setShowRaw] = React.useState(false);
const [rawLimit, setRawLimit] = React.useState(LARGE_PREVIEW_CHARS);
const { isLight } = useTheme();
const isTooLarge = content.length > MAX_MARKDOWN_CHARS;
const disableHighlight = content.length > DISABLE_HIGHLIGHT_CHARS;
@ -601,7 +606,11 @@ export const MarkdownViewer: React.FC<MarkdownViewerProps> = ({
// Create markdown components with optional search highlighting
// When search is active, create fresh each render (match counter is stateful and must start at 0)
// useMemo would cache stale closures when parent re-renders without search deps changing
const baseComponents = searchCtx ? createViewerMarkdownComponents(searchCtx) : defaultComponents;
const baseComponents = searchCtx
? createViewerMarkdownComponents(searchCtx, isLight)
: isLight
? createViewerMarkdownComponents(null, true)
: defaultComponents;
// When baseDir is set (editor preview), override img to load local files via IPC
const components = baseDir

View file

@ -34,7 +34,7 @@ export const OngoingIndicator = ({
<span className={`relative inline-flex rounded-full ${dotSize} bg-green-500`} />
</span>
{showLabel && (
<span className="text-sm" style={{ color: 'var(--info-text, #3b82f6)' }}>
<span className="text-sm" style={{ color: 'var(--info-text)' }}>
{label}
</span>
)}
@ -51,15 +51,12 @@ export const OngoingBanner = (): React.JSX.Element => {
<div
className="flex w-full items-center justify-center gap-2 rounded-lg px-4 py-3"
style={{
backgroundColor: 'var(--info-bg, rgba(59, 130, 246, 0.1))',
border: '1px solid var(--info-border, rgba(59, 130, 246, 0.3))',
backgroundColor: 'var(--info-bg)',
border: '1px solid var(--info-border)',
}}
>
<Loader2
className="size-4 shrink-0 animate-spin"
style={{ color: 'var(--info-text, #3b82f6)' }}
/>
<span className="text-sm" style={{ color: 'var(--info-text, #3b82f6)' }}>
<Loader2 className="size-4 shrink-0 animate-spin" style={{ color: 'var(--info-text)' }} />
<span className="text-sm font-medium" style={{ color: 'var(--info-text)' }}>
Session is in progress...
</span>
</div>

View file

@ -37,7 +37,7 @@ export const UpdateBanner = (): React.JSX.Element | null => {
className="mb-1.5 flex items-center gap-2 text-xs"
style={{ color: 'var(--color-text-secondary)' }}
>
<Loader2 className="size-3.5 shrink-0 animate-spin text-blue-400" />
<Loader2 className="size-3.5 shrink-0 animate-spin text-blue-600 dark:text-blue-400" />
<span>Updating app</span>
<span className="tabular-nums" style={{ color: 'var(--color-text-muted)' }}>
{clampedPercent}%
@ -48,7 +48,7 @@ export const UpdateBanner = (): React.JSX.Element | null => {
style={{ backgroundColor: 'var(--color-border)' }}
>
<div
className="h-full rounded-full bg-blue-500 transition-all duration-300 ease-out"
className="h-full rounded-full bg-blue-600 transition-all duration-300 ease-out dark:bg-blue-500"
style={{ width: `${clampedPercent}%` }}
/>
</div>

View file

@ -34,7 +34,7 @@ const VARIANT_STYLES: Record<BannerVariant, { border: string; bg: string }> = {
loading: { border: 'var(--color-border)', bg: 'transparent' },
error: { border: '#ef4444', bg: 'rgba(239, 68, 68, 0.06)' },
success: { border: '#22c55e', bg: 'rgba(34, 197, 94, 0.04)' },
info: { border: '#3b82f6', bg: 'rgba(59, 130, 246, 0.04)' },
info: { border: 'var(--info-border)', bg: 'var(--info-bg)' },
warning: { border: '#f59e0b', bg: 'rgba(245, 158, 11, 0.06)' },
};
@ -255,7 +255,7 @@ export const CliStatusBanner = (): React.JSX.Element | null => {
>
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<Loader2 className="size-4 shrink-0 animate-spin text-blue-400" />
<Loader2 className="size-4 shrink-0 animate-spin text-blue-600 dark:text-blue-400" />
<span className="text-sm" style={{ color: 'var(--color-text-secondary)' }}>
Downloading Claude CLI...
</span>

View file

@ -287,7 +287,7 @@ const RepositoryCard = ({
<>
<span className="text-text-muted">·</span>
{taskCounts.inProgress > 0 && (
<span className="inline-flex items-center rounded-full bg-blue-500/15 px-1.5 py-0.5 text-[10px] font-medium text-blue-400">
<span className="inline-flex items-center rounded-full bg-blue-500/15 px-1.5 py-0.5 text-[10px] font-medium text-blue-600 dark:text-blue-400">
{taskCounts.inProgress} active
</span>
)}

View file

@ -7,7 +7,8 @@ import { useCallback, useState } from 'react';
import { useSortable } from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import { getTeamColorSet } from '@renderer/constants/teamColors';
import { getTeamColorSet, getThemedBadge } from '@renderer/constants/teamColors';
import { useTheme } from '@renderer/hooks/useTheme';
import { useStore } from '@renderer/store';
import { nameColorSet } from '@renderer/utils/projectColor';
import {
@ -61,6 +62,7 @@ export const SortableTab = ({
setRef,
}: SortableTabProps): React.JSX.Element => {
const [isHovered, setIsHovered] = useState(false);
const { isLight } = useTheme();
const isPinned = useStore(
useShallow((s) =>
@ -96,14 +98,14 @@ export const SortableTab = ({
opacity: isDragging ? 0.3 : 1,
backgroundColor: isActive
? teamColorSet
? teamColorSet.badge
? getThemedBadge(teamColorSet, isLight)
: 'var(--color-surface-raised)'
: isHovered
? teamColorSet
? teamColorSet.badge
? getThemedBadge(teamColorSet, isLight)
: 'var(--color-surface-overlay)'
: teamColorSet
? teamColorSet.badge
? getThemedBadge(teamColorSet, isLight)
: 'transparent',
color:
isActive || isHovered

View file

@ -7,12 +7,13 @@ import { useCallback, useEffect, useMemo, useState } from 'react';
import { api, isElectronMode } from '@renderer/api';
import { confirm } from '@renderer/components/common/ConfirmDialog';
import { Combobox } from '@renderer/components/ui/combobox';
import { cn } from '@renderer/lib/utils';
import { useStore } from '@renderer/store';
import { getFullResetState } from '@renderer/store/utils/stateResetHelpers';
import { AGENT_LANGUAGE_OPTIONS, resolveLanguageName } from '@shared/utils/agentLanguage';
import { Check, Copy, FolderOpen, Laptop, Loader2, RotateCcw } from 'lucide-react';
import { SettingRow, SettingsSectionHeader, SettingsSelect, SettingsToggle } from '../components';
import { SettingRow, SettingsSectionHeader, SettingsToggle } from '../components';
import type { SafeConfig } from '../hooks/useSettingsConfig';
import type { ClaudeRootInfo, WslClaudeRootCandidate } from '@shared/types';
@ -335,12 +336,24 @@ export const GeneralSection = ({
<SettingsSectionHeader title="Appearance" />
<SettingRow label="Theme" description="Choose your preferred color theme">
<SettingsSelect
value={safeConfig.general.theme}
options={THEME_OPTIONS}
onChange={onThemeChange}
disabled={saving}
/>
<div className="inline-flex rounded-md border border-[var(--color-border)] bg-[var(--color-surface)] p-0.5">
{THEME_OPTIONS.map((opt) => (
<button
key={opt.value}
type="button"
disabled={saving}
className={cn(
'rounded-[3px] px-3 py-1 text-xs font-medium transition-colors disabled:opacity-50',
safeConfig.general.theme === opt.value
? 'bg-[var(--color-surface-raised)] text-[var(--color-text)] shadow-sm'
: 'text-[var(--color-text-muted)] hover:text-[var(--color-text-secondary)]'
)}
onClick={() => onThemeChange(opt.value)}
>
{opt.label}
</button>
))}
</div>
</SettingRow>
<SettingRow
label="Expand AI responses by default"

View file

@ -494,7 +494,7 @@ export const GlobalTaskList = ({
<div className="flex shrink-0 items-center gap-1.5 px-2 py-1">
<span className="shrink-0 text-[11px] text-text-muted">Group by:</span>
<div
className="bg-surface-raised/60 inline-flex rounded-md p-0.5 text-[11px]"
className="border-border-emphasis/40 inline-flex rounded-md border bg-[var(--color-surface)] p-0.5 text-[11px]"
role="group"
aria-label="Group by"
>
@ -508,7 +508,7 @@ export const GlobalTaskList = ({
className={cn(
'rounded px-2 py-0.5 transition-colors',
groupingMode === mode
? 'bg-surface-raised text-text shadow-sm'
? 'ring-border-emphasis/60 bg-surface-raised text-text shadow-sm ring-1'
: 'text-text-muted hover:text-text-secondary'
)}
>

View file

@ -2,6 +2,7 @@ import { useEffect, useMemo, useRef, useState } from 'react';
import { Tooltip, TooltipContent, TooltipTrigger } from '@renderer/components/ui/tooltip';
import { getTeamColorSet } from '@renderer/constants/teamColors';
import { useTheme } from '@renderer/hooks/useTheme';
import { useUnreadCommentCount } from '@renderer/hooks/useUnreadCommentCount';
import { useStore } from '@renderer/store';
import { buildMemberColorMap } from '@renderer/utils/memberHelpers';
@ -78,6 +79,7 @@ export const SidebarTaskItem = ({
const openGlobalTaskDetail = useStore((s) => s.openGlobalTaskDetail);
const teamMembers = useStore((s) => s.teamByName[task.teamName]?.members);
const unreadCount = useUnreadCommentCount(task.teamName, task.id, task.comments);
const { isLight } = useTheme();
const isRenaming = renamingKey === `${task.teamName}:${task.id}`;
const displaySubject = getDisplaySubject?.(task) ?? task.subject;
@ -118,19 +120,24 @@ export const SidebarTaskItem = ({
return colorName ? getTeamColorSet(colorName) : null;
}, [teamMembers, task.owner]);
const ownerTextColor = useMemo(() => {
if (!ownerColorSet) return undefined;
return isLight && ownerColorSet.textLight ? ownerColorSet.textLight : ownerColorSet.text;
}, [ownerColorSet, isLight]);
const projectLabel = useMemo(() => {
if (!task.projectPath?.trim()) return null;
return projectLabelFromPath(task.projectPath);
}, [task.projectPath]);
const projectColorSet = useMemo(
() => (projectLabel ? projectColor(projectLabel) : null),
[projectLabel]
() => (projectLabel ? projectColor(projectLabel, isLight) : null),
[projectLabel, isLight]
);
const teamColor = useMemo(
() => (showTeamName ? nameColorSet(task.teamDisplayName) : null),
[showTeamName, task.teamDisplayName]
() => (showTeamName ? nameColorSet(task.teamDisplayName, isLight) : null),
[showTeamName, task.teamDisplayName, isLight]
);
const showTeamRow = showTeamName && !hideTeamName;
@ -220,17 +227,19 @@ export const SidebarTaskItem = ({
)}
{!showTeamRow && (
<>
{projectLabel && <span className="opacity-40">·</span>}
{projectLabel && <span className="opacity-100 dark:opacity-40">·</span>}
<span
className="shrink-0 opacity-60"
style={ownerColorSet ? { color: ownerColorSet.text } : undefined}
className="shrink-0 opacity-100 dark:opacity-60"
style={ownerTextColor ? { color: ownerTextColor } : undefined}
>
{task.owner ?? 'unassigned'}
</span>
</>
)}
{dateLabel && (
<span className={`ml-auto shrink-0 ${updatedLabel ? 'italic opacity-70' : ''}`}>
<span
className={`ml-auto shrink-0 ${updatedLabel ? 'italic opacity-100 dark:opacity-70' : ''}`}
>
{dateLabel}
</span>
)}
@ -242,14 +251,14 @@ export const SidebarTaskItem = ({
className="mt-0.5 flex w-full items-center gap-1.5 text-[10px] leading-tight"
style={{ color: 'var(--color-text-muted)' }}
>
<span className="shrink-0 opacity-50">Team:</span>
<span className="shrink-0 opacity-100 dark:opacity-50">Team:</span>
<span className="shrink-0" style={teamColor ? { color: teamColor.text } : undefined}>
{task.teamDisplayName}
</span>
<span className="opacity-40">·</span>
<span className="opacity-100 dark:opacity-40">·</span>
<span
className="shrink-0 opacity-60"
style={ownerColorSet ? { color: ownerColorSet.text } : undefined}
className="shrink-0 opacity-100 dark:opacity-60"
style={ownerTextColor ? { color: ownerTextColor } : undefined}
>
{task.owner ?? 'unassigned'}
</span>

View file

@ -106,7 +106,7 @@ export const CollapsibleTeamSection = ({
{secondaryBadge != null && secondaryBadge > 0 && (
<Badge
variant="secondary"
className="bg-blue-500/20 px-1.5 py-0.5 text-[10px] font-normal leading-none text-blue-400"
className="bg-blue-500/20 px-1.5 py-0.5 text-[10px] font-normal leading-none text-blue-600 dark:text-blue-400"
title={`${secondaryBadge} unread`}
>
{secondaryBadge} new

View file

@ -1,4 +1,5 @@
import { getTeamColorSet } from '@renderer/constants/teamColors';
import { getTeamColorSet, getThemedBadge } from '@renderer/constants/teamColors';
import { useTheme } from '@renderer/hooks/useTheme';
import { agentAvatarUrl } from '@renderer/utils/memberHelpers';
interface MemberBadgeProps {
@ -24,12 +25,13 @@ export const MemberBadge = ({
onClick,
}: MemberBadgeProps): React.JSX.Element => {
const colors = getTeamColorSet(color ?? '');
const { isLight } = useTheme();
const avatarSize = size === 'md' ? 32 : 24;
const avatarClass = size === 'md' ? 'size-6' : 'size-5';
const textClass = size === 'md' ? 'text-xs' : 'text-[10px]';
const badgeStyle = {
backgroundColor: colors.badge,
backgroundColor: getThemedBadge(colors, isLight),
color: colors.text,
border: `1px solid ${colors.border}40`,
};

View file

@ -13,11 +13,12 @@ import {
DialogTitle,
} from '@renderer/components/ui/dialog';
import { Tooltip, TooltipContent, TooltipTrigger } from '@renderer/components/ui/tooltip';
import { getTeamColorSet } from '@renderer/constants/teamColors';
import { getTeamColorSet, getThemedBadge } from '@renderer/constants/teamColors';
import { useBranchSync } from '@renderer/hooks/useBranchSync';
import { useTabUI } from '@renderer/hooks/useTabUI';
import { useTeamMessagesRead } from '@renderer/hooks/useTeamMessagesRead';
import { useTeamMessagesExpanded } from '@renderer/hooks/useTeamMessagesExpanded';
import { useTheme } from '@renderer/hooks/useTheme';
import { cn } from '@renderer/lib/utils';
import { useStore } from '@renderer/store';
import { createChipFromSelection } from '@renderer/utils/chipUtils';
@ -125,6 +126,7 @@ function filterKanbanTasks(tasks: TeamTaskWithKanban[], query: string): TeamTask
}
export const TeamDetailView = ({ teamName }: TeamDetailViewProps): React.JSX.Element => {
const { isLight } = useTheme();
const [requestChangesTaskId, setRequestChangesTaskId] = useState<string | null>(null);
const [selectedTask, setSelectedTask] = useState<TeamTaskWithKanban | null>(null);
const [selectedMember, setSelectedMember] = useState<ResolvedTeamMember | null>(null);
@ -961,7 +963,7 @@ export const TeamDetailView = ({ teamName }: TeamDetailViewProps): React.JSX.Ele
{headerColorSet ? (
<div
className="pointer-events-none absolute inset-0 z-0 rounded-lg"
style={{ backgroundColor: headerColorSet.badge }}
style={{ backgroundColor: getThemedBadge(headerColorSet, isLight) }}
/>
) : null}
<div

View file

@ -11,8 +11,9 @@ import {
TooltipProvider,
TooltipTrigger,
} from '@renderer/components/ui/tooltip';
import { getTeamColorSet } from '@renderer/constants/teamColors';
import { getTeamColorSet, getThemedBadge } from '@renderer/constants/teamColors';
import { useBranchSync } from '@renderer/hooks/useBranchSync';
import { useTheme } from '@renderer/hooks/useTheme';
import { useStore } from '@renderer/store';
import { buildMemberColorMap } from '@renderer/utils/memberHelpers';
import { buildTaskCountsByTeam, normalizePath } from '@renderer/utils/pathNormalize';
@ -70,7 +71,7 @@ function folderName(fullPath: string): string {
return getBaseName(fullPath) || fullPath;
}
function renderMemberChips(members: TeamSummaryMember[]): React.JSX.Element {
function renderMemberChips(members: TeamSummaryMember[], isLight: boolean): React.JSX.Element {
const teamColorMap = buildMemberColorMap(members);
return (
<>
@ -84,7 +85,7 @@ function renderMemberChips(members: TeamSummaryMember[]): React.JSX.Element {
style={
memberColor
? {
backgroundColor: memberColor.badge,
backgroundColor: getThemedBadge(memberColor, isLight),
color: memberColor.text,
border: `1px solid ${memberColor.border}40`,
}
@ -177,6 +178,7 @@ const StatusBadge = ({ status }: { status: TeamStatus }): React.JSX.Element => {
};
export const TeamListView = (): React.JSX.Element => {
const { isLight } = useTheme();
const electronMode = isElectronMode();
const [showCreateDialog, setShowCreateDialog] = useState(false);
const [copyData, setCopyData] = useState<TeamCopyData | null>(null);
@ -679,7 +681,7 @@ export const TeamListView = (): React.JSX.Element => {
{teamColorSet ? (
<div
className="pointer-events-none absolute inset-0 z-0 rounded-lg"
style={{ backgroundColor: teamColorSet.badge }}
style={{ backgroundColor: getThemedBadge(teamColorSet, isLight) }}
/>
) : null}
<div
@ -760,7 +762,7 @@ export const TeamListView = (): React.JSX.Element => {
</div>
<div className="mt-3 flex flex-wrap items-center gap-1.5">
{team.members && team.members.length > 0 ? (
renderMemberChips(team.members)
renderMemberChips(team.members, isLight)
) : team.memberCount === 0 ? (
<Badge variant="secondary" className="text-[10px] font-normal">
Solo
@ -895,7 +897,7 @@ export const TeamListView = (): React.JSX.Element => {
</p>
{team.members && team.members.length > 0 && (
<div className="mt-3 flex flex-wrap items-center gap-1.5">
{renderMemberChips(team.members)}
{renderMemberChips(team.members, isLight)}
</div>
)}
</div>

View file

@ -130,7 +130,7 @@ export const TeamSessionsSection = ({
{selectedSessionId !== null && (
<button
type="button"
className="flex w-full items-center gap-1.5 rounded-md px-2.5 py-1.5 text-xs text-blue-400 transition-colors hover:bg-blue-500/10"
className="flex w-full items-center gap-1.5 rounded-md px-2.5 py-1.5 text-xs text-blue-600 transition-colors hover:bg-blue-500/10 dark:text-blue-400"
onClick={() => onSelectSession(null)}
>
<FilterX size={12} />
@ -201,7 +201,7 @@ const SessionRow = ({
{isLead && (
<>
<span style={{ opacity: 0.5 }}>·</span>
<span className="text-blue-400">lead</span>
<span className="text-blue-600 dark:text-blue-400">lead</span>
</>
)}
</div>

View file

@ -1,6 +1,7 @@
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { getTeamColorSet } from '@renderer/constants/teamColors';
import { getTeamColorSet, getThemedBadge } from '@renderer/constants/teamColors';
import { useTheme } from '@renderer/hooks/useTheme';
import { useStore } from '@renderer/store';
import { FileText, Search, Terminal } from 'lucide-react';
@ -112,6 +113,7 @@ export const ToolApprovalSheet: React.FC = () => {
const teamSummary = teams.find((t) => t.teamName === current.teamName);
const teamColor = teamSummary?.color ? getTeamColorSet(teamSummary.color) : null;
const { isLight } = useTheme();
return (
<div
@ -140,7 +142,7 @@ export const ToolApprovalSheet: React.FC = () => {
<span
className="rounded-full px-2 py-0.5 text-[10px] font-medium"
style={{
backgroundColor: teamColor.badge,
backgroundColor: getThemedBadge(teamColor, isLight),
color: teamColor.text,
border: `1px solid ${teamColor.border}`,
}}

View file

@ -15,7 +15,7 @@ export const UnreadCommentsBadge = ({
<span
className={`inline-flex items-center gap-0.5 rounded-full px-1.5 py-0 text-[10px] font-medium ${
unreadCount > 0
? 'bg-blue-500/20 text-blue-400'
? 'bg-blue-500/20 text-blue-600 dark:text-blue-400'
: 'bg-[var(--color-surface-raised)] text-[var(--color-text-muted)]'
}`}
title={unreadCount > 0 ? `${unreadCount} unread` : 'All read'}

View file

@ -1,5 +1,6 @@
import { CARD_BG, CARD_BORDER_STYLE, CARD_ICON_MUTED } from '@renderer/constants/cssVariables';
import { getTeamColorSet } from '@renderer/constants/teamColors';
import { getTeamColorSet, getThemedBadge } from '@renderer/constants/teamColors';
import { useTheme } from '@renderer/hooks/useTheme';
import { formatAgentRole } from '@renderer/utils/formatAgentRole';
import { buildMemberColorMap } from '@renderer/utils/memberHelpers';
import { Loader2 } from 'lucide-react';
@ -19,6 +20,7 @@ export const ActiveTasksBlock = ({
onMemberClick,
onTaskClick,
}: ActiveTasksBlockProps): React.JSX.Element | null => {
const { isLight } = useTheme();
const colorMap = buildMemberColorMap(members);
const taskMap = new Map(tasks.map((t) => [t.id, t]));
const working = members.filter((m) => m.currentTaskId != null);
@ -61,7 +63,7 @@ export const ActiveTasksBlock = ({
type="button"
className="rounded px-1.5 py-0.5 text-[10px] font-medium tracking-wide transition-opacity hover:opacity-90 focus:outline-none focus:ring-1 focus:ring-[var(--color-border)]"
style={{
backgroundColor: colors.badge,
backgroundColor: getThemedBadge(colors, isLight),
color: colors.text,
border: `1px solid ${colors.border}40`,
}}
@ -73,7 +75,7 @@ export const ActiveTasksBlock = ({
<span
className="rounded px-1.5 py-0.5 text-[10px] font-medium tracking-wide"
style={{
backgroundColor: colors.badge,
backgroundColor: getThemedBadge(colors, isLight),
color: colors.text,
border: `1px solid ${colors.border}40`,
}}

View file

@ -142,6 +142,33 @@ function getSystemMessageLabel(text: string): string | null {
return null;
}
/** Labels to highlight in task assignment / review messages (bold in markdown). */
const TASK_MESSAGE_LABELS = [
'New task assigned to you:',
'Description:',
'Task approved',
'Task needs fixes',
'Review changes requested',
'Changes requested:',
'Comments:',
'Reviewer:',
'Related:',
'Blocked by:',
'Blocks:',
];
/** Make known structural labels bold in system/task messages. */
function highlightSystemLabels(text: string, isSystem: boolean): string {
if (!isSystem) return text;
let result = text;
for (const label of TASK_MESSAGE_LABELS) {
// Escape any regex-special chars in the label, match at line start or after newline
const escaped = label.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
result = result.replace(new RegExp(`(^|\\n)(${escaped})`, 'g'), '$1**$2**');
}
return result;
}
/** Detect authentication/authorization errors that may be resolved by restarting. */
const AUTH_ERROR_PATTERNS = [
/OAuth token has expired/i,
@ -193,7 +220,7 @@ function linkifyTaskIds(text: string, onClick: (taskId: string) => void): React.
<TaskTooltip key={i} taskId={taskId}>
<button
type="button"
className="cursor-pointer font-medium text-blue-400 hover:underline"
className="cursor-pointer font-medium text-blue-600 hover:underline dark:text-blue-400"
onClick={(e) => {
e.stopPropagation();
onClick(taskId);
@ -262,11 +289,12 @@ export const ActivityItem = ({
// Linkify task IDs (always, for TaskTooltip) + @mentions for display
const displayText = useMemo(() => {
if (!strippedText) return null;
let result = linkifyTaskIdsInMarkdown(strippedText);
let result = highlightSystemLabels(strippedText, !!systemLabel);
result = linkifyTaskIdsInMarkdown(result);
if (memberColorMap && memberColorMap.size > 0)
result = linkifyMentionsInMarkdown(result, memberColorMap);
return result;
}, [strippedText, memberColorMap]);
}, [strippedText, memberColorMap, systemLabel]);
const rawSummary =
message.summary || (structured ? getStructuredMessageSummary(structured) : '') || '';

View file

@ -373,9 +373,11 @@ export const ActivityTimeline = ({
className="flex items-center gap-3"
style={{ paddingTop: 90, paddingBottom: 90 }}
>
<div className="h-px flex-1 bg-blue-400/30" />
<span className="whitespace-nowrap text-[11px] text-blue-400">New session</span>
<div className="h-px flex-1 bg-blue-400/30" />
<div className="h-px flex-1 bg-blue-600/30 dark:bg-blue-400/30" />
<span className="whitespace-nowrap text-[11px] font-medium text-blue-600 dark:text-blue-400">
New session
</span>
<div className="h-px flex-1 bg-blue-600/30 dark:bg-blue-400/30" />
</div>
);
}
@ -464,7 +466,7 @@ export const ActivityTimeline = ({
<span className="text-[11px] tabular-nums text-[var(--color-text-muted)]">
+{hiddenCount} older
</span>
<span className="h-3 w-px bg-blue-400/30" />
<span className="h-3 w-px bg-blue-600/30 dark:bg-blue-400/30" />
<button
onClick={handleShowMore}
className="rounded-full px-2.5 py-0.5 text-[11px] font-medium text-[var(--color-text-secondary)] transition-all hover:bg-[rgba(255,255,255,0.08)] hover:text-[var(--color-text)]"
@ -473,7 +475,7 @@ export const ActivityTimeline = ({
</button>
{hiddenCount > MESSAGES_PAGE_SIZE && (
<>
<span className="h-3 w-px bg-blue-400/30" />
<span className="h-3 w-px bg-blue-600/30 dark:bg-blue-400/30" />
<button
onClick={handleShowAll}
className="rounded-full px-2.5 py-0.5 text-[11px] text-[var(--color-text-muted)] transition-all hover:bg-[rgba(255,255,255,0.08)] hover:text-[var(--color-text-secondary)]"

View file

@ -2,6 +2,7 @@ import { useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } fr
import { ChevronDown, ChevronRight, ChevronUp } from 'lucide-react';
import { CopyButton } from '@renderer/components/common/CopyButton';
import { MarkdownViewer } from '@renderer/components/chat/viewers/MarkdownViewer';
import { MemberBadge } from '@renderer/components/team/MemberBadge';
import { Tooltip, TooltipContent, TooltipTrigger } from '@renderer/components/ui/tooltip';
@ -329,7 +330,7 @@ const LeadThoughtItem = ({
/>
</div>
)}
<div className="flex text-[11px]">
<div className="group/thought relative flex text-[11px]">
<div className="min-w-0 flex-1 [&_>div>div]:p-0" style={{ color: CARD_TEXT_LIGHT }}>
<span
onClickCapture={
@ -351,6 +352,9 @@ const LeadThoughtItem = ({
<MarkdownViewer content={displayContent} maxHeight="max-h-none" bare />
</span>
</div>
<div className="absolute right-1 top-0.5 opacity-0 transition-opacity group-hover/thought:opacity-100">
<CopyButton text={thought.text} inline />
</div>
</div>
{thought.toolSummary && (
<Tooltip>

View file

@ -1,5 +1,6 @@
import { CARD_BG, CARD_BORDER_STYLE, CARD_ICON_MUTED } from '@renderer/constants/cssVariables';
import { getTeamColorSet } from '@renderer/constants/teamColors';
import { getTeamColorSet, getThemedBadge } from '@renderer/constants/teamColors';
import { useTheme } from '@renderer/hooks/useTheme';
import { formatAgentRole } from '@renderer/utils/formatAgentRole';
import { buildMemberColorMap } from '@renderer/utils/memberHelpers';
import { formatDistanceToNowStrict } from 'date-fns';
@ -18,6 +19,7 @@ export const PendingRepliesBlock = ({
pendingRepliesByMember,
onMemberClick,
}: PendingRepliesBlockProps): React.JSX.Element | null => {
const { isLight } = useTheme();
const colorMap = buildMemberColorMap(members);
const pending = Object.entries(pendingRepliesByMember)
.map(([name, sentAtMs]) => ({
@ -62,7 +64,7 @@ export const PendingRepliesBlock = ({
type="button"
className="rounded px-1.5 py-0.5 text-[10px] font-medium tracking-wide transition-opacity hover:opacity-90 focus:outline-none focus:ring-1 focus:ring-[var(--color-border)]"
style={{
backgroundColor: colors.badge,
backgroundColor: getThemedBadge(colors, isLight),
color: colors.text,
border: `1px solid ${colors.border}40`,
}}
@ -75,7 +77,7 @@ export const PendingRepliesBlock = ({
<span
className="rounded px-1.5 py-0.5 text-[10px] font-medium tracking-wide"
style={{
backgroundColor: colors.badge,
backgroundColor: getThemedBadge(colors, isLight),
color: colors.text,
border: `1px solid ${colors.border}40`,
}}

View file

@ -29,15 +29,15 @@ export const ReplyQuoteBlock = ({
return (
<div className="space-y-2">
{/* Quote block — styled like SendMessageDialog */}
<div className="relative overflow-hidden rounded-md border border-blue-500/20 bg-blue-950/20 py-2 pl-3 pr-2">
<div className="relative overflow-hidden rounded-md border border-blue-400/20 bg-blue-100/40 py-2 pl-3 pr-2 dark:border-blue-500/20 dark:bg-blue-950/20">
{/* Decorative quotation mark */}
<span className="pointer-events-none absolute -right-1 top-1/2 -translate-y-1/2 select-none font-serif text-[48px] leading-none text-blue-400/[0.08]">
<span className="pointer-events-none absolute -right-1 top-1/2 -translate-y-1/2 select-none font-serif text-[48px] leading-none text-blue-600/[0.08] dark:text-blue-400/[0.08]">
&ldquo;
</span>
{/* "Replying to" + MemberBadge */}
<div className="mb-1 flex items-center gap-1.5">
<span className="text-[10px] text-blue-300/60">Replying to</span>
<span className="text-[10px] text-blue-600/60 dark:text-blue-300/60">Replying to</span>
<MemberBadge name={reply.agentName} color={memberColor} size="sm" />
</div>
@ -50,7 +50,7 @@ export const ReplyQuoteBlock = ({
{isLong ? (
<button
type="button"
className="mt-0.5 text-[10px] text-blue-400/60 hover:text-blue-300"
className="mt-0.5 text-[10px] text-blue-600/60 hover:text-blue-700 dark:text-blue-400/60 dark:hover:text-blue-300"
onClick={() => setExpanded((v) => !v)}
>
{expanded ? 'less' : 'more'}

View file

@ -21,10 +21,11 @@ import {
import { Input } from '@renderer/components/ui/input';
import { Label } from '@renderer/components/ui/label';
import { MentionableTextarea } from '@renderer/components/ui/MentionableTextarea';
import { getTeamColorSet } from '@renderer/constants/teamColors';
import { getTeamColorSet, getThemedBadge } from '@renderer/constants/teamColors';
import { useChipDraftPersistence } from '@renderer/hooks/useChipDraftPersistence';
import { useDraftPersistence } from '@renderer/hooks/useDraftPersistence';
import { useFileListCacheWarmer } from '@renderer/hooks/useFileListCacheWarmer';
import { useTheme } from '@renderer/hooks/useTheme';
import { cn } from '@renderer/lib/utils';
import { normalizePath } from '@renderer/utils/pathNormalize';
import { getMemberColor } from '@shared/constants/memberColors';
@ -203,6 +204,7 @@ export const CreateTeamDialog = ({
onOpenTeam,
}: CreateTeamDialogProps): React.JSX.Element => {
const isDev = process.env.NODE_ENV !== 'production';
const { isLight } = useTheme();
const [teamName, setTeamName] = useState('');
const descriptionDraft = useDraftPersistence({ key: 'createTeam:description' });
@ -874,7 +876,7 @@ export const CreateTeamDialog = ({
isSelected ? 'scale-110' : 'opacity-70 hover:opacity-100'
)}
style={{
backgroundColor: colorSet.badge,
backgroundColor: getThemedBadge(colorSet, isLight),
borderColor: isSelected ? colorSet.border : 'transparent',
}}
title={colorName}

View file

@ -16,9 +16,10 @@ import {
DialogHeader,
DialogTitle,
} from '@renderer/components/ui/dialog';
import { getTeamColorSet } from '@renderer/constants/teamColors';
import { getTeamColorSet, getThemedBadge } from '@renderer/constants/teamColors';
import { CUSTOM_ROLE, PRESET_ROLES } from '@renderer/constants/teamRoles';
import { useFileListCacheWarmer } from '@renderer/hooks/useFileListCacheWarmer';
import { useTheme } from '@renderer/hooks/useTheme';
import { cn } from '@renderer/lib/utils';
import { Loader2 } from 'lucide-react';
@ -73,6 +74,7 @@ export const EditTeamDialog = ({
onClose,
onSaved,
}: EditTeamDialogProps): React.JSX.Element => {
const { isLight } = useTheme();
const [name, setName] = useState(currentName);
const [description, setDescription] = useState(currentDescription);
const [color, setColor] = useState(currentColor);
@ -191,7 +193,7 @@ export const EditTeamDialog = ({
isSelected ? 'scale-110' : 'opacity-70 hover:opacity-100'
)}
style={{
backgroundColor: colorSet.badge,
backgroundColor: getThemedBadge(colorSet, isLight),
borderColor: isSelected ? colorSet.border : 'transparent',
}}
title={colorName}

View file

@ -202,7 +202,7 @@ export const TaskCommentsSection = ({
Approved
</span>
) : comment.type === 'review_request' ? (
<span className="inline-flex items-center gap-0.5 rounded-full bg-blue-500/15 px-1.5 py-0.5 text-[10px] font-medium text-blue-400">
<span className="inline-flex items-center gap-0.5 rounded-full bg-blue-500/15 px-1.5 py-0.5 text-[10px] font-medium text-blue-600 dark:text-blue-400">
<Eye size={10} />
Review requested
</span>

View file

@ -536,25 +536,22 @@ export const TaskDetailDialog = ({
</div>
</div>
) : currentTask.description ? (
<div
role="button"
tabIndex={0}
className="group cursor-pointer"
onClick={startEditDescription}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
startEditDescription();
}
}}
>
<div className="group relative">
<ExpandableContent collapsedHeight={200}>
<MarkdownViewer content={currentTask.description} maxHeight="max-h-none" bare />
</ExpandableContent>
<Pencil
size={12}
className="mt-1 text-[var(--color-text-muted)] opacity-0 transition-opacity group-hover:opacity-100"
/>
<Tooltip>
<TooltipTrigger asChild>
<button
type="button"
className="absolute right-0 top-0 rounded p-1 text-[var(--color-text-muted)] opacity-0 transition-opacity hover:bg-[var(--color-surface-raised)] hover:text-[var(--color-text)] group-hover:opacity-100"
onClick={startEditDescription}
>
<Pencil size={12} />
</button>
</TooltipTrigger>
<TooltipContent side="top">Edit description</TooltipContent>
</Tooltip>
</div>
) : (
<button

View file

@ -725,7 +725,7 @@ export const ProjectEditorOverlay = ({
{/* External change banner */}
{activeTabId && externalChanges[activeTabId] && (
<div className="flex shrink-0 items-center gap-2 border-b border-blue-500/30 bg-blue-500/10 px-3 py-1.5 text-xs text-blue-300">
<div className="flex shrink-0 items-center gap-2 border-b border-blue-400/30 bg-blue-100/50 px-3 py-1.5 text-xs text-blue-700 dark:border-blue-500/30 dark:bg-blue-500/10 dark:text-blue-300">
<RefreshCw className="size-3.5 shrink-0" />
<span>
{externalChanges[activeTabId] === 'delete'

View file

@ -268,9 +268,7 @@ export const KanbanTaskCard = ({
</span>
<div className="mb-2 pt-2">
<div className="flex items-center gap-1">
{task.owner ? (
<MemberBadge name={task.owner} color={colorMap.get(task.owner)} />
) : null}
{task.owner ? <MemberBadge name={task.owner} color={colorMap.get(task.owner)} /> : null}
{!compact && <TruncatedTitle text={task.subject} className="min-w-0" />}
</div>
{task.needsClarification ? (
@ -278,7 +276,7 @@ export const KanbanTaskCard = ({
className={`mt-1 inline-flex items-center gap-1 rounded-full px-1.5 py-0.5 text-[10px] font-medium ${
task.needsClarification === 'user'
? 'bg-red-500/15 text-red-400'
: 'bg-blue-500/15 text-blue-400'
: 'bg-blue-500/15 text-blue-600 dark:text-blue-400'
}`}
>
<HelpCircle size={10} />
@ -307,7 +305,7 @@ export const KanbanTaskCard = ({
{hasBlocks ? (
<div className="mb-2 flex flex-wrap items-center gap-1">
<span className="inline-flex items-center gap-0.5 text-[10px] text-blue-400">
<span className="inline-flex items-center gap-0.5 text-[10px] text-blue-600 dark:text-blue-400">
<ArrowRightFromLine size={10} />
Blocks
</span>

View file

@ -1,6 +1,7 @@
import { Badge } from '@renderer/components/ui/badge';
import { Tooltip, TooltipContent, TooltipTrigger } from '@renderer/components/ui/tooltip';
import { getTeamColorSet } from '@renderer/constants/teamColors';
import { getTeamColorSet, getThemedBadge } from '@renderer/constants/teamColors';
import { useTheme } from '@renderer/hooks/useTheme';
import { formatAgentRole } from '@renderer/utils/formatAgentRole';
import { agentAvatarUrl, getMemberDotClass, getPresenceLabel } from '@renderer/utils/memberHelpers';
import { GitBranch, Loader2, MessageSquare, Plus } from 'lucide-react';
@ -47,6 +48,7 @@ export const MemberCard = ({
const dotClass = getMemberDotClass(member, isTeamAlive, isTeamProvisioning, leadActivity);
const presenceLabel = getPresenceLabel(member, isTeamAlive, isTeamProvisioning, leadActivity);
const colors = getTeamColorSet(memberColor);
const { isLight } = useTheme();
const pending = taskCounts?.pending ?? 0;
const inProgress = taskCounts?.inProgress ?? 0;
const completed = taskCounts?.completed ?? 0;
@ -59,7 +61,7 @@ export const MemberCard = ({
className="group relative cursor-pointer rounded px-2 py-1.5"
style={{
borderLeft: `3px solid ${colors.border}`,
background: `linear-gradient(to right, ${colors.badge}, transparent)`,
background: `linear-gradient(to right, ${getThemedBadge(colors, isLight)}, transparent)`,
}}
title={member.currentTaskId ? `Current task: ${member.currentTaskId}` : undefined}
role="button"

View file

@ -14,7 +14,9 @@ export const TaskRow = ({ task }: TaskRowProps): React.JSX.Element => {
<tr className="border-t border-[var(--color-border)]">
<td className="px-3 py-2 text-xs text-[var(--color-text-muted)]">{task.id}</td>
<td className="px-3 py-2 text-sm text-[var(--color-text)]">{task.subject}</td>
<td className="px-3 py-2 text-xs text-[var(--color-text-muted)]">{task.owner ?? 'Unassigned'}</td>
<td className="px-3 py-2 text-xs text-[var(--color-text-muted)]">
{task.owner ?? 'Unassigned'}
</td>
<td className="px-3 py-2 text-xs text-[var(--color-text-muted)]">
{task.kanbanColumn && task.kanbanColumn in KANBAN_COLUMN_DISPLAY
? KANBAN_COLUMN_DISPLAY[task.kanbanColumn].label
@ -29,7 +31,9 @@ export const TaskRow = ({ task }: TaskRowProps): React.JSX.Element => {
</td>
<td className="px-3 py-2 text-xs">
{blocksIds.length > 0 ? (
<span className="text-blue-400">{blocksIds.map((id) => `#${id}`).join(', ')}</span>
<span className="text-blue-600 dark:text-blue-400">
{blocksIds.map((id) => `#${id}`).join(', ')}
</span>
) : (
<span className="text-[var(--color-text-muted)]">{'\u2014'}</span>
)}

View file

@ -1,6 +1,7 @@
import * as React from 'react';
import { getTeamColorSet } from '@renderer/constants/teamColors';
import { getTeamColorSet, getThemedBadge } from '@renderer/constants/teamColors';
import { useTheme } from '@renderer/hooks/useTheme';
import { cn } from '@renderer/lib/utils';
import { formatAgentRole } from '@renderer/utils/formatAgentRole';
import { agentAvatarUrl, buildMemberColorMap } from '@renderer/utils/memberHelpers';
@ -39,6 +40,7 @@ export const MemberSelect = ({
const [open, setOpen] = React.useState(false);
const [search, setSearch] = React.useState('');
const listboxId = React.useId();
const { isLight } = useTheme();
const colorMap = React.useMemo(() => buildMemberColorMap(members), [members]);
const selectedMember = React.useMemo(
@ -66,7 +68,7 @@ export const MemberSelect = ({
<span
className={`rounded px-1.5 py-0.5 ${textSize} font-medium tracking-wide`}
style={{
backgroundColor: colors.badge,
backgroundColor: getThemedBadge(colors, isLight),
color: colors.text,
border: `1px solid ${colors.border}40`,
}}

View file

@ -1,8 +1,9 @@
import * as React from 'react';
import { getTeamColorSet } from '@renderer/constants/teamColors';
import { getTeamColorSet, getThemedBadge } from '@renderer/constants/teamColors';
import { useFileSuggestions } from '@renderer/hooks/useFileSuggestions';
import { useMentionDetection } from '@renderer/hooks/useMentionDetection';
import { useTheme } from '@renderer/hooks/useTheme';
import { cn } from '@renderer/lib/utils';
import { chipToken } from '@renderer/types/inlineChip';
import {
@ -232,6 +233,7 @@ export const MentionableTextarea = React.forwardRef<HTMLTextAreaElement, Mention
const internalRef = React.useRef<HTMLTextAreaElement | null>(null);
const backdropRef = React.useRef<HTMLDivElement>(null);
const [scrollTop, setScrollTop] = React.useState(0);
const { isLight } = useTheme();
// --- File search activation ---
const enableFiles = !!projectPath;
@ -599,7 +601,7 @@ export const MentionableTextarea = React.forwardRef<HTMLTextAreaElement, Mention
const rotatingTips = React.useMemo(
() => [
'Tip: Use @ to mention team members or search files',
'Tip: Mention "create a task" to add it to the board',
'Tip: Mention "create a task" to add it to the kanban',
"Tip: Don't overload the team lead with tasks — ask them to delegate to teammates",
],
[]
@ -653,7 +655,7 @@ export const MentionableTextarea = React.forwardRef<HTMLTextAreaElement, Mention
const colorSet = seg.suggestion.color
? getTeamColorSet(seg.suggestion.color)
: null;
const bg = colorSet?.badge ?? DEFAULT_MENTION_BG;
const bg = colorSet ? getThemedBadge(colorSet, isLight) : DEFAULT_MENTION_BG;
const fg = colorSet?.text ?? DEFAULT_MENTION_TEXT;
return (
<span

View file

@ -10,22 +10,86 @@ export interface TeamColorSet {
border: string;
/** Badge background (semi-transparent) */
badge: string;
/** Text color for labels */
/** Badge background for light theme (more visible on white) */
badgeLight?: string;
/** Text color for labels (dark theme) */
text: string;
/** Text color for labels on light backgrounds (higher contrast) */
textLight?: string;
}
const TEAMMATE_COLORS: Record<string, TeamColorSet> = {
blue: { border: '#3b82f6', badge: 'rgba(59, 130, 246, 0.15)', text: '#60a5fa' },
green: { border: '#22c55e', badge: 'rgba(34, 197, 94, 0.15)', text: '#4ade80' },
red: { border: '#ef4444', badge: 'rgba(239, 68, 68, 0.15)', text: '#f87171' },
yellow: { border: '#eab308', badge: 'rgba(234, 179, 8, 0.15)', text: '#facc15' },
purple: { border: '#a855f7', badge: 'rgba(168, 85, 247, 0.15)', text: '#c084fc' },
cyan: { border: '#06b6d4', badge: 'rgba(6, 182, 212, 0.15)', text: '#22d3ee' },
orange: { border: '#f97316', badge: 'rgba(249, 115, 22, 0.15)', text: '#fb923c' },
pink: { border: '#ec4899', badge: 'rgba(236, 72, 153, 0.15)', text: '#f472b6' },
magenta: { border: '#d946ef', badge: 'rgba(217, 70, 239, 0.15)', text: '#e879f9' },
blue: {
border: '#3b82f6',
badge: 'rgba(59, 130, 246, 0.15)',
badgeLight: 'rgba(59, 130, 246, 0.12)',
text: '#60a5fa',
textLight: '#2563eb',
},
green: {
border: '#22c55e',
badge: 'rgba(34, 197, 94, 0.15)',
badgeLight: 'rgba(34, 197, 94, 0.12)',
text: '#4ade80',
textLight: '#16a34a',
},
red: {
border: '#ef4444',
badge: 'rgba(239, 68, 68, 0.15)',
badgeLight: 'rgba(239, 68, 68, 0.12)',
text: '#f87171',
textLight: '#dc2626',
},
yellow: {
border: '#eab308',
badge: 'rgba(234, 179, 8, 0.15)',
badgeLight: 'rgba(161, 98, 7, 0.12)',
text: '#facc15',
textLight: '#a16207',
},
purple: {
border: '#a855f7',
badge: 'rgba(168, 85, 247, 0.15)',
badgeLight: 'rgba(168, 85, 247, 0.12)',
text: '#c084fc',
textLight: '#7c3aed',
},
cyan: {
border: '#06b6d4',
badge: 'rgba(6, 182, 212, 0.15)',
badgeLight: 'rgba(6, 182, 212, 0.12)',
text: '#22d3ee',
textLight: '#0891b2',
},
orange: {
border: '#f97316',
badge: 'rgba(249, 115, 22, 0.15)',
badgeLight: 'rgba(249, 115, 22, 0.12)',
text: '#fb923c',
textLight: '#c2410c',
},
pink: {
border: '#ec4899',
badge: 'rgba(236, 72, 153, 0.15)',
badgeLight: 'rgba(236, 72, 153, 0.12)',
text: '#f472b6',
textLight: '#db2777',
},
magenta: {
border: '#d946ef',
badge: 'rgba(217, 70, 239, 0.15)',
badgeLight: 'rgba(217, 70, 239, 0.12)',
text: '#e879f9',
textLight: '#a21caf',
},
/** Reserved for the human user — never assigned to team members. */
user: { border: '#f5f5f4', badge: 'rgba(245, 245, 244, 0.12)', text: '#d6d3d1' },
user: {
border: '#f5f5f4',
badge: 'rgba(245, 245, 244, 0.12)',
badgeLight: 'rgba(0, 0, 0, 0.08)',
text: '#d6d3d1',
textLight: '#57534e',
},
};
const DEFAULT_COLOR: TeamColorSet = TEAMMATE_COLORS.blue;
@ -76,3 +140,11 @@ export function getTeamColorSet(colorName: string): TeamColorSet {
return DEFAULT_COLOR;
}
/**
* Get the appropriate badge background for the current theme.
* Uses badgeLight in light theme when available, falls back to badge.
*/
export function getThemedBadge(colorSet: TeamColorSet, isLight: boolean): string {
return isLight && colorSet.badgeLight ? colorSet.badgeLight : colorSet.badge;
}

View file

@ -15,6 +15,7 @@
--color-text: #f1f5f9;
--color-text-secondary: #94a3b8;
--color-text-muted: #64748b;
--color-accent: #818cf8; /* Accent — indigo-400, visible on dark surfaces */
/* Scrollbar colors */
--scrollbar-thumb: rgba(148, 163, 184, 0.15);
@ -187,6 +188,11 @@
--system-activity-border: rgba(59, 130, 246, 0.12);
--system-activity-accent: rgba(96, 165, 250, 0.5);
/* Info style — banners, status indicators */
--info-bg: rgba(59, 130, 246, 0.08);
--info-border: rgba(59, 130, 246, 0.25);
--info-text: #60a5fa;
/* Assessment severity colors (badges, health indicators) */
--assess-good: #4ade80;
--assess-warning: #fbbf24;
@ -236,6 +242,7 @@
--color-text: #1c1b19; /* Warm near-black text */
--color-text-secondary: #4d4b46; /* Warm secondary text */
--color-text-muted: #6d6b65; /* Warm muted text */
--color-accent: #4f46e5; /* Accent — indigo-600, visible on light surfaces */
/* Assessment severity colors - darker for light backgrounds */
--assess-good: #16a34a;
@ -410,9 +417,14 @@
--card-separator: #d5d3cf;
/* System activity messages */
--system-activity-bg: rgba(59, 130, 246, 0.06);
--system-activity-border: rgba(59, 130, 246, 0.15);
--system-activity-accent: rgba(37, 99, 235, 0.5);
--system-activity-bg: rgba(59, 130, 246, 0.08);
--system-activity-border: rgba(59, 130, 246, 0.25);
--system-activity-accent: rgba(37, 99, 235, 0.7);
/* Info style — banners, status indicators */
--info-bg: rgba(59, 130, 246, 0.1);
--info-border: rgba(37, 99, 235, 0.3);
--info-text: #2563eb;
/* Sticky Context button - transparent glass */
--context-btn-bg: rgba(0, 0, 0, 0.06);

View file

@ -42,10 +42,7 @@
}
:root.light #splash-text { color: #52525b; }
:root.light #splash-noise { opacity: 0.02; }
:root.light .splash-logo-bg { fill: #e4e4e7; }
:root.light .splash-node-fill { fill: #52525b; }
:root.light .splash-core-fill { fill: #fafafa; }
:root.light .splash-edge { stroke: #71717a; }
:root.light #splash-logo { filter: drop-shadow(0 2px 8px rgba(0,0,0,0.15)); }
</style>
<script>
// Flash prevention: Apply cached theme before React loads

View file

@ -13,8 +13,8 @@ export function agentAvatarUrl(name: string, size = 64): string {
export const STATUS_DOT_COLORS: Record<MemberStatus, string> = {
active: 'bg-emerald-400',
idle: 'bg-emerald-400/50',
terminated: 'bg-zinc-500',
idle: 'bg-zinc-400',
terminated: 'bg-red-400',
unknown: 'bg-zinc-600',
};

View file

@ -15,8 +15,16 @@ export interface ProjectColorSet {
text: string;
}
export function projectColor(name: string): ProjectColorSet {
export function projectColor(name: string, isLight = false): ProjectColorSet {
const hue = hashStringToHue(name);
if (isLight) {
return {
border: `hsla(${hue}, 70%, 40%, 0.7)`,
glow: `hsla(${hue}, 70%, 40%, 0.08)`,
icon: `hsla(${hue}, 70%, 40%, 0.85)`,
text: `hsla(${hue}, 50%, 35%, 0.9)`,
};
}
return {
border: `hsla(${hue}, 70%, 55%, 0.5)`,
glow: `hsla(${hue}, 70%, 55%, 0.06)`,
@ -26,8 +34,15 @@ export function projectColor(name: string): ProjectColorSet {
}
/** Generate a TeamColorSet from any name (deterministic hue). */
export function nameColorSet(name: string): TeamColorSet {
export function nameColorSet(name: string, isLight = false): TeamColorSet {
const hue = hashStringToHue(name);
if (isLight) {
return {
border: `hsl(${hue}, 70%, 40%)`,
badge: `hsla(${hue}, 70%, 40%, 0.1)`,
text: `hsla(${hue}, 50%, 35%, 0.9)`,
};
}
return {
border: `hsl(${hue}, 70%, 55%)`,
badge: `hsla(${hue}, 70%, 55%, 0.08)`,

View file

@ -23,6 +23,8 @@ module.exports = {
subtle: 'var(--color-border-subtle)',
emphasis: 'var(--color-border-emphasis)',
},
// Theme-aware accent color
accent: 'var(--color-accent)',
// Theme-aware text colors (use CSS variables)
text: {
DEFAULT: 'var(--color-text)',
@ -36,6 +38,12 @@ module.exports = {
warning: '#f59e0b', // amber-500
info: '#3b82f6', // blue-500
},
// Theme-aware info color (use for blue informational elements)
info: {
DEFAULT: 'var(--info-text)',
bg: 'var(--info-bg)',
border: 'var(--info-border)',
},
// Theme-aware colors using CSS variables
// These aliases enable all existing components to automatically support light/dark mode
'claude-dark': {