feat: enhance settings UI with optional icons and add kanban focus animation

- Updated SettingRow and SettingsSectionHeader components to support optional icons, improving visual clarity and user experience.
- Introduced a new CSS animation for kanban card focus, enhancing the visual feedback when tasks are interacted with.
- Adjusted NotificationsSection to utilize the new icon feature in headers and rows, providing a more engaging settings interface.
- Refined team detail view to incorporate the kanban focus animation for better task visibility during interactions.
This commit is contained in:
iliya 2026-03-15 13:12:09 +02:00
parent 3381dad0da
commit d4688825fd
6 changed files with 124 additions and 28 deletions

View file

@ -1,17 +1,19 @@
/**
* SettingRow - Setting row component for consistent layout.
* Linear-style clean row without icons.
* Linear-style clean row with optional icon.
*/
interface SettingRowProps {
readonly label: string;
readonly description?: string;
readonly icon?: React.ReactNode;
readonly children: React.ReactNode;
}
export const SettingRow = ({
label,
description,
icon,
children,
}: SettingRowProps): React.JSX.Element => {
return (
@ -19,15 +21,22 @@ export const SettingRow = ({
className="flex items-center justify-between border-b py-3"
style={{ borderColor: 'var(--color-border-subtle)' }}
>
<div>
<div className="text-sm font-medium" style={{ color: 'var(--color-text)' }}>
{label}
</div>
{description && (
<div className="text-xs" style={{ color: 'var(--color-text-muted)' }}>
{description}
<div className="flex items-start gap-2.5">
{icon ? (
<div className="mt-0.5 shrink-0" style={{ color: 'var(--color-text-muted)' }}>
{icon}
</div>
)}
) : null}
<div>
<div className="text-sm font-medium" style={{ color: 'var(--color-text)' }}>
{label}
</div>
{description && (
<div className="text-xs" style={{ color: 'var(--color-text-muted)' }}>
{description}
</div>
)}
</div>
</div>
<div className="shrink-0">{children}</div>
</div>

View file

@ -1,18 +1,23 @@
/**
* SettingsSectionHeader - Section header component.
* Linear-style subtle label.
* Linear-style subtle label with optional icon.
*/
interface SettingsSectionHeaderProps {
readonly title: string;
readonly icon?: React.ReactNode;
}
export const SettingsSectionHeader = ({ title }: SettingsSectionHeaderProps): React.JSX.Element => {
export const SettingsSectionHeader = ({
title,
icon,
}: SettingsSectionHeaderProps): React.JSX.Element => {
return (
<h3
className="mb-2 mt-6 text-xs font-medium uppercase tracking-widest first:mt-0"
className="mb-2 mt-6 flex items-center gap-1.5 text-xs font-medium uppercase tracking-widest first:mt-0"
style={{ color: 'var(--color-text-muted)' }}
>
{icon}
{title}
</h3>
);

View file

@ -7,7 +7,21 @@ import {
RepositoryDropdown,
SelectedRepositoryItem,
} from '@renderer/components/common/RepositoryDropdown';
import { ExternalLink } from 'lucide-react';
import {
AlertTriangle,
ArrowRightLeft,
Bell,
BellRing,
Clock,
ExternalLink,
EyeOff,
HelpCircle,
Inbox,
Mail,
MessageSquare,
PartyPopper,
Volume2,
} from 'lucide-react';
import { SettingRow, SettingsSectionHeader, SettingsSelect, SettingsToggle } from '../components';
import { NotificationTriggerSettings } from '../NotificationTriggerSettings';
@ -80,7 +94,10 @@ export const NotificationsSection = ({
return (
<div>
{/* Task Completion Notifications */}
<SettingsSectionHeader title="Task Completion Notifications" />
<SettingsSectionHeader
title="Task Completion Notifications"
icon={<PartyPopper className="size-3.5" />}
/>
<div
className="mb-4 rounded-lg border p-4"
style={{
@ -108,10 +125,11 @@ export const NotificationsSection = ({
</div>
{/* Notification Settings */}
<SettingsSectionHeader title="Notification Settings" />
<SettingsSectionHeader title="Notification Settings" icon={<Bell className="size-3.5" />} />
<SettingRow
label="Enable System Notifications"
description="Show system notifications for errors and events"
icon={<BellRing className="size-4" />}
>
<SettingsToggle
enabled={safeConfig.notifications.enabled}
@ -119,7 +137,11 @@ export const NotificationsSection = ({
disabled={saving}
/>
</SettingRow>
<SettingRow label="Play sound" description="Play a sound when notifications appear">
<SettingRow
label="Play sound"
description="Play a sound when notifications appear"
icon={<Volume2 className="size-4" />}
>
<SettingsToggle
enabled={safeConfig.notifications.soundEnabled}
onChange={(v) => onNotificationToggle('soundEnabled', v)}
@ -129,6 +151,7 @@ export const NotificationsSection = ({
<SettingRow
label="Include subagent errors"
description="Detect and notify about errors in subagent sessions"
icon={<AlertTriangle className="size-4" />}
>
<SettingsToggle
enabled={safeConfig.notifications.includeSubagentErrors}
@ -139,6 +162,7 @@ export const NotificationsSection = ({
<SettingRow
label="Lead inbox notifications"
description="Notify when teammates send messages to the team lead"
icon={<Inbox className="size-4" />}
>
<SettingsToggle
enabled={safeConfig.notifications.notifyOnLeadInbox}
@ -149,6 +173,7 @@ export const NotificationsSection = ({
<SettingRow
label="User inbox notifications"
description="Notify when teammates send messages to you"
icon={<Mail className="size-4" />}
>
<SettingsToggle
enabled={safeConfig.notifications.notifyOnUserInbox}
@ -159,6 +184,7 @@ export const NotificationsSection = ({
<SettingRow
label="Task clarification notifications"
description="Show native OS notifications when a task needs your input"
icon={<HelpCircle className="size-4" />}
>
<SettingsToggle
enabled={safeConfig.notifications.notifyOnClarifications}
@ -169,6 +195,7 @@ export const NotificationsSection = ({
<SettingRow
label="Task comment notifications"
description="Show native OS notifications when agents comment on tasks"
icon={<MessageSquare className="size-4" />}
>
<SettingsToggle
enabled={safeConfig.notifications.notifyOnTaskComments}
@ -183,6 +210,7 @@ export const NotificationsSection = ({
? `Snoozed until ${new Date(safeConfig.notifications.snoozedUntil!).toLocaleTimeString()}`
: 'Temporarily pause notifications'
}
icon={<Clock className="size-4" />}
>
<div className="flex items-center gap-2">
{isSnoozed ? (
@ -208,12 +236,17 @@ export const NotificationsSection = ({
{/* Task Status Change Notifications — grouped section */}
<div className="border-b py-3" style={{ borderColor: 'var(--color-border-subtle)' }}>
<div className="flex items-center justify-between">
<div>
<div className="text-sm font-medium" style={{ color: 'var(--color-text)' }}>
Task status change notifications
<div className="flex items-start gap-2.5">
<div className="mt-0.5 shrink-0" style={{ color: 'var(--color-text-muted)' }}>
<ArrowRightLeft className="size-4" />
</div>
<div className="text-xs" style={{ color: 'var(--color-text-muted)' }}>
Show native OS notifications when a task&apos;s status changes
<div>
<div className="text-sm font-medium" style={{ color: 'var(--color-text)' }}>
Task status change notifications
</div>
<div className="text-xs" style={{ color: 'var(--color-text-muted)' }}>
Show native OS notifications when a task&apos;s status changes
</div>
</div>
</div>
<div className="shrink-0">
@ -282,7 +315,7 @@ export const NotificationsSection = ({
onRemoveTrigger={onRemoveTrigger}
/>
<SettingsSectionHeader title="Ignored Repositories" />
<SettingsSectionHeader title="Ignored Repositories" icon={<EyeOff className="size-3.5" />} />
<p className="mb-3 text-xs" style={{ color: 'var(--color-text-muted)' }}>
Notifications from these repositories will be ignored
</p>

View file

@ -1581,8 +1581,14 @@ export const TeamDetailView = ({ teamName }: TeamDetailViewProps): React.JSX.Ele
const el = document.querySelector(`[data-task-id="${taskId}"]`);
if (el) {
el.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
el.classList.add('ring-2', 'ring-blue-400/50');
setTimeout(() => el.classList.remove('ring-2', 'ring-blue-400/50'), 1500);
el.classList.remove('kanban-card-focus-pulse');
void (el as HTMLElement).offsetWidth;
el.classList.add('kanban-card-focus-pulse');
el.addEventListener(
'animationend',
() => el.classList.remove('kanban-card-focus-pulse'),
{ once: true }
);
}
}}
onTaskClick={(task) => setSelectedTask(task)}
@ -1919,8 +1925,14 @@ export const TeamDetailView = ({ teamName }: TeamDetailViewProps): React.JSX.Ele
const el = document.querySelector(`[data-task-id="${taskId}"]`);
if (el) {
el.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
el.classList.add('ring-2', 'ring-blue-400/50');
setTimeout(() => el.classList.remove('ring-2', 'ring-blue-400/50'), 1500);
el.classList.remove('kanban-card-focus-pulse');
void (el as HTMLElement).offsetWidth;
el.classList.add('kanban-card-focus-pulse');
el.addEventListener(
'animationend',
() => el.classList.remove('kanban-card-focus-pulse'),
{ once: true }
);
}
}}
onOwnerChange={(taskId, owner) => {

View file

@ -89,9 +89,9 @@ const TEAMMATE_COLORS: Record<string, TeamColorSet> = {
/** Reserved for the human user — never assigned to team members. */
user: {
border: '#f5f5f4',
borderLight: '#a8a29e',
borderLight: '#78716c',
badge: 'rgba(245, 245, 244, 0.12)',
badgeLight: 'rgba(120, 113, 108, 0.14)',
badgeLight: 'rgba(87, 83, 78, 0.18)',
text: '#d6d3d1',
textLight: '#44403c',
},

View file

@ -295,6 +295,43 @@
}
}
@keyframes kanban-focus-pulse {
0% {
box-shadow: 0 0 0 0 rgba(96, 165, 250, 0.6);
background-color: rgba(96, 165, 250, 0.15);
}
14% {
box-shadow: 0 0 16px 6px rgba(96, 165, 250, 0.5);
background-color: rgba(96, 165, 250, 0.12);
}
28% {
box-shadow: 0 0 0 0 rgba(96, 165, 250, 0.1);
background-color: rgba(96, 165, 250, 0.02);
}
42% {
box-shadow: 0 0 14px 5px rgba(96, 165, 250, 0.45);
background-color: rgba(96, 165, 250, 0.1);
}
56% {
box-shadow: 0 0 0 0 rgba(96, 165, 250, 0.1);
background-color: rgba(96, 165, 250, 0.02);
}
70% {
box-shadow: 0 0 10px 4px rgba(96, 165, 250, 0.3);
background-color: rgba(96, 165, 250, 0.06);
}
100% {
box-shadow: 0 0 0 0 rgba(96, 165, 250, 0);
background-color: transparent;
}
}
.kanban-card-focus-pulse {
animation: kanban-focus-pulse 2.2s ease-out;
outline: 2px solid rgba(96, 165, 250, 0.6);
outline-offset: -1px;
}
.kanban-grid-resize-handle-n,
.kanban-grid-resize-handle-s {
left: 50%;