refactor: update styling and color classes across components

- Changed error message styling in ICalImportModal to use new color classes.
- Updated task completion styling in TaskListView to use new success color classes.
- Modified selected styling in ContactListItem tests to reflect new background class.
- Adjusted duplicate warning styling in ContactImportDialog to use new warning color classes.
- Refactored background and border colors in CalendarInvitationBanner for various statuses.
- Updated email list item and viewer components to use new warning and success color classes.
- Refined styling for unread indicators in email components.
- Enhanced error fallback styling in error components to use new warning color classes.
- Updated filter rule modal button hover styles to use new destructive color classes.
- Added experimental feature descriptions in Plugins and Themes settings.
- Refined vacation settings validation warning styling to use new warning color classes.
- Updated toast component styles to use new color classes for different states.
- Introduced a new built-in theme 'Qui' with specific color variables.
- Adjusted email security status colors to use new warning color classes.
This commit is contained in:
Linus Rath
2026-03-25 23:10:47 +01:00
parent f64306be5e
commit b5172802bb
33 changed files with 481 additions and 166 deletions
+4 -1
View File
@@ -46,4 +46,7 @@ next-env.d.ts
/data/ /data/
# Local private data # Local private data
/local-data/ /local-data/
# Sibling repos
/repos/
+16
View File
@@ -0,0 +1,16 @@
refactor: update styling and color classes across components
- Changed error message styling in ICalImportModal to use new color classes.
- Updated task completion styling in TaskListView to use new success color classes.
- Modified selected styling in ContactListItem tests to reflect new background class.
- Adjusted duplicate warning styling in ContactImportDialog to use new warning color classes.
- Refactored background and border colors in CalendarInvitationBanner for various statuses.
- Updated email list item and viewer components to use new warning and success color classes.
- Refined styling for unread indicators in email components.
- Enhanced error fallback styling in error components to use new warning color classes.
- Updated filter rule modal button hover styles to use new destructive color classes.
- Added experimental feature descriptions in Plugins and Themes settings.
- Refined vacation settings validation warning styling to use new warning color classes.
- Updated toast component styles to use new color classes for different states.
- Introduced a new built-in theme 'Qui' with specific color variables.
- Adjusted email security status colors to use new warning color classes.
+6 -6
View File
@@ -733,9 +733,9 @@ export default function LoginPage() {
)} )}
</Button> </Button>
) : oauthDiscoveryDone ? ( ) : oauthDiscoveryDone ? (
<div className="p-3.5 bg-amber-500/10 border border-amber-500/20 rounded-xl flex items-start gap-2"> <div className="p-3.5 bg-warning/10 border border-warning/20 rounded-xl flex items-start gap-2">
<AlertCircle className="w-4 h-4 text-amber-700 dark:text-amber-400 flex-shrink-0 mt-0.5" /> <AlertCircle className="w-4 h-4 text-warning flex-shrink-0 mt-0.5" />
<p className="text-sm text-amber-700 dark:text-amber-400"> <p className="text-sm text-warning">
{t("error.oauth_discovery_failed")} {t("error.oauth_discovery_failed")}
</p> </p>
</div> </div>
@@ -941,9 +941,9 @@ export default function LoginPage() {
)} )}
{oauthEnabled && oauthDiscoveryDone && !oauthMetadata && ( {oauthEnabled && oauthDiscoveryDone && !oauthMetadata && (
<div className="mt-2 p-3 bg-amber-500/10 border border-amber-500/20 rounded-xl flex items-start gap-2"> <div className="mt-2 p-3 bg-warning/10 border border-warning/20 rounded-xl flex items-start gap-2">
<AlertCircle className="w-4 h-4 text-amber-700 dark:text-amber-400 flex-shrink-0 mt-0.5" /> <AlertCircle className="w-4 h-4 text-warning flex-shrink-0 mt-0.5" />
<p className="text-sm text-amber-700 dark:text-amber-400"> <p className="text-sm text-warning">
{t("error.oauth_discovery_failed")} {t("error.oauth_discovery_failed")}
</p> </p>
</div> </div>
+13 -2
View File
@@ -69,6 +69,7 @@ interface TabDef {
label: string; label: string;
icon: LucideIcon; icon: LucideIcon;
group: TabGroup; group: TabGroup;
experimental?: boolean;
} }
const tabIcons: Record<Tab, LucideIcon> = { const tabIcons: Record<Tab, LucideIcon> = {
@@ -162,8 +163,8 @@ export default function SettingsPage() {
{ id: 'contacts', label: t('tabs.contacts'), icon: tabIcons.contacts, group: 'apps' }, { id: 'contacts', label: t('tabs.contacts'), icon: tabIcons.contacts, group: 'apps' },
...(supportsFiles ? [{ id: 'files' as Tab, label: t('tabs.files'), icon: tabIcons.files, group: 'apps' as TabGroup }] : []), ...(supportsFiles ? [{ id: 'files' as Tab, label: t('tabs.files'), icon: tabIcons.files, group: 'apps' as TabGroup }] : []),
...(isFeatureEnabled('sidebarAppsEnabled') ? [{ id: 'sidebar_apps' as Tab, label: t('tabs.sidebar_apps'), icon: tabIcons.sidebar_apps, group: 'apps' as TabGroup }] : []), ...(isFeatureEnabled('sidebarAppsEnabled') ? [{ id: 'sidebar_apps' as Tab, label: t('tabs.sidebar_apps'), icon: tabIcons.sidebar_apps, group: 'apps' as TabGroup }] : []),
...(isFeatureEnabled('themesEnabled') ? [{ id: 'themes' as Tab, label: 'Themes', icon: tabIcons.themes, group: 'system' as TabGroup }] : []), ...(isFeatureEnabled('themesEnabled') ? [{ id: 'themes' as Tab, label: 'Themes', icon: tabIcons.themes, group: 'system' as TabGroup, experimental: true }] : []),
...(isFeatureEnabled('pluginsEnabled') ? [{ id: 'plugins' as Tab, label: 'Plugins', icon: tabIcons.plugins, group: 'system' as TabGroup }] : []), ...(isFeatureEnabled('pluginsEnabled') ? [{ id: 'plugins' as Tab, label: 'Plugins', icon: tabIcons.plugins, group: 'system' as TabGroup, experimental: true }] : []),
{ id: 'advanced', label: t('tabs.advanced'), icon: tabIcons.advanced, group: 'system' }, { id: 'advanced', label: t('tabs.advanced'), icon: tabIcons.advanced, group: 'system' },
]; ];
@@ -290,6 +291,11 @@ export default function SettingsPage() {
<span className="flex items-center gap-3"> <span className="flex items-center gap-3">
<Icon className="w-4 h-4 text-muted-foreground" /> <Icon className="w-4 h-4 text-muted-foreground" />
{tab.label} {tab.label}
{tab.experimental && (
<span className="text-[10px] font-medium px-1.5 py-0.5 rounded-full bg-warning/15 text-warning">
Experimental
</span>
)}
</span> </span>
<ChevronRight className="w-4 h-4 text-muted-foreground" /> <ChevronRight className="w-4 h-4 text-muted-foreground" />
</button> </button>
@@ -396,6 +402,11 @@ export default function SettingsPage() {
activeTab === tab.id ? 'text-accent-foreground' : 'text-muted-foreground' activeTab === tab.id ? 'text-accent-foreground' : 'text-muted-foreground'
)} /> )} />
{tab.label} {tab.label}
{tab.experimental && (
<span className="ml-auto text-[10px] font-medium px-1.5 py-0.5 rounded-full bg-warning/15 text-warning shrink-0">
Experimental
</span>
)}
</button> </button>
); );
})} })}
+7 -7
View File
@@ -105,18 +105,18 @@ export default function AdminDashboardPage() {
<div className="max-w-3xl space-y-8"> <div className="max-w-3xl space-y-8">
{/* Warnings */} {/* Warnings */}
{warnings.map((msg, i) => ( {warnings.map((msg, i) => (
<div key={i} className="flex items-start gap-3 rounded-lg border border-amber-200 bg-amber-50 dark:border-amber-900 dark:bg-amber-950/30 p-4"> <div key={i} className="flex items-start gap-3 rounded-lg border border-warning/20 bg-warning/10 p-4">
<AlertTriangle className="w-5 h-5 text-amber-600 dark:text-amber-400 mt-0.5 shrink-0" /> <AlertTriangle className="w-5 h-5 text-warning mt-0.5 shrink-0" />
<p className="text-sm text-amber-700 dark:text-amber-300">{msg}</p> <p className="text-sm text-warning">{msg}</p>
</div> </div>
))} ))}
{status && !status.lastLogin && ( {status && !status.lastLogin && (
<div className="flex items-start gap-3 rounded-lg border border-amber-200 bg-amber-50 dark:border-amber-900 dark:bg-amber-950/30 p-4"> <div className="flex items-start gap-3 rounded-lg border border-warning/20 bg-warning/10 p-4">
<AlertTriangle className="w-5 h-5 text-amber-600 dark:text-amber-400 mt-0.5 shrink-0" /> <AlertTriangle className="w-5 h-5 text-warning mt-0.5 shrink-0" />
<div> <div>
<p className="text-sm font-medium text-amber-800 dark:text-amber-200">First login detected</p> <p className="text-sm font-medium text-warning">First login detected</p>
<p className="text-sm text-amber-700 dark:text-amber-300 mt-0.5"> <p className="text-sm text-warning/80 mt-0.5">
Remember to remove ADMIN_PASSWORD from your .env file now that the hash is stored securely. Remember to remove ADMIN_PASSWORD from your .env file now that the hash is stored securely.
</p> </p>
</div> </div>
+2 -2
View File
@@ -252,8 +252,8 @@ export default function AdminPluginsPage() {
</div> </div>
{plugin.permissions.length > 0 && ( {plugin.permissions.length > 0 && (
<div className="flex items-center gap-1 mt-1"> <div className="flex items-center gap-1 mt-1">
<AlertTriangle className="w-3 h-3 text-amber-500" /> <AlertTriangle className="w-3 h-3 text-warning" />
<span className="text-xs text-amber-600 dark:text-amber-400"> <span className="text-xs text-warning">
Permissions: {plugin.permissions.join(', ')} Permissions: {plugin.permissions.join(', ')}
</span> </span>
</div> </div>
+99 -47
View File
@@ -23,6 +23,39 @@
--color-popover: #ffffff; --color-popover: #ffffff;
--color-popover-foreground: #0f172a; --color-popover-foreground: #0f172a;
/* Sidebar */
--color-sidebar: #f8fafc;
--color-sidebar-foreground: #0f172a;
--color-sidebar-border: #e2e8f0;
--color-sidebar-accent: #f1f5f9;
--color-sidebar-accent-foreground: #0f172a;
/* Card */
--color-card: #ffffff;
--color-card-foreground: #0f172a;
/* Semantic status colors */
--color-success: #22c55e;
--color-success-foreground: #ffffff;
--color-warning: #eab308;
--color-warning-foreground: #ffffff;
--color-info: #3b82f6;
--color-info-foreground: #ffffff;
/* Selection states */
--color-selection: #dbeafe;
--color-selection-foreground: #1e40af;
/* Unread indicator */
--color-unread: #3b82f6;
/* Chart colors */
--color-chart-1: #3b82f6;
--color-chart-2: #22c55e;
--color-chart-3: #f59e0b;
--color-chart-4: #ef4444;
--color-chart-5: #8b5cf6;
/* Settings variables */ /* Settings variables */
--font-size-base: 16px; --font-size-base: 16px;
--list-item-height: 48px; --list-item-height: 48px;
@@ -54,6 +87,39 @@
--color-destructive-foreground: #fafafa; --color-destructive-foreground: #fafafa;
--color-popover: #1c1c1c; --color-popover: #1c1c1c;
--color-popover-foreground: #fafafa; --color-popover-foreground: #fafafa;
/* Sidebar */
--color-sidebar: #0a0a0a;
--color-sidebar-foreground: #fafafa;
--color-sidebar-border: #262626;
--color-sidebar-accent: #1a1a1a;
--color-sidebar-accent-foreground: #fafafa;
/* Card */
--color-card: #141414;
--color-card-foreground: #fafafa;
/* Semantic status colors */
--color-success: #16a34a;
--color-success-foreground: #ffffff;
--color-warning: #ca8a04;
--color-warning-foreground: #ffffff;
--color-info: #60a5fa;
--color-info-foreground: #ffffff;
/* Selection states */
--color-selection: rgba(59, 130, 246, 0.25);
--color-selection-foreground: #93c5fd;
/* Unread indicator */
--color-unread: #60a5fa;
/* Chart colors */
--color-chart-1: #60a5fa;
--color-chart-2: #4ade80;
--color-chart-3: #fbbf24;
--color-chart-4: #f87171;
--color-chart-5: #a78bfa;
} }
@theme inline { @theme inline {
@@ -74,6 +140,27 @@
--color-destructive-foreground: var(--color-destructive-foreground); --color-destructive-foreground: var(--color-destructive-foreground);
--color-popover: var(--color-popover); --color-popover: var(--color-popover);
--color-popover-foreground: var(--color-popover-foreground); --color-popover-foreground: var(--color-popover-foreground);
--color-sidebar: var(--color-sidebar);
--color-sidebar-foreground: var(--color-sidebar-foreground);
--color-sidebar-border: var(--color-sidebar-border);
--color-sidebar-accent: var(--color-sidebar-accent);
--color-sidebar-accent-foreground: var(--color-sidebar-accent-foreground);
--color-card: var(--color-card);
--color-card-foreground: var(--color-card-foreground);
--color-success: var(--color-success);
--color-success-foreground: var(--color-success-foreground);
--color-warning: var(--color-warning);
--color-warning-foreground: var(--color-warning-foreground);
--color-info: var(--color-info);
--color-info-foreground: var(--color-info-foreground);
--color-selection: var(--color-selection);
--color-selection-foreground: var(--color-selection-foreground);
--color-unread: var(--color-unread);
--color-chart-1: var(--color-chart-1);
--color-chart-2: var(--color-chart-2);
--color-chart-3: var(--color-chart-3);
--color-chart-4: var(--color-chart-4);
--color-chart-5: var(--color-chart-5);
} }
* { * {
@@ -163,21 +250,16 @@ body {
} }
.email-content blockquote { .email-content blockquote {
border-left: 3px solid #d1d5db; border-left: 3px solid var(--color-border);
padding-left: 1rem; padding-left: 1rem;
margin: 1rem 0; margin: 1rem 0;
color: #4b5563; color: var(--color-muted-foreground);
font-style: italic; font-style: italic;
} }
.dark .email-content blockquote {
border-left-color: #4b5563;
color: #9ca3af;
}
.email-content pre { .email-content pre {
background-color: #f5f5f7; background-color: var(--color-muted);
border: 1px solid #e5e7eb; border: 1px solid var(--color-border);
border-radius: 0.375rem; border-radius: 0.375rem;
padding: 1rem; padding: 1rem;
font-family: "SF Mono", Monaco, Menlo, Consolas, monospace; font-family: "SF Mono", Monaco, Menlo, Consolas, monospace;
@@ -186,23 +268,13 @@ body {
margin: 1rem 0; margin: 1rem 0;
} }
.dark .email-content pre {
background-color: #1f2937;
border-color: #374151;
}
.email-content code { .email-content code {
background-color: #f3f4f6; background-color: var(--color-muted);
padding: 0.125rem 0.375rem; padding: 0.125rem 0.375rem;
border-radius: 0.25rem; border-radius: 0.25rem;
font-family: "SF Mono", Monaco, Menlo, Consolas, monospace; font-family: "SF Mono", Monaco, Menlo, Consolas, monospace;
font-size: 0.875rem; font-size: 0.875rem;
color: #dc2626; color: var(--color-destructive);
}
.dark .email-content code {
background-color: #374151;
color: #f87171;
} }
.email-content h1, .email-content h1,
@@ -282,26 +354,15 @@ body {
.email-content table[border="1"] th, .email-content table[border="1"] th,
.email-content table[border="1"] td { .email-content table[border="1"] td {
padding: 0.625rem; padding: 0.625rem;
border: 1px solid #e5e7eb; border: 1px solid var(--color-border);
text-align: left; text-align: left;
} }
.email-content table.data-table th, .email-content table.data-table th,
.email-content table[border="1"] th { .email-content table[border="1"] th {
background-color: #f9fafb; background-color: var(--color-muted);
font-weight: 600; font-weight: 600;
color: #374151; color: var(--color-foreground);
}
.dark .email-content table.data-table th,
.dark .email-content table[border="1"] th {
background-color: #1f2937;
color: #d1d5db;
}
.dark .email-content table.data-table td,
.dark .email-content table[border="1"] td {
border-color: #374151;
} }
/* Reset styles for layout tables (commonly used in HTML emails) */ /* Reset styles for layout tables (commonly used in HTML emails) */
@@ -323,28 +384,19 @@ body {
.email-content hr { .email-content hr {
border: none; border: none;
border-top: 1px solid #e5e7eb; border-top: 1px solid var(--color-border);
margin: 1.5rem 0; margin: 1.5rem 0;
} }
.dark .email-content hr {
border-top-color: #374151;
}
/* Email thread quoted text */ /* Email thread quoted text */
.email-content .quoted-text { .email-content .quoted-text {
border-left: 3px solid #d1d5db; border-left: 3px solid var(--color-border);
padding-left: 1rem; padding-left: 1rem;
margin: 1rem 0; margin: 1rem 0;
color: #4b5563; color: var(--color-muted-foreground);
opacity: 0.8; opacity: 0.8;
} }
.dark .email-content .quoted-text {
border-left-color: #4b5563;
color: #9ca3af;
}
/* Toast animations */ /* Toast animations */
@keyframes toast-enter { @keyframes toast-enter {
from { from {
+3 -3
View File
@@ -182,7 +182,7 @@ export function CalendarDayView({
className={cn( className={cn(
"flex-shrink-0 w-3.5 h-3.5 rounded-full border flex items-center justify-center", "flex-shrink-0 w-3.5 h-3.5 rounded-full border flex items-center justify-center",
isCompleted isCompleted
? "bg-green-500 border-green-500 text-white" ? "bg-success border-success text-success-foreground"
: "border-muted-foreground/40 hover:border-primary" : "border-muted-foreground/40 hover:border-primary"
)} )}
> >
@@ -288,8 +288,8 @@ export function CalendarDayView({
style={{ top: (nowMinutes / 60) * HOUR_HEIGHT }} style={{ top: (nowMinutes / 60) * HOUR_HEIGHT }}
> >
<div className="flex items-center"> <div className="flex items-center">
<div className="w-2.5 h-2.5 rounded-full bg-red-500 -ml-1" /> <div className="w-2.5 h-2.5 rounded-full bg-destructive -ml-1" />
<div className="flex-1 h-px bg-red-500" /> <div className="flex-1 h-px bg-destructive" />
</div> </div>
</div> </div>
)} )}
+3 -3
View File
@@ -252,7 +252,7 @@ export function CalendarWeekView({
> >
<span className={cn( <span className={cn(
"w-2.5 h-2.5 rounded-full border flex-shrink-0 flex items-center justify-center", "w-2.5 h-2.5 rounded-full border flex-shrink-0 flex items-center justify-center",
isCompleted ? "bg-green-500 border-green-500" : "border-current" isCompleted ? "bg-success border-success" : "border-current"
)}> )}>
{isCompleted && <Check className="h-2 w-2 text-white" />} {isCompleted && <Check className="h-2 w-2 text-white" />}
</span> </span>
@@ -402,8 +402,8 @@ export function CalendarWeekView({
style={{ top: (nowMinutes / 60) * HOUR_HEIGHT }} style={{ top: (nowMinutes / 60) * HOUR_HEIGHT }}
> >
<div className="flex items-center"> <div className="flex items-center">
<div className="w-2 h-2 rounded-full bg-red-500 -ml-1" /> <div className="w-2 h-2 rounded-full bg-destructive -ml-1" />
<div className="flex-1 h-px bg-red-500" /> <div className="flex-1 h-px bg-destructive" />
</div> </div>
</div> </div>
)} )}
+11 -11
View File
@@ -297,7 +297,7 @@ export function EventDetailPopover({
<p className="text-xs text-muted-foreground mt-0.5 pl-[18px]"> <p className="text-xs text-muted-foreground mt-0.5 pl-[18px]">
{calendar.name} {calendar.name}
{event.status === "tentative" && ( {event.status === "tentative" && (
<span className="ml-2 inline-flex items-center px-1.5 py-0.5 rounded text-[10px] font-medium bg-amber-100 text-amber-800 dark:bg-amber-900/30 dark:text-amber-400"> <span className="ml-2 inline-flex items-center px-1.5 py-0.5 rounded text-[10px] font-medium bg-warning/15 text-warning">
{t("detail.tentative")} {t("detail.tentative")}
</span> </span>
)} )}
@@ -507,8 +507,8 @@ export function EventDetailPopover({
onClick={() => onRsvp("accepted")} onClick={() => onRsvp("accepted")}
className={ className={
userCurrentStatus === "accepted" userCurrentStatus === "accepted"
? "bg-green-600 hover:bg-green-700 text-white dark:bg-green-500 dark:hover:bg-green-600" ? "bg-success hover:bg-success/80 text-success-foreground"
: "text-green-600 dark:text-green-400 border-green-300 dark:border-green-700 hover:bg-green-50 dark:hover:bg-green-950" : "text-success border-success/30 hover:bg-success/10"
} }
> >
{userCurrentStatus === "accepted" && <Check className="w-3.5 h-3.5 mr-1" />} {userCurrentStatus === "accepted" && <Check className="w-3.5 h-3.5 mr-1" />}
@@ -520,8 +520,8 @@ export function EventDetailPopover({
onClick={() => onRsvp("tentative")} onClick={() => onRsvp("tentative")}
className={ className={
userCurrentStatus === "tentative" userCurrentStatus === "tentative"
? "bg-amber-600 hover:bg-amber-700 text-white dark:bg-amber-500 dark:hover:bg-amber-600" ? "bg-warning hover:bg-warning/80 text-warning-foreground"
: "border border-amber-500 text-amber-600 hover:bg-amber-50 dark:text-amber-400 dark:hover:bg-amber-950" : "border border-warning/30 text-warning hover:bg-warning/10"
} }
> >
{userCurrentStatus === "tentative" && <Check className="w-3.5 h-3.5 mr-1" />} {userCurrentStatus === "tentative" && <Check className="w-3.5 h-3.5 mr-1" />}
@@ -533,8 +533,8 @@ export function EventDetailPopover({
onClick={() => onRsvp("declined")} onClick={() => onRsvp("declined")}
className={ className={
userCurrentStatus === "declined" userCurrentStatus === "declined"
? "bg-red-600 hover:bg-red-700 text-white dark:bg-red-500 dark:hover:bg-red-600" ? "bg-destructive hover:bg-destructive/80 text-destructive-foreground"
: "text-red-600 hover:bg-red-50 dark:text-red-400 dark:hover:bg-red-950" : "text-destructive hover:bg-destructive/10"
} }
> >
{userCurrentStatus === "declined" && <Check className="w-3.5 h-3.5 mr-1" />} {userCurrentStatus === "declined" && <Check className="w-3.5 h-3.5 mr-1" />}
@@ -548,7 +548,7 @@ export function EventDetailPopover({
<div className="px-4 py-2.5 border-t border-border flex items-center gap-1.5"> <div className="px-4 py-2.5 border-t border-border flex items-center gap-1.5">
{showDeleteConfirm ? ( {showDeleteConfirm ? (
<div className="flex items-center gap-2 w-full"> <div className="flex items-center gap-2 w-full">
<span className="text-sm text-red-600 dark:text-red-400 flex-1"> <span className="text-sm text-destructive flex-1">
{t("form.delete_confirm")} {t("form.delete_confirm")}
</span> </span>
<Button <Button
@@ -611,9 +611,9 @@ function ParticipantStatusBadge({
t: ReturnType<typeof useTranslations>; t: ReturnType<typeof useTranslations>;
}) { }) {
const colors: Record<string, string> = { const colors: Record<string, string> = {
accepted: "text-green-600 dark:text-green-400", accepted: "text-success",
declined: "text-red-600 dark:text-red-400", declined: "text-destructive",
tentative: "text-amber-600 dark:text-amber-400", tentative: "text-warning",
"needs-action": "text-muted-foreground", "needs-action": "text-muted-foreground",
}; };
const labels: Record<string, string> = { const labels: Record<string, string> = {
+12 -12
View File
@@ -530,8 +530,8 @@ export function EventModal({
variant={userCurrentStatus === "accepted" ? "default" : "outline"} variant={userCurrentStatus === "accepted" ? "default" : "outline"}
onClick={() => handleRsvp("accepted")} onClick={() => handleRsvp("accepted")}
className={userCurrentStatus === "accepted" className={userCurrentStatus === "accepted"
? "bg-green-600 hover:bg-green-700 text-white dark:bg-green-500 dark:hover:bg-green-600" ? "bg-success hover:bg-success/80 text-success-foreground"
: "text-green-600 dark:text-green-400 border-green-300 dark:border-green-700 hover:bg-green-50 dark:hover:bg-green-950"} : "text-success border-success/30 hover:bg-success/10"}
> >
{userCurrentStatus === "accepted" && <Check className="w-4 h-4 mr-1" />} {userCurrentStatus === "accepted" && <Check className="w-4 h-4 mr-1" />}
{t("participants.accepted")} {t("participants.accepted")}
@@ -541,8 +541,8 @@ export function EventModal({
variant={userCurrentStatus === "tentative" ? "default" : "outline"} variant={userCurrentStatus === "tentative" ? "default" : "outline"}
onClick={() => handleRsvp("tentative")} onClick={() => handleRsvp("tentative")}
className={userCurrentStatus === "tentative" className={userCurrentStatus === "tentative"
? "bg-amber-600 hover:bg-amber-700 text-white dark:bg-amber-500 dark:hover:bg-amber-600" ? "bg-warning hover:bg-warning/80 text-warning-foreground"
: "border border-amber-500 text-amber-600 hover:bg-amber-50 dark:text-amber-400 dark:hover:bg-amber-950"} : "border border-warning/30 text-warning hover:bg-warning/10"}
> >
{userCurrentStatus === "tentative" && <Check className="w-4 h-4 mr-1" />} {userCurrentStatus === "tentative" && <Check className="w-4 h-4 mr-1" />}
{t("participants.tentative")} {t("participants.tentative")}
@@ -552,8 +552,8 @@ export function EventModal({
variant={userCurrentStatus === "declined" ? "default" : "ghost"} variant={userCurrentStatus === "declined" ? "default" : "ghost"}
onClick={() => handleRsvp("declined")} onClick={() => handleRsvp("declined")}
className={userCurrentStatus === "declined" className={userCurrentStatus === "declined"
? "bg-red-600 hover:bg-red-700 text-white dark:bg-red-500 dark:hover:bg-red-600" ? "bg-destructive hover:bg-destructive/80 text-destructive-foreground"
: "text-red-600 hover:bg-red-50 dark:text-red-400 dark:hover:bg-red-950"} : "text-destructive hover:bg-destructive/10"}
> >
{userCurrentStatus === "declined" && <Check className="w-4 h-4 mr-1" />} {userCurrentStatus === "declined" && <Check className="w-4 h-4 mr-1" />}
{t("participants.declined")} {t("participants.declined")}
@@ -701,8 +701,8 @@ export function EventModal({
{onDelete && ( {onDelete && (
showDeleteConfirm ? ( showDeleteConfirm ? (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<span className="text-sm text-red-600 dark:text-red-400">{t("form.delete_confirm")}</span> <span className="text-sm text-destructive">{t("form.delete_confirm")}</span>
<Button variant="outline" size="sm" onClick={() => { onDelete(event.id, hasParticipants || undefined); onClose(); }} className="text-red-600 dark:text-red-400 border-red-300 dark:border-red-700"> <Button variant="outline" size="sm" onClick={() => { onDelete(event.id, hasParticipants || undefined); onClose(); }} className="text-destructive border-destructive/30">
{t("events.delete")} {t("events.delete")}
</Button> </Button>
<Button variant="ghost" size="sm" onClick={() => setShowDeleteConfirm(false)}> <Button variant="ghost" size="sm" onClick={() => setShowDeleteConfirm(false)}>
@@ -710,7 +710,7 @@ export function EventModal({
</Button> </Button>
</div> </div>
) : ( ) : (
<Button variant="ghost" size="sm" onClick={() => setShowDeleteConfirm(true)} className="text-red-600 dark:text-red-400"> <Button variant="ghost" size="sm" onClick={() => setShowDeleteConfirm(true)} className="text-destructive">
<Trash2 className="w-4 h-4 mr-1" /> <Trash2 className="w-4 h-4 mr-1" />
{t("events.delete")} {t("events.delete")}
</Button> </Button>
@@ -997,9 +997,9 @@ function StatusBadge({ status, isOrganizer, t }: {
return <span className="text-xs text-primary">{t("participants.organizer")}</span>; return <span className="text-xs text-primary">{t("participants.organizer")}</span>;
} }
const colors: Record<string, string> = { const colors: Record<string, string> = {
accepted: "text-green-600 dark:text-green-400", accepted: "text-success",
declined: "text-red-600 dark:text-red-400", declined: "text-destructive",
tentative: "text-amber-600 dark:text-amber-400", tentative: "text-warning",
"needs-action": "text-muted-foreground", "needs-action": "text-muted-foreground",
}; };
const labels: Record<string, string> = { const labels: Record<string, string> = {
+1 -1
View File
@@ -349,7 +349,7 @@ export function ICalImportModal({ calendars, client, onClose }: ICalImportModalP
)} )}
{error && ( {error && (
<div className="text-sm text-red-600 dark:text-red-400 bg-red-50 dark:bg-red-950/30 rounded-md px-3 py-2"> <div className="text-sm text-destructive bg-destructive/10 rounded-md px-3 py-2">
{error} {error}
</div> </div>
)} )}
+1 -1
View File
@@ -204,7 +204,7 @@ export function TaskListView({
className={cn( className={cn(
"mt-0.5 flex-shrink-0 w-5 h-5 rounded-full border-2 flex items-center justify-center transition-colors", "mt-0.5 flex-shrink-0 w-5 h-5 rounded-full border-2 flex items-center justify-center transition-colors",
isCompleted isCompleted
? "bg-green-500 border-green-500 text-white" ? "bg-success border-success text-success-foreground"
: "border-muted-foreground/40 hover:border-primary" : "border-muted-foreground/40 hover:border-primary"
)} )}
aria-label={isCompleted ? t("tasks.mark_incomplete") : t("tasks.mark_complete")} aria-label={isCompleted ? t("tasks.mark_incomplete") : t("tasks.mark_complete")}
@@ -52,7 +52,7 @@ describe('ContactListItem', () => {
it('applies selected styling', () => { it('applies selected styling', () => {
const { container } = render(<ContactListItem contact={contact} {...baseProps} isSelected={true} />); const { container } = render(<ContactListItem contact={contact} {...baseProps} isSelected={true} />);
const div = container.firstElementChild; const div = container.firstElementChild;
expect(div?.className).toContain('bg-blue-200'); expect(div?.className).toContain('bg-selection');
}); });
it('shows email as display name when no name exists', () => { it('shows email as display name when no name exists', () => {
@@ -205,7 +205,7 @@ export function ContactImportDialog({
)} )}
</div> </div>
{isDupe && ( {isDupe && (
<span className="text-xs px-1.5 py-0.5 rounded bg-amber-100 dark:bg-amber-900 text-amber-700 dark:text-amber-400 flex-shrink-0"> <span className="text-xs px-1.5 py-0.5 rounded bg-warning/15 text-warning flex-shrink-0">
{t("import.duplicate")} {t("import.duplicate")}
</span> </span>
)} )}
+2 -2
View File
@@ -59,9 +59,9 @@ export function ContactListItem({ contact, isSelected, isChecked, hasSelection,
className={cn( className={cn(
"w-full flex items-center cursor-pointer select-none transition-all duration-200 border-b border-border", "w-full flex items-center cursor-pointer select-none transition-all duration-200 border-b border-border",
isSelected isSelected
? "bg-blue-200 dark:bg-blue-900/50 shadow-sm" ? "bg-selection shadow-sm"
: "bg-background hover:bg-muted hover:shadow-sm", : "bg-background hover:bg-muted hover:shadow-sm",
isChecked && !isSelected && "ring-2 ring-primary/20 bg-blue-100 dark:bg-blue-900/30", isChecked && !isSelected && "ring-2 ring-primary/20 bg-selection/60",
)} )}
style={{ gap: 'var(--density-item-gap)', paddingInline: '16px', paddingBlock: 'var(--density-item-py)' }} style={{ gap: 'var(--density-item-gap)', paddingInline: '16px', paddingBlock: 'var(--density-item-py)' }}
> >
+21 -21
View File
@@ -171,11 +171,11 @@ function getParticipationLabel(
function getParticipationTone(status: string | null): string { function getParticipationTone(status: string | null): string {
switch (status) { switch (status) {
case 'accepted': case 'accepted':
return 'bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400'; return 'bg-success/15 text-success';
case 'tentative': case 'tentative':
return 'bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-400'; return 'bg-warning/15 text-warning';
case 'declined': case 'declined':
return 'bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400'; return 'bg-destructive/15 text-destructive';
default: default:
return 'bg-muted text-muted-foreground'; return 'bg-muted text-muted-foreground';
} }
@@ -731,9 +731,9 @@ export function CalendarInvitationBanner({ email }: CalendarInvitationBannerProp
if (state === 'error') { if (state === 'error') {
return ( return (
<div className="rounded-lg border border-red-200 dark:border-red-900/50 bg-red-50/50 dark:bg-red-950/20 px-4 py-3 flex items-center gap-2.5"> <div className="rounded-lg border border-destructive/20 bg-destructive/10 px-4 py-3 flex items-center gap-2.5">
<AlertCircle className="w-4 h-4 text-red-600 dark:text-red-400 flex-shrink-0" /> <AlertCircle className="w-4 h-4 text-destructive flex-shrink-0" />
<span className="text-sm text-red-600 dark:text-red-400">{t('parse_error')}</span> <span className="text-sm text-destructive">{t('parse_error')}</span>
</div> </div>
); );
} }
@@ -744,7 +744,7 @@ export function CalendarInvitationBanner({ email }: CalendarInvitationBannerProp
<div className="flex items-center justify-between px-4 py-2.5 bg-muted/30 border-b border-border"> <div className="flex items-center justify-between px-4 py-2.5 bg-muted/30 border-b border-border">
<div className="flex items-center gap-2 min-w-0"> <div className="flex items-center gap-2 min-w-0">
{isCancellation ? ( {isCancellation ? (
<CalendarX className="w-4 h-4 text-red-600 dark:text-red-400 flex-shrink-0" /> <CalendarX className="w-4 h-4 text-destructive flex-shrink-0" />
) : ( ) : (
<Calendar className="w-4 h-4 text-primary flex-shrink-0" /> <Calendar className="w-4 h-4 text-primary flex-shrink-0" />
)} )}
@@ -808,19 +808,19 @@ export function CalendarInvitationBanner({ email }: CalendarInvitationBannerProp
<span className={cn( <span className={cn(
"rounded-full px-2 py-0.5 text-[11px] font-medium", "rounded-full px-2 py-0.5 text-[11px] font-medium",
parsedEvent.status === 'cancelled' parsedEvent.status === 'cancelled'
? "bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400" ? "bg-destructive/15 text-destructive"
: "bg-amber-100 text-amber-700 dark:bg-amber-900/30 dark:text-amber-400" : "bg-warning/15 text-warning"
)}> )}>
{t(`event_status_${parsedEvent.status}`)} {t(`event_status_${parsedEvent.status}`)}
</span> </span>
)} )}
{existingEvent && ( {existingEvent && (
<span className="rounded-full bg-green-100 px-2 py-0.5 text-[11px] font-medium text-green-700 dark:bg-green-900/30 dark:text-green-400"> <span className="rounded-full bg-success/15 px-2 py-0.5 text-[11px] font-medium text-success">
{t('already_in_calendar')} {t('already_in_calendar')}
</span> </span>
)} )}
{userIsOrganizer && ( {userIsOrganizer && (
<span className="rounded-full bg-blue-100 px-2 py-0.5 text-[11px] font-medium text-blue-700 dark:bg-blue-900/30 dark:text-blue-400"> <span className="rounded-full bg-info/15 px-2 py-0.5 text-[11px] font-medium text-info">
{t('organizer_role')} {t('organizer_role')}
</span> </span>
)} )}
@@ -833,7 +833,7 @@ export function CalendarInvitationBanner({ email }: CalendarInvitationBannerProp
</span> </span>
)} )}
{actionFeedback && ( {actionFeedback && (
<span className="rounded-full bg-green-100 px-2 py-0.5 text-[11px] font-medium text-green-700 dark:bg-green-900/30 dark:text-green-400"> <span className="rounded-full bg-success/15 px-2 py-0.5 text-[11px] font-medium text-success">
{actionFeedback} {actionFeedback}
</span> </span>
)} )}
@@ -877,8 +877,8 @@ export function CalendarInvitationBanner({ email }: CalendarInvitationBannerProp
<div className={cn( <div className={cn(
'flex items-start gap-1.5 text-xs rounded-md px-3 py-2', 'flex items-start gap-1.5 text-xs rounded-md px-3 py-2',
trustAssessment.level === 'warning' trustAssessment.level === 'warning'
? 'bg-red-50 dark:bg-red-950/20 text-red-700 dark:text-red-400 border border-red-200 dark:border-red-900/50' ? 'bg-destructive/10 text-destructive border border-destructive/20'
: 'bg-amber-50 dark:bg-amber-950/20 text-amber-700 dark:text-amber-400 border border-amber-200 dark:border-amber-900/50' : 'bg-warning/10 text-warning border border-warning/20'
)}> )}>
<AlertCircle className="w-3.5 h-3.5 mt-0.5 flex-shrink-0" /> <AlertCircle className="w-3.5 h-3.5 mt-0.5 flex-shrink-0" />
<span>{trustMessage}</span> <span>{trustMessage}</span>
@@ -887,7 +887,7 @@ export function CalendarInvitationBanner({ email }: CalendarInvitationBannerProp
{/* Action error */} {/* Action error */}
{actionError && ( {actionError && (
<div className="flex items-start gap-1.5 text-xs text-red-600 dark:text-red-400 rounded-md px-3 py-2 bg-red-50 dark:bg-red-950/20 border border-red-200 dark:border-red-900/50"> <div className="flex items-start gap-1.5 text-xs text-destructive rounded-md px-3 py-2 bg-destructive/10 border border-destructive/20">
<AlertCircle className="w-3.5 h-3.5 mt-0.5 flex-shrink-0" /> <AlertCircle className="w-3.5 h-3.5 mt-0.5 flex-shrink-0" />
<span>{actionError}</span> <span>{actionError}</span>
</div> </div>
@@ -905,8 +905,8 @@ export function CalendarInvitationBanner({ email }: CalendarInvitationBannerProp
className={cn( className={cn(
"inline-flex items-center gap-1.5 text-sm font-medium px-3 py-1.5 rounded-md transition-colors min-h-[36px] disabled:opacity-50 border", "inline-flex items-center gap-1.5 text-sm font-medium px-3 py-1.5 rounded-md transition-colors min-h-[36px] disabled:opacity-50 border",
currentRsvp === 'accepted' currentRsvp === 'accepted'
? "bg-green-100 dark:bg-green-900/30 text-green-700 dark:text-green-400 border-green-200 dark:border-green-800" ? "bg-success/15 text-success border-success/20"
: "text-muted-foreground hover:text-green-700 dark:hover:text-green-400 border-border hover:border-green-300 dark:hover:border-green-800 hover:bg-green-50 dark:hover:bg-green-950/20" : "text-muted-foreground hover:text-success border-border hover:border-success/30 hover:bg-success/10"
)} )}
> >
<Check className="w-3.5 h-3.5" /> <Check className="w-3.5 h-3.5" />
@@ -919,8 +919,8 @@ export function CalendarInvitationBanner({ email }: CalendarInvitationBannerProp
className={cn( className={cn(
"inline-flex items-center gap-1.5 text-sm font-medium px-3 py-1.5 rounded-md transition-colors min-h-[36px] disabled:opacity-50 border", "inline-flex items-center gap-1.5 text-sm font-medium px-3 py-1.5 rounded-md transition-colors min-h-[36px] disabled:opacity-50 border",
currentRsvp === 'tentative' currentRsvp === 'tentative'
? "bg-amber-100 dark:bg-amber-900/30 text-amber-700 dark:text-amber-400 border-amber-200 dark:border-amber-800" ? "bg-warning/15 text-warning border-warning/20"
: "text-muted-foreground hover:text-amber-700 dark:hover:text-amber-400 border-border hover:border-amber-300 dark:hover:border-amber-800 hover:bg-amber-50 dark:hover:bg-amber-950/20" : "text-muted-foreground hover:text-warning border-border hover:border-warning/30 hover:bg-warning/10"
)} )}
> >
<HelpCircle className="w-3.5 h-3.5" /> <HelpCircle className="w-3.5 h-3.5" />
@@ -933,8 +933,8 @@ export function CalendarInvitationBanner({ email }: CalendarInvitationBannerProp
className={cn( className={cn(
"inline-flex items-center gap-1.5 text-sm font-medium px-3 py-1.5 rounded-md transition-colors min-h-[36px] disabled:opacity-50 border", "inline-flex items-center gap-1.5 text-sm font-medium px-3 py-1.5 rounded-md transition-colors min-h-[36px] disabled:opacity-50 border",
currentRsvp === 'declined' currentRsvp === 'declined'
? "bg-red-100 dark:bg-red-900/30 text-red-700 dark:text-red-400 border-red-200 dark:border-red-800" ? "bg-destructive/15 text-destructive border-destructive/20"
: "text-muted-foreground hover:text-red-700 dark:hover:text-red-400 border-border hover:border-red-300 dark:hover:border-red-800 hover:bg-red-50 dark:hover:bg-red-950/20" : "text-muted-foreground hover:text-destructive border-border hover:border-destructive/30 hover:bg-destructive/10"
)} )}
> >
<X className="w-3.5 h-3.5" /> <X className="w-3.5 h-3.5" />
+5 -5
View File
@@ -87,15 +87,15 @@ export function EmailListItem({ email, selected, onClick, onContextMenu, onToggl
// Apply color tag as background, with selected and unread states // Apply color tag as background, with selected and unread states
colorTag ? colorTag : ( colorTag ? colorTag : (
selected selected
? "bg-blue-200 dark:bg-blue-900/50" ? "bg-selection"
: "bg-background" : "bg-background"
), ),
selected && !colorTag && "shadow-sm", selected && !colorTag && "shadow-sm",
!colorTag && !selected && "hover:bg-muted hover:shadow-sm", !colorTag && !selected && "hover:bg-muted hover:shadow-sm",
colorTag && "hover:brightness-95 dark:hover:brightness-110", colorTag && "hover:brightness-95 dark:hover:brightness-110",
isUnread && !selected && !colorTag && "bg-amber-50 dark:bg-amber-900/20", isUnread && !selected && !colorTag && "bg-warning/10",
// Add visual feedback for checked state // Add visual feedback for checked state
isChecked && "ring-2 ring-primary/20 bg-blue-100 dark:bg-blue-900/30", isChecked && "ring-2 ring-primary/20 bg-selection/60",
// Drag state visual feedback // Drag state visual feedback
isDragging && "opacity-50 scale-[0.98] ring-2 ring-primary/30", isDragging && "opacity-50 scale-[0.98] ring-2 ring-primary/30",
// Long press visual feedback // Long press visual feedback
@@ -140,7 +140,7 @@ export function EmailListItem({ email, selected, onClick, onContextMenu, onToggl
{/* Unread indicator */} {/* Unread indicator */}
{isUnread && ( {isUnread && (
<div className="absolute left-1 top-1/2 -translate-y-1/2"> <div className="absolute left-1 top-1/2 -translate-y-1/2">
<Circle className="w-2 h-2 fill-blue-600 text-blue-600 dark:fill-blue-400 dark:text-blue-400" /> <Circle className="w-2 h-2 fill-unread text-unread" />
</div> </div>
)} )}
@@ -172,7 +172,7 @@ export function EmailListItem({ email, selected, onClick, onContextMenu, onToggl
<Star className="w-3.5 h-3.5 fill-amber-400 text-amber-400" /> <Star className="w-3.5 h-3.5 fill-amber-400 text-amber-400" />
)} )}
{isImportant && ( {isImportant && (
<span className="px-1.5 py-0.5 text-xs bg-amber-100 dark:bg-amber-900/30 text-amber-700 dark:text-amber-400 rounded font-medium"> <span className="px-1.5 py-0.5 text-xs bg-warning/15 text-warning dark:text-warning rounded font-medium">
Important Important
</span> </span>
)} )}
+3 -3
View File
@@ -3541,7 +3541,7 @@ export function EmailViewer({
) : null; ) : null;
})()} })()}
{isImportant && ( {isImportant && (
<span className="px-1.5 lg:px-2 py-0.5 bg-amber-50 dark:bg-amber-900/30 text-amber-700 dark:text-amber-400 rounded-full text-xs font-medium whitespace-nowrap flex-shrink-0 self-center"> <span className="px-1.5 lg:px-2 py-0.5 bg-warning/15 text-warning rounded-full text-xs font-medium whitespace-nowrap flex-shrink-0 self-center">
{t('important')} {t('important')}
</span> </span>
)} )}
@@ -4372,9 +4372,9 @@ export function EmailViewer({
{/* Draft Banner */} {/* Draft Banner */}
{isDraft && ( {isDraft && (
<div className="border-b border-border bg-amber-50 dark:bg-amber-950/30"> <div className="border-b border-border bg-warning/10">
<div className="max-w-4xl mx-auto px-6 py-2.5 flex items-center justify-between"> <div className="max-w-4xl mx-auto px-6 py-2.5 flex items-center justify-between">
<div className="flex items-center gap-2 text-amber-700 dark:text-amber-400"> <div className="flex items-center gap-2 text-warning">
<File className="w-4 h-4" /> <File className="w-4 h-4" />
<span className="text-sm font-medium">{t('draft_banner')}</span> <span className="text-sm font-medium">{t('draft_banner')}</span>
</div> </div>
+4 -4
View File
@@ -83,10 +83,10 @@ export function SmimeStatusBanner({ status, onUnlockKey, className }: SmimeStatu
if (items.length === 0) return null; if (items.length === 0) return null;
const variantStyles = { const variantStyles = {
success: 'bg-green-50 dark:bg-green-950/30 text-green-700 dark:text-green-400 border-green-200 dark:border-green-800', success: 'bg-success/10 text-success border-success/30',
warning: 'bg-yellow-50 dark:bg-yellow-950/30 text-yellow-700 dark:text-yellow-400 border-yellow-200 dark:border-yellow-800', warning: 'bg-warning/10 text-warning border-warning/30',
error: 'bg-red-50 dark:bg-red-950/30 text-red-700 dark:text-red-400 border-red-200 dark:border-red-800', error: 'bg-destructive/10 text-destructive border-destructive/30',
info: 'bg-blue-50 dark:bg-blue-950/30 text-blue-700 dark:text-blue-400 border-blue-200 dark:border-blue-800', info: 'bg-info/10 text-info border-info/30',
}; };
return ( return (
+3 -3
View File
@@ -85,9 +85,9 @@ export function ThreadEmailItem({
"pl-12 pr-4", "pl-12 pr-4",
"border-l-2 border-l-transparent", "border-l-2 border-l-transparent",
selected selected
? "bg-blue-200 dark:bg-blue-900/50 border-l-primary" ? "bg-selection border-l-primary"
: "hover:bg-muted/50", : "hover:bg-muted/50",
isUnread && !selected && "bg-amber-50 dark:bg-amber-900/20", isUnread && !selected && "bg-warning/10",
!isLast && "border-b border-border/30", !isLast && "border-b border-border/30",
isChecked && "ring-2 ring-primary/20 bg-accent/40", isChecked && "ring-2 ring-primary/20 bg-accent/40",
isDragging && "opacity-50 scale-[0.98] ring-2 ring-primary/30", isDragging && "opacity-50 scale-[0.98] ring-2 ring-primary/30",
@@ -121,7 +121,7 @@ export function ThreadEmailItem({
{/* Unread indicator */} {/* Unread indicator */}
{isUnread && ( {isUnread && (
<div className="absolute left-7 top-1/2 -translate-y-1/2"> <div className="absolute left-7 top-1/2 -translate-y-1/2">
<Circle className="w-1.5 h-1.5 fill-blue-600 text-blue-600 dark:fill-blue-400 dark:text-blue-400" /> <Circle className="w-1.5 h-1.5 fill-unread text-unread" />
</div> </div>
)} )}
+2 -2
View File
@@ -156,7 +156,7 @@ const SingleEmailItem = React.forwardRef<HTMLDivElement, SingleEmailItemProps>(
{isUnread && ( {isUnread && (
<div className="absolute left-1 top-1/2 -translate-y-1/2"> <div className="absolute left-1 top-1/2 -translate-y-1/2">
<Circle className="w-2 h-2 fill-blue-600 text-blue-600 dark:fill-blue-400 dark:text-blue-400" /> <Circle className="w-2 h-2 fill-unread text-unread" />
</div> </div>
)} )}
@@ -456,7 +456,7 @@ export const ThreadListItem = React.forwardRef<HTMLDivElement, ThreadListItemPro
{hasUnread && ( {hasUnread && (
<div className="absolute left-1 top-1/2 -translate-y-1/2"> <div className="absolute left-1 top-1/2 -translate-y-1/2">
<Circle className="w-2 h-2 fill-blue-600 text-blue-600 dark:fill-blue-400 dark:text-blue-400" /> <Circle className="w-2 h-2 fill-unread text-unread" />
</div> </div>
)} )}
+1 -1
View File
@@ -94,7 +94,7 @@ export function EmailViewerErrorFallback({ resetError, t }: FallbackProps) {
export function ComposerErrorFallback({ resetError, t }: FallbackProps) { export function ComposerErrorFallback({ resetError, t }: FallbackProps) {
return ( return (
<div className="flex flex-col h-full bg-background border rounded-lg items-center justify-center p-8"> <div className="flex flex-col h-full bg-background border rounded-lg items-center justify-center p-8">
<AlertCircle className="w-10 h-10 text-amber-500 mb-3" /> <AlertCircle className="w-10 h-10 text-warning mb-3" />
<p className="text-sm text-muted-foreground text-center mb-4"> <p className="text-sm text-muted-foreground text-center mb-4">
{t("composer_error")} {t("composer_error")}
</p> </p>
+2 -2
View File
@@ -299,7 +299,7 @@ export function FilterRuleModal({
type="button" type="button"
onClick={() => removeCondition(index)} onClick={() => removeCondition(index)}
disabled={conditions.length <= 1} disabled={conditions.length <= 1}
className="p-1.5 rounded hover:bg-muted text-muted-foreground hover:text-red-600 dark:hover:text-red-400 transition-colors disabled:opacity-30 disabled:pointer-events-none" className="p-1.5 rounded hover:bg-muted text-muted-foreground hover:text-destructive transition-colors disabled:opacity-30 disabled:pointer-events-none"
aria-label={t("delete_rule")} aria-label={t("delete_rule")}
> >
<Trash2 className="w-4 h-4" /> <Trash2 className="w-4 h-4" />
@@ -387,7 +387,7 @@ export function FilterRuleModal({
type="button" type="button"
onClick={() => removeAction(index)} onClick={() => removeAction(index)}
disabled={actions.length <= 1} disabled={actions.length <= 1}
className="p-1.5 rounded hover:bg-muted text-muted-foreground hover:text-red-600 dark:hover:text-red-400 transition-colors disabled:opacity-30 disabled:pointer-events-none" className="p-1.5 rounded hover:bg-muted text-muted-foreground hover:text-destructive transition-colors disabled:opacity-30 disabled:pointer-events-none"
aria-label={t("delete_rule")} aria-label={t("delete_rule")}
> >
<Trash2 className="w-4 h-4" /> <Trash2 className="w-4 h-4" />
+4 -4
View File
@@ -93,7 +93,7 @@ export function SieveEditorModal({
</div> </div>
<div className="px-6 py-4 flex-1 overflow-hidden flex flex-col space-y-4"> <div className="px-6 py-4 flex-1 overflow-hidden flex flex-col space-y-4">
<div className="flex items-start gap-2 p-3 rounded-md bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800 text-sm text-amber-700 dark:text-amber-400"> <div className="flex items-start gap-2 p-3 rounded-md bg-warning/10 border border-warning/20 text-sm text-warning">
<AlertTriangle className="w-4 h-4 mt-0.5 flex-shrink-0" /> <AlertTriangle className="w-4 h-4 mt-0.5 flex-shrink-0" />
<p>{t("warning")}</p> <p>{t("warning")}</p>
</div> </div>
@@ -131,8 +131,8 @@ export function SieveEditorModal({
<div <div
className={`flex items-start gap-2 p-3 rounded-md text-sm ${ className={`flex items-start gap-2 p-3 rounded-md text-sm ${
validationResult.isValid validationResult.isValid
? "bg-green-50 dark:bg-green-900/20 border border-green-200 dark:border-green-800 text-green-700 dark:text-green-400" ? "bg-success/10 border border-success/20 text-success"
: "bg-red-50 dark:bg-red-900/20 border border-red-200 dark:border-red-800 text-red-700 dark:text-red-400" : "bg-destructive/10 border border-destructive/20 text-destructive"
}`} }`}
> >
{validationResult.isValid ? ( {validationResult.isValid ? (
@@ -157,7 +157,7 @@ export function SieveEditorModal({
)} )}
{showSaveWarning && ( {showSaveWarning && (
<div className="flex items-start gap-2 p-3 rounded-md bg-amber-50 dark:bg-amber-900/20 border border-amber-200 dark:border-amber-800 text-sm text-amber-700 dark:text-amber-400"> <div className="flex items-start gap-2 p-3 rounded-md bg-warning/10 border border-warning/20 text-sm text-warning">
<AlertTriangle className="w-4 h-4 mt-0.5 flex-shrink-0" /> <AlertTriangle className="w-4 h-4 mt-0.5 flex-shrink-0" />
<p>{t("save_warning")}</p> <p>{t("save_warning")}</p>
</div> </div>
+6 -6
View File
@@ -73,10 +73,10 @@ function StorageQuotaCircle({ quota, usagePercent }: { quota: { used: number; to
const free = quota.total - quota.used; const free = quota.total - quota.used;
const strokeColor = usagePercent > 90 const strokeColor = usagePercent > 90
? "stroke-red-500 dark:stroke-red-400" ? "stroke-destructive"
: usagePercent > 70 : usagePercent > 70
? "stroke-amber-500 dark:stroke-amber-400" ? "stroke-warning"
: "stroke-green-500 dark:stroke-green-400"; : "stroke-success";
return ( return (
<div className="relative"> <div className="relative">
@@ -123,10 +123,10 @@ function StorageQuotaCircle({ quota, usagePercent }: { quota: { used: number; to
className={cn( className={cn(
"h-1.5 rounded-full transition-all", "h-1.5 rounded-full transition-all",
usagePercent > 90 usagePercent > 90
? "bg-red-500 dark:bg-red-400" ? "bg-destructive"
: usagePercent > 70 : usagePercent > 70
? "bg-amber-500 dark:bg-amber-400" ? "bg-warning"
: "bg-green-500 dark:bg-green-400" : "bg-success"
)} )}
style={{ width: `${usagePercent}%` }} style={{ width: `${usagePercent}%` }}
/> />
+1 -1
View File
@@ -68,7 +68,7 @@ export function PluginsSettings() {
}; };
return ( return (
<SettingsSection title="Plugins" description="Manage installed plugins. Upload plugin .zip files to add new functionality."> <SettingsSection title="Plugins" description="Manage installed plugins. Upload plugin .zip files to add new functionality." experimental experimentalDescription="Plugins is an experimental feature. The plugin API is not yet stable and may change between releases, which could break existing plugins. Plugins run in a sandboxed environment but have access to your data within the application. Only install plugins from sources you trust.">
{/* Plugin List */} {/* Plugin List */}
{plugins.length === 0 ? ( {plugins.length === 0 ? (
<div className="flex flex-col items-center justify-center py-12 text-center"> <div className="flex flex-col items-center justify-center py-12 text-center">
+15 -2
View File
@@ -1,16 +1,29 @@
import { ReactNode } from 'react'; import { ReactNode } from 'react';
import { Lock } from 'lucide-react'; import { Lock, FlaskConical } from 'lucide-react';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
interface SettingsSectionProps { interface SettingsSectionProps {
title: string; title: string;
description?: string; description?: string;
children: ReactNode; children: ReactNode;
experimental?: boolean;
experimentalDescription?: string;
} }
export function SettingsSection({ title, description, children }: SettingsSectionProps) { export function SettingsSection({ title, description, children, experimental, experimentalDescription }: SettingsSectionProps) {
return ( return (
<div className="space-y-4"> <div className="space-y-4">
{experimental && (
<div className="flex gap-3 rounded-lg border border-amber-300 bg-amber-50 p-3 dark:border-amber-700 dark:bg-amber-950/30">
<FlaskConical className="w-5 h-5 text-amber-600 dark:text-amber-400 mt-0.5 shrink-0" />
<div>
<p className="text-sm font-medium text-amber-800 dark:text-amber-300">Experimental Feature</p>
{experimentalDescription && (
<p className="text-xs text-amber-700 dark:text-amber-400/80 mt-1">{experimentalDescription}</p>
)}
</div>
</div>
)}
<div> <div>
<h3 className="text-lg font-medium text-foreground">{title}</h3> <h3 className="text-lg font-medium text-foreground">{title}</h3>
{description && ( {description && (
+2 -1
View File
@@ -69,7 +69,8 @@ export function ThemesSettings() {
}; };
return ( return (
<SettingsSection title="Themes" description="Customize the appearance with color themes. Upload .zip theme files or activate built-in presets."> <SettingsSection title="Themes" description="Customize the appearance with color themes. Upload .zip theme files or activate built-in presets." experimental experimentalDescription="Themes is an experimental feature. Custom themes may not cover all UI elements, and theme formats could change in future updates. Built-in presets are stable, but uploaded themes may require updates after application upgrades.">
{/* Theme Grid */} {/* Theme Grid */}
<div className="grid grid-cols-2 sm:grid-cols-3 gap-3"> <div className="grid grid-cols-2 sm:grid-cols-3 gap-3">
{/* Default theme card */} {/* Default theme card */}
+1 -1
View File
@@ -240,7 +240,7 @@ export function VacationSettings() {
{validationWarnings.length > 0 && ( {validationWarnings.length > 0 && (
<div className="space-y-2"> <div className="space-y-2">
{validationWarnings.map((warning, i) => ( {validationWarnings.map((warning, i) => (
<div key={i} className="flex items-start gap-2 text-sm text-amber-700 dark:text-amber-400"> <div key={i} className="flex items-start gap-2 text-sm text-warning">
<AlertTriangle className="w-4 h-4 mt-0.5 flex-shrink-0" /> <AlertTriangle className="w-4 h-4 mt-0.5 flex-shrink-0" />
<span>{warning}</span> <span>{warning}</span>
</div> </div>
+8 -8
View File
@@ -36,17 +36,17 @@ const icons = {
}; };
const iconContainerStyles = { const iconContainerStyles = {
success: "bg-emerald-500 text-white", success: "bg-success text-success-foreground",
error: "bg-red-500 text-white", error: "bg-destructive text-destructive-foreground",
info: "bg-blue-500 text-white", info: "bg-info text-info-foreground",
warning: "bg-amber-500 text-white", warning: "bg-warning text-warning-foreground",
}; };
const progressBarStyles = { const progressBarStyles = {
success: "bg-emerald-500", success: "bg-success",
error: "bg-red-500", error: "bg-destructive",
info: "bg-blue-500", info: "bg-info",
warning: "bg-amber-500", warning: "bg-warning",
}; };
export function ToastItem({ toast, onClose }: ToastProps) { export function ToastItem({ toast, onClose }: ToastProps) {
+219
View File
@@ -1,5 +1,87 @@
import type { InstalledTheme } from './plugin-types'; import type { InstalledTheme } from './plugin-types';
const quiCSS = `
:root {
--color-border: #e4e4e7;
--color-input: #e4e4e7;
--color-ring: #3b82f6;
--color-background: #ffffff;
--color-foreground: #09090b;
--color-primary: #3b82f6;
--color-primary-foreground: #ffffff;
--color-secondary: #f4f4f5;
--color-secondary-foreground: #18181b;
--color-muted: #f4f4f5;
--color-muted-foreground: #71717a;
--color-accent: #dbeafe;
--color-accent-foreground: #1d4ed8;
--color-destructive: #ef4444;
--color-destructive-foreground: #ffffff;
--color-popover: #ffffff;
--color-popover-foreground: #09090b;
--color-sidebar: #fafafa;
--color-sidebar-foreground: #09090b;
--color-sidebar-border: #e4e4e7;
--color-sidebar-accent: #f4f4f5;
--color-sidebar-accent-foreground: #18181b;
--color-card: #ffffff;
--color-card-foreground: #09090b;
--color-success: #22c55e;
--color-success-foreground: #ffffff;
--color-warning: #eab308;
--color-warning-foreground: #ffffff;
--color-info: #3b82f6;
--color-info-foreground: #ffffff;
--color-selection: #dbeafe;
--color-selection-foreground: #1d4ed8;
--color-unread: #3b82f6;
--color-chart-1: #3b82f6;
--color-chart-2: #22c55e;
--color-chart-3: #f59e0b;
--color-chart-4: #ef4444;
--color-chart-5: #8b5cf6;
}
.dark {
--color-border: #27272a;
--color-input: #27272a;
--color-ring: #3b82f6;
--color-background: #09090b;
--color-foreground: #fafafa;
--color-primary: #3b82f6;
--color-primary-foreground: #ffffff;
--color-secondary: #18181b;
--color-secondary-foreground: #fafafa;
--color-muted: #18181b;
--color-muted-foreground: #a1a1aa;
--color-accent: #172554;
--color-accent-foreground: #93c5fd;
--color-destructive: #ef4444;
--color-destructive-foreground: #fafafa;
--color-popover: #18181b;
--color-popover-foreground: #fafafa;
--color-sidebar: #09090b;
--color-sidebar-foreground: #fafafa;
--color-sidebar-border: #27272a;
--color-sidebar-accent: #18181b;
--color-sidebar-accent-foreground: #fafafa;
--color-card: #141414;
--color-card-foreground: #fafafa;
--color-success: #16a34a;
--color-success-foreground: #ffffff;
--color-warning: #ca8a04;
--color-warning-foreground: #ffffff;
--color-info: #60a5fa;
--color-info-foreground: #ffffff;
--color-selection: rgba(59, 130, 246, 0.25);
--color-selection-foreground: #93c5fd;
--color-unread: #60a5fa;
--color-chart-1: #60a5fa;
--color-chart-2: #4ade80;
--color-chart-3: #fbbf24;
--color-chart-4: #f87171;
--color-chart-5: #a78bfa;
}`;
const nordCSS = ` const nordCSS = `
:root { :root {
--color-border: #d8dee9; --color-border: #d8dee9;
@@ -19,6 +101,27 @@ const nordCSS = `
--color-destructive-foreground: #eceff4; --color-destructive-foreground: #eceff4;
--color-popover: #eceff4; --color-popover: #eceff4;
--color-popover-foreground: #2e3440; --color-popover-foreground: #2e3440;
--color-sidebar: #e5e9f0;
--color-sidebar-foreground: #2e3440;
--color-sidebar-border: #d8dee9;
--color-sidebar-accent: #d8dee9;
--color-sidebar-accent-foreground: #2e3440;
--color-card: #eceff4;
--color-card-foreground: #2e3440;
--color-success: #a3be8c;
--color-success-foreground: #2e3440;
--color-warning: #ebcb8b;
--color-warning-foreground: #2e3440;
--color-info: #81a1c1;
--color-info-foreground: #eceff4;
--color-selection: #d8dee9;
--color-selection-foreground: #5e81ac;
--color-unread: #5e81ac;
--color-chart-1: #5e81ac;
--color-chart-2: #a3be8c;
--color-chart-3: #ebcb8b;
--color-chart-4: #bf616a;
--color-chart-5: #b48ead;
} }
.dark { .dark {
--color-border: #3b4252; --color-border: #3b4252;
@@ -38,6 +141,27 @@ const nordCSS = `
--color-destructive-foreground: #eceff4; --color-destructive-foreground: #eceff4;
--color-popover: #3b4252; --color-popover: #3b4252;
--color-popover-foreground: #eceff4; --color-popover-foreground: #eceff4;
--color-sidebar: #2e3440;
--color-sidebar-foreground: #eceff4;
--color-sidebar-border: #3b4252;
--color-sidebar-accent: #3b4252;
--color-sidebar-accent-foreground: #eceff4;
--color-card: #3b4252;
--color-card-foreground: #eceff4;
--color-success: #a3be8c;
--color-success-foreground: #2e3440;
--color-warning: #ebcb8b;
--color-warning-foreground: #2e3440;
--color-info: #88c0d0;
--color-info-foreground: #2e3440;
--color-selection: rgba(136, 192, 208, 0.2);
--color-selection-foreground: #88c0d0;
--color-unread: #88c0d0;
--color-chart-1: #88c0d0;
--color-chart-2: #a3be8c;
--color-chart-3: #ebcb8b;
--color-chart-4: #bf616a;
--color-chart-5: #b48ead;
}`; }`;
const catppuccinCSS = ` const catppuccinCSS = `
@@ -59,6 +183,27 @@ const catppuccinCSS = `
--color-destructive-foreground: #eff1f5; --color-destructive-foreground: #eff1f5;
--color-popover: #eff1f5; --color-popover: #eff1f5;
--color-popover-foreground: #4c4f69; --color-popover-foreground: #4c4f69;
--color-sidebar: #e6e9ef;
--color-sidebar-foreground: #4c4f69;
--color-sidebar-border: #ccd0da;
--color-sidebar-accent: #dce0e8;
--color-sidebar-accent-foreground: #4c4f69;
--color-card: #eff1f5;
--color-card-foreground: #4c4f69;
--color-success: #40a02b;
--color-success-foreground: #eff1f5;
--color-warning: #df8e1d;
--color-warning-foreground: #eff1f5;
--color-info: #1e66f5;
--color-info-foreground: #eff1f5;
--color-selection: #dce0e8;
--color-selection-foreground: #8839ef;
--color-unread: #8839ef;
--color-chart-1: #8839ef;
--color-chart-2: #40a02b;
--color-chart-3: #df8e1d;
--color-chart-4: #d20f39;
--color-chart-5: #1e66f5;
} }
.dark { .dark {
--color-border: #45475a; --color-border: #45475a;
@@ -78,6 +223,27 @@ const catppuccinCSS = `
--color-destructive-foreground: #1e1e2e; --color-destructive-foreground: #1e1e2e;
--color-popover: #313244; --color-popover: #313244;
--color-popover-foreground: #cdd6f4; --color-popover-foreground: #cdd6f4;
--color-sidebar: #1e1e2e;
--color-sidebar-foreground: #cdd6f4;
--color-sidebar-border: #45475a;
--color-sidebar-accent: #313244;
--color-sidebar-accent-foreground: #cdd6f4;
--color-card: #313244;
--color-card-foreground: #cdd6f4;
--color-success: #a6e3a1;
--color-success-foreground: #1e1e2e;
--color-warning: #f9e2af;
--color-warning-foreground: #1e1e2e;
--color-info: #89b4fa;
--color-info-foreground: #1e1e2e;
--color-selection: rgba(203, 166, 247, 0.2);
--color-selection-foreground: #cba6f7;
--color-unread: #cba6f7;
--color-chart-1: #cba6f7;
--color-chart-2: #a6e3a1;
--color-chart-3: #f9e2af;
--color-chart-4: #f38ba8;
--color-chart-5: #89b4fa;
}`; }`;
const solarizedCSS = ` const solarizedCSS = `
@@ -99,6 +265,27 @@ const solarizedCSS = `
--color-destructive-foreground: #fdf6e3; --color-destructive-foreground: #fdf6e3;
--color-popover: #fdf6e3; --color-popover: #fdf6e3;
--color-popover-foreground: #657b83; --color-popover-foreground: #657b83;
--color-sidebar: #eee8d5;
--color-sidebar-foreground: #657b83;
--color-sidebar-border: #eee8d5;
--color-sidebar-accent: #eee8d5;
--color-sidebar-accent-foreground: #586e75;
--color-card: #fdf6e3;
--color-card-foreground: #657b83;
--color-success: #859900;
--color-success-foreground: #fdf6e3;
--color-warning: #b58900;
--color-warning-foreground: #fdf6e3;
--color-info: #268bd2;
--color-info-foreground: #fdf6e3;
--color-selection: #eee8d5;
--color-selection-foreground: #268bd2;
--color-unread: #268bd2;
--color-chart-1: #268bd2;
--color-chart-2: #859900;
--color-chart-3: #b58900;
--color-chart-4: #dc322f;
--color-chart-5: #6c71c4;
} }
.dark { .dark {
--color-border: #073642; --color-border: #073642;
@@ -118,9 +305,41 @@ const solarizedCSS = `
--color-destructive-foreground: #fdf6e3; --color-destructive-foreground: #fdf6e3;
--color-popover: #073642; --color-popover: #073642;
--color-popover-foreground: #93a1a1; --color-popover-foreground: #93a1a1;
--color-sidebar: #002b36;
--color-sidebar-foreground: #839496;
--color-sidebar-border: #073642;
--color-sidebar-accent: #073642;
--color-sidebar-accent-foreground: #93a1a1;
--color-card: #073642;
--color-card-foreground: #93a1a1;
--color-success: #859900;
--color-success-foreground: #002b36;
--color-warning: #b58900;
--color-warning-foreground: #002b36;
--color-info: #268bd2;
--color-info-foreground: #002b36;
--color-selection: rgba(38, 139, 210, 0.2);
--color-selection-foreground: #268bd2;
--color-unread: #268bd2;
--color-chart-1: #268bd2;
--color-chart-2: #859900;
--color-chart-3: #b58900;
--color-chart-4: #dc322f;
--color-chart-5: #6c71c4;
}`; }`;
export const BUILTIN_THEMES: InstalledTheme[] = [ export const BUILTIN_THEMES: InstalledTheme[] = [
{
id: 'builtin-qui',
name: 'Qui',
version: '1.0.0',
author: 'Built-in',
description: 'Clean, modern zinc-and-blue theme inspired by autobrr/qui',
css: quiCSS,
variants: ['light', 'dark'],
enabled: true,
builtIn: true,
},
{ {
id: 'builtin-nord', id: 'builtin-nord',
name: 'Nord', name: 'Nord',
+2 -2
View File
@@ -154,10 +154,10 @@ export function getSecurityStatus(result?: string): {
case 'neutral': case 'neutral':
case 'temperror': case 'temperror':
return { return {
color: 'text-amber-700 dark:text-amber-400', color: 'text-warning',
icon: 'alert', icon: 'alert',
bgColor: 'bg-gray-50 dark:bg-gray-800', bgColor: 'bg-gray-50 dark:bg-gray-800',
borderColor: 'border-l-4 border-amber-600 dark:border-amber-500' borderColor: 'border-l-4 border-warning'
}; };
default: default:
return { return {