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:
parent
919d40b7bc
commit
e9b369e667
41 changed files with 342 additions and 143 deletions
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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`,
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -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`,
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
)}
|
||||
>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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`,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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}`,
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -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'}
|
||||
|
|
|
|||
|
|
@ -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`,
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -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) : '') || '';
|
||||
|
|
|
|||
|
|
@ -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)]"
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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`,
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -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]">
|
||||
“
|
||||
</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'}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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'
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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`,
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -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)`,
|
||||
|
|
|
|||
|
|
@ -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': {
|
||||
|
|
|
|||
Loading…
Reference in a new issue