Fix status indicators using accent color instead of semantic colors

Status badges (Connected/Offline, HMS errors, maintenance, success/failed)
were using bambu-green which follows the user's accent color theme. This
caused confusing UX - e.g., "Connected" appearing red with a red accent.

Added fixed semantic status colors that never change with theme:
- status-ok: #22c55e (green) - success, online, OK states
- status-error: #ef4444 (red) - failed, offline, error states
- status-warning: #f59e0b (amber) - warnings, pending states

Updated components:
- PrintersPage: connection badge, HMS errors, maintenance, WiFi signal
- SmartPlugCard/SwitchbarPopover: plug connectivity status
- StatsPage: success/failed counts, failure rate, time accuracy
- QueuePage: queue item statuses (pending, completed, failed)
- ArchivesPage: failed/aborted archive badge
- ProjectDetailPage: child status, timeline events, BOM checklist
- CompareArchivesModal: archive completion status
- NotificationProviderCard: last success/error status

Closes #136
This commit is contained in:
maziggy
2026-01-24 07:25:35 +01:00
parent 5672d7965a
commit bda659e11d
14 changed files with 63 additions and 53 deletions
@@ -91,8 +91,8 @@ function ComparisonContent({ comparison }: { comparison: ArchiveComparison }) {
{archive.print_name}
</div>
<div className={`text-xs ${
archive.status === 'completed' ? 'text-bambu-green' :
archive.status === 'failed' ? 'text-red-400' : 'text-bambu-gray'
archive.status === 'completed' ? 'text-status-ok' :
archive.status === 'failed' ? 'text-status-error' : 'text-bambu-gray'
}`}>
{archive.status}
</div>
@@ -91,13 +91,13 @@ export function NotificationProviderCard({ provider, onEdit }: NotificationProvi
{/* Quick enable/disable toggle + Status indicator */}
<div className="flex items-center gap-3">
{provider.last_success && (
<span className="text-xs text-bambu-green hidden sm:inline">Last: {formatDateOnly(provider.last_success)}</span>
<span className="text-xs text-status-ok hidden sm:inline">Last: {formatDateOnly(provider.last_success)}</span>
)}
{/* Only show error if it's more recent than last success */}
{provider.last_error && provider.last_error_at && (
!provider.last_success || (parseUTCDate(provider.last_error_at)?.getTime() || 0) > (parseUTCDate(provider.last_success)?.getTime() || 0)
) && (
<span className="text-xs text-red-400" title={provider.last_error}>Error</span>
<span className="text-xs text-status-error" title={provider.last_error}>Error</span>
)}
<Toggle
checked={provider.enabled}
+3 -3
View File
@@ -136,11 +136,11 @@ export function SmartPlugCard({ plug, onEdit }: SmartPlugCardProps) {
<Loader2 className="w-4 h-4 text-bambu-gray animate-spin" />
) : isReachable ? (
<div className="flex items-center gap-1 text-sm">
<Wifi className="w-4 h-4 text-bambu-green" />
<span className={isOn ? 'text-bambu-green' : 'text-bambu-gray'}>{status?.state || 'Unknown'}</span>
<Wifi className="w-4 h-4 text-status-ok" />
<span className={isOn ? 'text-status-ok' : 'text-bambu-gray'}>{status?.state || 'Unknown'}</span>
</div>
) : (
<div className="flex items-center gap-1 text-sm text-red-400">
<div className="flex items-center gap-1 text-sm text-status-error">
<WifiOff className="w-4 h-4" />
<span>Offline</span>
</div>
+4 -4
View File
@@ -53,8 +53,8 @@ function SwitchItem({ plug }: { plug: SmartPlug }) {
<Loader2 className="w-3 h-3 text-bambu-gray animate-spin" />
) : isReachable ? (
<>
<Wifi className="w-3 h-3 text-bambu-green" />
<span className={isOn ? 'text-bambu-green' : 'text-bambu-gray'}>{status?.state || 'Unknown'}</span>
<Wifi className="w-3 h-3 text-status-ok" />
<span className={isOn ? 'text-status-ok' : 'text-bambu-gray'}>{status?.state || 'Unknown'}</span>
{status?.energy?.power !== null && status?.energy?.power !== undefined && (
<>
<span className="text-bambu-gray mx-1">|</span>
@@ -65,8 +65,8 @@ function SwitchItem({ plug }: { plug: SmartPlug }) {
</>
) : (
<>
<WifiOff className="w-3 h-3 text-red-400" />
<span className="text-red-400">Offline</span>
<WifiOff className="w-3 h-3 text-status-error" />
<span className="text-status-error">Offline</span>
</>
)}
</div>
+10
View File
@@ -10,6 +10,11 @@
--color-bambu-green-light: var(--accent-light);
--color-bambu-green-dark: var(--accent-dark);
/* Semantic status colors - fixed, don't change with accent */
--color-status-ok: var(--status-ok);
--color-status-error: var(--status-error);
--color-status-warning: var(--status-warning);
/* Theme-aware colors via CSS variables */
--color-bambu-dark: var(--bg-primary);
--color-bambu-dark-secondary: var(--bg-secondary);
@@ -29,6 +34,11 @@
--accent-light: #00c64d;
--accent-dark: #009438;
/* Semantic status colors - these never change with accent theme */
--status-ok: #22c55e; /* green-500 - always green for success/online/ok */
--status-error: #ef4444; /* red-500 - always red for error/offline/failed */
--status-warning: #f59e0b; /* amber-500 - always amber for warnings */
/* Default light mode background (neutral) */
--bg-primary: #f5f5f5;
--bg-secondary: #ffffff;
+1 -1
View File
@@ -543,7 +543,7 @@ function ArchiveCard({
/>
</button>
{(archive.status === 'failed' || archive.status === 'aborted') && (
<div className="absolute top-2 left-12 px-2 py-1 rounded text-xs bg-red-500/80 text-white">
<div className="absolute top-2 left-12 px-2 py-1 rounded text-xs bg-status-error/80 text-white">
{archive.status === 'aborted' ? 'cancelled' : 'failed'}
</div>
)}
+15 -15
View File
@@ -1354,7 +1354,7 @@ function PrinterCard({
{viewMode === 'compact' && (
<div
className={`w-2 h-2 rounded-full flex-shrink-0 ${
status?.connected ? 'bg-bambu-green' : 'bg-red-500'
status?.connected ? 'bg-status-ok' : 'bg-status-error'
}`}
title={status?.connected ? 'Connected' : 'Offline'}
/>
@@ -1440,8 +1440,8 @@ function PrinterCard({
<span
className={`flex items-center gap-1.5 px-2 py-1 rounded-full text-xs ${
status?.connected
? 'bg-bambu-green/20 text-bambu-green'
: 'bg-red-500/20 text-red-400'
? 'bg-status-ok/20 text-status-ok'
: 'bg-status-error/20 text-status-error'
}`}
>
{status?.connected ? (
@@ -1456,14 +1456,14 @@ function PrinterCard({
<span
className={`flex items-center gap-1 px-2 py-1 rounded-full text-xs ${
wifiSignal >= -50
? 'bg-bambu-green/20 text-bambu-green'
? 'bg-status-ok/20 text-status-ok'
: wifiSignal >= -60
? 'bg-bambu-green/20 text-bambu-green'
? 'bg-status-ok/20 text-status-ok'
: wifiSignal >= -70
? 'bg-amber-500/20 text-amber-600'
? 'bg-status-warning/20 text-status-warning'
: wifiSignal >= -80
? 'bg-orange-500/20 text-orange-600'
: 'bg-red-500/20 text-red-600'
: 'bg-status-error/20 text-status-error'
}`}
title={`WiFi: ${wifiSignal} dBm - ${getWifiStrength(wifiSignal).label}`}
>
@@ -1480,9 +1480,9 @@ function PrinterCard({
className={`flex items-center gap-1 px-2 py-1 rounded-full text-xs cursor-pointer hover:opacity-80 transition-opacity ${
knownErrors.length > 0
? knownErrors.some(e => e.severity <= 2)
? 'bg-red-500/20 text-red-400'
: 'bg-orange-500/20 text-orange-400'
: 'bg-bambu-green/20 text-bambu-green'
? 'bg-status-error/20 text-status-error'
: 'bg-status-warning/20 text-status-warning'
: 'bg-status-ok/20 text-status-ok'
}`}
title="Click to view HMS errors"
>
@@ -1497,10 +1497,10 @@ function PrinterCard({
onClick={() => navigate('/maintenance')}
className={`flex items-center gap-1 px-2 py-1 rounded-full text-xs cursor-pointer hover:opacity-80 transition-opacity ${
maintenanceInfo.due_count > 0
? 'bg-red-500/20 text-red-400'
? 'bg-status-error/20 text-status-error'
: maintenanceInfo.warning_count > 0
? 'bg-orange-500/20 text-orange-400'
: 'bg-bambu-green/20 text-bambu-green'
? 'bg-status-warning/20 text-status-warning'
: 'bg-status-ok/20 text-status-ok'
}`}
title={
maintenanceInfo.due_count > 0 || maintenanceInfo.warning_count > 0
@@ -3487,8 +3487,8 @@ function FirmwareUpdateModal({
<div className="w-full bg-bambu-dark-tertiary rounded-full h-2">
<div
className={`h-2 rounded-full transition-all ${
uploadStatus.status === 'error' ? 'bg-red-500' :
uploadStatus.status === 'complete' ? 'bg-bambu-green' : 'bg-orange-500'
uploadStatus.status === 'error' ? 'bg-status-error' :
uploadStatus.status === 'complete' ? 'bg-status-ok' : 'bg-orange-500'
} ${uploadStatus.status === 'uploading' ? 'animate-pulse' : ''}`}
style={{ width: `${uploadStatus.progress}%` }}
/>
+6 -6
View File
@@ -520,7 +520,7 @@ export function ProjectDetailPage() {
<p className="text-sm text-bambu-gray">Print Jobs</p>
<p className="text-xl font-semibold text-white">{stats.total_archives} <span className="text-sm font-normal text-bambu-gray">total</span></p>
{stats.failed_prints > 0 && (
<p className="text-sm text-red-400">{stats.failed_prints} failed</p>
<p className="text-sm text-status-error">{stats.failed_prints} failed</p>
)}
<p className="text-sm text-bambu-gray">{stats.completed_prints} parts printed</p>
</div>
@@ -610,7 +610,7 @@ export function ProjectDetailPage() {
/>
<span className="text-white">{child.name}</span>
<span className={`text-xs px-2 py-0.5 rounded ${
child.status === 'completed' ? 'bg-bambu-green/20 text-bambu-green' :
child.status === 'completed' ? 'bg-status-ok/20 text-status-ok' :
child.status === 'archived' ? 'bg-bambu-gray/20 text-bambu-gray' :
'bg-blue-500/20 text-blue-400'
}`}>
@@ -900,7 +900,7 @@ export function ProjectDetailPage() {
<div
key={item.id}
className={`p-3 rounded-lg transition-colors ${
item.is_complete ? 'bg-bambu-green/10' : 'bg-bambu-dark'
item.is_complete ? 'bg-status-ok/10' : 'bg-bambu-dark'
}`}
>
<div className="flex items-start gap-3">
@@ -909,7 +909,7 @@ export function ProjectDetailPage() {
disabled={updateBomMutation.isPending}
className={`w-5 h-5 mt-0.5 rounded border-2 flex items-center justify-center transition-colors flex-shrink-0 ${
item.is_complete
? 'bg-bambu-green border-bambu-green text-white'
? 'bg-status-ok border-status-ok text-white'
: 'border-bambu-gray hover:border-bambu-green'
}`}
>
@@ -1006,8 +1006,8 @@ export function ProjectDetailPage() {
{timeline.slice(0, 10).map((event, index) => (
<div key={index} className="flex gap-3">
<div className={`w-8 h-8 rounded-full flex items-center justify-center flex-shrink-0 ${
event.event_type === 'print_completed' ? 'bg-bambu-green/20 text-bambu-green' :
event.event_type === 'print_failed' ? 'bg-red-500/20 text-red-400' :
event.event_type === 'print_completed' ? 'bg-status-ok/20 text-status-ok' :
event.event_type === 'print_failed' ? 'bg-status-error/20 text-status-error' :
event.event_type === 'print_started' ? 'bg-yellow-500/20 text-yellow-400' :
'bg-bambu-dark-tertiary text-bambu-gray'
}`}>
+3 -3
View File
@@ -76,10 +76,10 @@ function formatRelativeTime(dateString: string | null, timeFormat: TimeFormat =
function StatusBadge({ status }: { status: PrintQueueItem['status'] }) {
const config = {
pending: { icon: Clock, color: 'text-yellow-400 bg-yellow-400/10 border-yellow-400/20', label: 'Pending' },
pending: { icon: Clock, color: 'text-status-warning bg-status-warning/10 border-status-warning/20', label: 'Pending' },
printing: { icon: Play, color: 'text-blue-400 bg-blue-400/10 border-blue-400/20', label: 'Printing' },
completed: { icon: CheckCircle, color: 'text-green-400 bg-green-400/10 border-green-400/20', label: 'Completed' },
failed: { icon: XCircle, color: 'text-red-400 bg-red-400/10 border-red-400/20', label: 'Failed' },
completed: { icon: CheckCircle, color: 'text-status-ok bg-status-ok/10 border-status-ok/20', label: 'Completed' },
failed: { icon: XCircle, color: 'text-status-error bg-status-error/10 border-status-error/20', label: 'Failed' },
skipped: { icon: SkipForward, color: 'text-orange-400 bg-orange-400/10 border-orange-400/20', label: 'Skipped' },
cancelled: { icon: X, color: 'text-gray-400 bg-gray-400/10 border-gray-400/20', label: 'Cancelled' },
};
+7 -7
View File
@@ -155,12 +155,12 @@ function SuccessRateWidget({
<div className="flex-1 min-w-0">
<div className="space-y-2">
<div className="flex items-center gap-2">
<CheckCircle className="w-4 h-4 text-bambu-green flex-shrink-0" />
<CheckCircle className="w-4 h-4 text-status-ok flex-shrink-0" />
<span className="text-sm text-bambu-gray">Successful:</span>
<span className="text-sm text-white font-medium">{stats?.successful_prints || 0}</span>
</div>
<div className="flex items-center gap-2">
<XCircle className="w-4 h-4 text-red-400 flex-shrink-0" />
<XCircle className="w-4 h-4 text-status-error flex-shrink-0" />
<span className="text-sm text-bambu-gray">Failed:</span>
<span className="text-sm text-white font-medium">{stats?.failed_prints || 0}</span>
</div>
@@ -276,8 +276,8 @@ function TimeAccuracyWidget({
{printerMap.get(printerId) || `Printer ${printerId}`}
</span>
<span className={`font-medium ${
acc >= 95 && acc <= 105 ? 'text-bambu-green' :
acc > 105 ? 'text-blue-400' : 'text-orange-400'
acc >= 95 && acc <= 105 ? 'text-status-ok' :
acc > 105 ? 'text-blue-400' : 'text-status-warning'
}`}>
{acc.toFixed(0)}%
</span>
@@ -452,7 +452,7 @@ function FailureAnalysisWidget({ size = 1 }: { size?: 1 | 2 | 4 }) {
<div className={size >= 2 ? 'flex-shrink-0' : ''}>
<div className="flex items-center gap-4">
<div className="flex items-center gap-2">
<AlertTriangle className={`w-5 h-5 ${analysis.failure_rate > 20 ? 'text-red-400' : analysis.failure_rate > 10 ? 'text-yellow-400' : 'text-bambu-green'}`} />
<AlertTriangle className={`w-5 h-5 ${analysis.failure_rate > 20 ? 'text-status-error' : analysis.failure_rate > 10 ? 'text-status-warning' : 'text-status-ok'}`} />
<span className={`font-bold text-white ${size >= 2 ? 'text-3xl' : 'text-2xl'}`}>{analysis.failure_rate.toFixed(1)}%</span>
</div>
</div>
@@ -465,8 +465,8 @@ function FailureAnalysisWidget({ size = 1 }: { size?: 1 | 2 | 4 }) {
<div className="flex items-center gap-2 text-sm">
<TrendingDown className={`w-4 h-4 ${
analysis.trend[analysis.trend.length - 1].failure_rate < analysis.trend[analysis.trend.length - 2].failure_rate
? 'text-bambu-green'
: 'text-red-400'
? 'text-status-ok'
: 'text-status-error'
}`} />
<span className="text-bambu-gray">
Last week: {analysis.trend[analysis.trend.length - 1].failure_rate.toFixed(1)}%
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -23,8 +23,8 @@
<!-- Splash screens for iOS -->
<link rel="apple-touch-startup-image" href="/img/android-chrome-512x512.png" />
<script type="module" crossorigin src="/assets/index-SsFUChPG.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-CVDQtTMh.css">
<script type="module" crossorigin src="/assets/index-oIN8PllK.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-DVKqcow3.css">
</head>
<body>
<div id="root"></div>