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:
parent
3381dad0da
commit
d4688825fd
6 changed files with 124 additions and 28 deletions
|
|
@ -1,17 +1,19 @@
|
||||||
/**
|
/**
|
||||||
* SettingRow - Setting row component for consistent layout.
|
* SettingRow - Setting row component for consistent layout.
|
||||||
* Linear-style clean row without icons.
|
* Linear-style clean row with optional icon.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
interface SettingRowProps {
|
interface SettingRowProps {
|
||||||
readonly label: string;
|
readonly label: string;
|
||||||
readonly description?: string;
|
readonly description?: string;
|
||||||
|
readonly icon?: React.ReactNode;
|
||||||
readonly children: React.ReactNode;
|
readonly children: React.ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const SettingRow = ({
|
export const SettingRow = ({
|
||||||
label,
|
label,
|
||||||
description,
|
description,
|
||||||
|
icon,
|
||||||
children,
|
children,
|
||||||
}: SettingRowProps): React.JSX.Element => {
|
}: SettingRowProps): React.JSX.Element => {
|
||||||
return (
|
return (
|
||||||
|
|
@ -19,15 +21,22 @@ export const SettingRow = ({
|
||||||
className="flex items-center justify-between border-b py-3"
|
className="flex items-center justify-between border-b py-3"
|
||||||
style={{ borderColor: 'var(--color-border-subtle)' }}
|
style={{ borderColor: 'var(--color-border-subtle)' }}
|
||||||
>
|
>
|
||||||
<div>
|
<div className="flex items-start gap-2.5">
|
||||||
<div className="text-sm font-medium" style={{ color: 'var(--color-text)' }}>
|
{icon ? (
|
||||||
{label}
|
<div className="mt-0.5 shrink-0" style={{ color: 'var(--color-text-muted)' }}>
|
||||||
</div>
|
{icon}
|
||||||
{description && (
|
|
||||||
<div className="text-xs" style={{ color: 'var(--color-text-muted)' }}>
|
|
||||||
{description}
|
|
||||||
</div>
|
</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>
|
||||||
<div className="shrink-0">{children}</div>
|
<div className="shrink-0">{children}</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -1,18 +1,23 @@
|
||||||
/**
|
/**
|
||||||
* SettingsSectionHeader - Section header component.
|
* SettingsSectionHeader - Section header component.
|
||||||
* Linear-style subtle label.
|
* Linear-style subtle label with optional icon.
|
||||||
*/
|
*/
|
||||||
|
|
||||||
interface SettingsSectionHeaderProps {
|
interface SettingsSectionHeaderProps {
|
||||||
readonly title: string;
|
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 (
|
return (
|
||||||
<h3
|
<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)' }}
|
style={{ color: 'var(--color-text-muted)' }}
|
||||||
>
|
>
|
||||||
|
{icon}
|
||||||
{title}
|
{title}
|
||||||
</h3>
|
</h3>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,21 @@ import {
|
||||||
RepositoryDropdown,
|
RepositoryDropdown,
|
||||||
SelectedRepositoryItem,
|
SelectedRepositoryItem,
|
||||||
} from '@renderer/components/common/RepositoryDropdown';
|
} 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 { SettingRow, SettingsSectionHeader, SettingsSelect, SettingsToggle } from '../components';
|
||||||
import { NotificationTriggerSettings } from '../NotificationTriggerSettings';
|
import { NotificationTriggerSettings } from '../NotificationTriggerSettings';
|
||||||
|
|
@ -80,7 +94,10 @@ export const NotificationsSection = ({
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{/* Task Completion Notifications */}
|
{/* Task Completion Notifications */}
|
||||||
<SettingsSectionHeader title="Task Completion Notifications" />
|
<SettingsSectionHeader
|
||||||
|
title="Task Completion Notifications"
|
||||||
|
icon={<PartyPopper className="size-3.5" />}
|
||||||
|
/>
|
||||||
<div
|
<div
|
||||||
className="mb-4 rounded-lg border p-4"
|
className="mb-4 rounded-lg border p-4"
|
||||||
style={{
|
style={{
|
||||||
|
|
@ -108,10 +125,11 @@ export const NotificationsSection = ({
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Notification Settings */}
|
{/* Notification Settings */}
|
||||||
<SettingsSectionHeader title="Notification Settings" />
|
<SettingsSectionHeader title="Notification Settings" icon={<Bell className="size-3.5" />} />
|
||||||
<SettingRow
|
<SettingRow
|
||||||
label="Enable System Notifications"
|
label="Enable System Notifications"
|
||||||
description="Show system notifications for errors and events"
|
description="Show system notifications for errors and events"
|
||||||
|
icon={<BellRing className="size-4" />}
|
||||||
>
|
>
|
||||||
<SettingsToggle
|
<SettingsToggle
|
||||||
enabled={safeConfig.notifications.enabled}
|
enabled={safeConfig.notifications.enabled}
|
||||||
|
|
@ -119,7 +137,11 @@ export const NotificationsSection = ({
|
||||||
disabled={saving}
|
disabled={saving}
|
||||||
/>
|
/>
|
||||||
</SettingRow>
|
</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
|
<SettingsToggle
|
||||||
enabled={safeConfig.notifications.soundEnabled}
|
enabled={safeConfig.notifications.soundEnabled}
|
||||||
onChange={(v) => onNotificationToggle('soundEnabled', v)}
|
onChange={(v) => onNotificationToggle('soundEnabled', v)}
|
||||||
|
|
@ -129,6 +151,7 @@ export const NotificationsSection = ({
|
||||||
<SettingRow
|
<SettingRow
|
||||||
label="Include subagent errors"
|
label="Include subagent errors"
|
||||||
description="Detect and notify about errors in subagent sessions"
|
description="Detect and notify about errors in subagent sessions"
|
||||||
|
icon={<AlertTriangle className="size-4" />}
|
||||||
>
|
>
|
||||||
<SettingsToggle
|
<SettingsToggle
|
||||||
enabled={safeConfig.notifications.includeSubagentErrors}
|
enabled={safeConfig.notifications.includeSubagentErrors}
|
||||||
|
|
@ -139,6 +162,7 @@ export const NotificationsSection = ({
|
||||||
<SettingRow
|
<SettingRow
|
||||||
label="Lead inbox notifications"
|
label="Lead inbox notifications"
|
||||||
description="Notify when teammates send messages to the team lead"
|
description="Notify when teammates send messages to the team lead"
|
||||||
|
icon={<Inbox className="size-4" />}
|
||||||
>
|
>
|
||||||
<SettingsToggle
|
<SettingsToggle
|
||||||
enabled={safeConfig.notifications.notifyOnLeadInbox}
|
enabled={safeConfig.notifications.notifyOnLeadInbox}
|
||||||
|
|
@ -149,6 +173,7 @@ export const NotificationsSection = ({
|
||||||
<SettingRow
|
<SettingRow
|
||||||
label="User inbox notifications"
|
label="User inbox notifications"
|
||||||
description="Notify when teammates send messages to you"
|
description="Notify when teammates send messages to you"
|
||||||
|
icon={<Mail className="size-4" />}
|
||||||
>
|
>
|
||||||
<SettingsToggle
|
<SettingsToggle
|
||||||
enabled={safeConfig.notifications.notifyOnUserInbox}
|
enabled={safeConfig.notifications.notifyOnUserInbox}
|
||||||
|
|
@ -159,6 +184,7 @@ export const NotificationsSection = ({
|
||||||
<SettingRow
|
<SettingRow
|
||||||
label="Task clarification notifications"
|
label="Task clarification notifications"
|
||||||
description="Show native OS notifications when a task needs your input"
|
description="Show native OS notifications when a task needs your input"
|
||||||
|
icon={<HelpCircle className="size-4" />}
|
||||||
>
|
>
|
||||||
<SettingsToggle
|
<SettingsToggle
|
||||||
enabled={safeConfig.notifications.notifyOnClarifications}
|
enabled={safeConfig.notifications.notifyOnClarifications}
|
||||||
|
|
@ -169,6 +195,7 @@ export const NotificationsSection = ({
|
||||||
<SettingRow
|
<SettingRow
|
||||||
label="Task comment notifications"
|
label="Task comment notifications"
|
||||||
description="Show native OS notifications when agents comment on tasks"
|
description="Show native OS notifications when agents comment on tasks"
|
||||||
|
icon={<MessageSquare className="size-4" />}
|
||||||
>
|
>
|
||||||
<SettingsToggle
|
<SettingsToggle
|
||||||
enabled={safeConfig.notifications.notifyOnTaskComments}
|
enabled={safeConfig.notifications.notifyOnTaskComments}
|
||||||
|
|
@ -183,6 +210,7 @@ export const NotificationsSection = ({
|
||||||
? `Snoozed until ${new Date(safeConfig.notifications.snoozedUntil!).toLocaleTimeString()}`
|
? `Snoozed until ${new Date(safeConfig.notifications.snoozedUntil!).toLocaleTimeString()}`
|
||||||
: 'Temporarily pause notifications'
|
: 'Temporarily pause notifications'
|
||||||
}
|
}
|
||||||
|
icon={<Clock className="size-4" />}
|
||||||
>
|
>
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{isSnoozed ? (
|
{isSnoozed ? (
|
||||||
|
|
@ -208,12 +236,17 @@ export const NotificationsSection = ({
|
||||||
{/* Task Status Change Notifications — grouped section */}
|
{/* Task Status Change Notifications — grouped section */}
|
||||||
<div className="border-b py-3" style={{ borderColor: 'var(--color-border-subtle)' }}>
|
<div className="border-b py-3" style={{ borderColor: 'var(--color-border-subtle)' }}>
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<div>
|
<div className="flex items-start gap-2.5">
|
||||||
<div className="text-sm font-medium" style={{ color: 'var(--color-text)' }}>
|
<div className="mt-0.5 shrink-0" style={{ color: 'var(--color-text-muted)' }}>
|
||||||
Task status change notifications
|
<ArrowRightLeft className="size-4" />
|
||||||
</div>
|
</div>
|
||||||
<div className="text-xs" style={{ color: 'var(--color-text-muted)' }}>
|
<div>
|
||||||
Show native OS notifications when a task's status changes
|
<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's status changes
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="shrink-0">
|
<div className="shrink-0">
|
||||||
|
|
@ -282,7 +315,7 @@ export const NotificationsSection = ({
|
||||||
onRemoveTrigger={onRemoveTrigger}
|
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)' }}>
|
<p className="mb-3 text-xs" style={{ color: 'var(--color-text-muted)' }}>
|
||||||
Notifications from these repositories will be ignored
|
Notifications from these repositories will be ignored
|
||||||
</p>
|
</p>
|
||||||
|
|
|
||||||
|
|
@ -1581,8 +1581,14 @@ export const TeamDetailView = ({ teamName }: TeamDetailViewProps): React.JSX.Ele
|
||||||
const el = document.querySelector(`[data-task-id="${taskId}"]`);
|
const el = document.querySelector(`[data-task-id="${taskId}"]`);
|
||||||
if (el) {
|
if (el) {
|
||||||
el.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
el.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
||||||
el.classList.add('ring-2', 'ring-blue-400/50');
|
el.classList.remove('kanban-card-focus-pulse');
|
||||||
setTimeout(() => el.classList.remove('ring-2', 'ring-blue-400/50'), 1500);
|
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)}
|
onTaskClick={(task) => setSelectedTask(task)}
|
||||||
|
|
@ -1919,8 +1925,14 @@ export const TeamDetailView = ({ teamName }: TeamDetailViewProps): React.JSX.Ele
|
||||||
const el = document.querySelector(`[data-task-id="${taskId}"]`);
|
const el = document.querySelector(`[data-task-id="${taskId}"]`);
|
||||||
if (el) {
|
if (el) {
|
||||||
el.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
el.scrollIntoView({ behavior: 'smooth', block: 'nearest' });
|
||||||
el.classList.add('ring-2', 'ring-blue-400/50');
|
el.classList.remove('kanban-card-focus-pulse');
|
||||||
setTimeout(() => el.classList.remove('ring-2', 'ring-blue-400/50'), 1500);
|
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) => {
|
onOwnerChange={(taskId, owner) => {
|
||||||
|
|
|
||||||
|
|
@ -89,9 +89,9 @@ const TEAMMATE_COLORS: Record<string, TeamColorSet> = {
|
||||||
/** Reserved for the human user — never assigned to team members. */
|
/** Reserved for the human user — never assigned to team members. */
|
||||||
user: {
|
user: {
|
||||||
border: '#f5f5f4',
|
border: '#f5f5f4',
|
||||||
borderLight: '#a8a29e',
|
borderLight: '#78716c',
|
||||||
badge: 'rgba(245, 245, 244, 0.12)',
|
badge: 'rgba(245, 245, 244, 0.12)',
|
||||||
badgeLight: 'rgba(120, 113, 108, 0.14)',
|
badgeLight: 'rgba(87, 83, 78, 0.18)',
|
||||||
text: '#d6d3d1',
|
text: '#d6d3d1',
|
||||||
textLight: '#44403c',
|
textLight: '#44403c',
|
||||||
},
|
},
|
||||||
|
|
|
||||||
|
|
@ -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-n,
|
||||||
.kanban-grid-resize-handle-s {
|
.kanban-grid-resize-handle-s {
|
||||||
left: 50%;
|
left: 50%;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue