Skip to content

Commit 2a2b4ff

Browse files
committed
improve status bar
1 parent a398358 commit 2a2b4ff

3 files changed

Lines changed: 97 additions & 14 deletions

File tree

Keypad.Flasher.Client/src/KeypadFlasherApp.tsx

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/// <reference types="w3c-web-usb" />
2-
import { useCallback, useEffect, useMemo, useRef, useState, type ChangeEvent, type DragEvent } from "react";
2+
import { useCallback, useEffect, useMemo, useRef, useState, type ChangeEvent, type DragEvent, type ReactNode } from "react";
33
import {
44
CH55xBootloader,
55
FakeBootloader,
@@ -1226,7 +1226,7 @@ export default function KeypadFlasherApp() {
12261226
const warnNoBootEntry = Boolean(selectedLayout && bootloaderOnBootCount === 0 && bootloaderChordCount === 0);
12271227
const warnSingleChord = Boolean(selectedLayout && bootloaderChordCount === 1);
12281228

1229-
const statusBanner = (() => {
1229+
const statusBanner: { tone: "info" | "success" | "warn" | "error"; title: string; body?: ReactNode; showSpinner?: boolean } | null = (() => {
12301230
switch (status.state) {
12311231
case "requesting":
12321232
return { tone: "info" as const, title: "Requesting device…", body: "Approve the WebUSB prompt to continue." };
@@ -1235,7 +1235,7 @@ export default function KeypadFlasherApp() {
12351235
case "connectedUnknown":
12361236
return { tone: "warn" as const, title: "Device not recognized", body: "Use debug firmware or pick a supported layout." };
12371237
case "compiling":
1238-
return { tone: "info" as const, title: "Compiling firmware…", body: status.detail ? `Building ${status.detail}.` : undefined };
1238+
return { tone: "info" as const, title: "Compiling firmware…", body: status.detail ? `Building ${status.detail}.` : undefined, showSpinner: true };
12391239
case "unsupported":
12401240
return { tone: "warn" as const, title: "Unknown device", body: status.detail };
12411241
case "flashing":
@@ -1259,10 +1259,12 @@ export default function KeypadFlasherApp() {
12591259
case "error":
12601260
return { tone: "error" as const, title: "Error", body: status.detail };
12611261
default:
1262-
return null;
1262+
return null;
12631263
}
12641264
})();
12651265

1266+
const statusProgress = progress.total > 0 ? progress : null;
1267+
12661268
return (
12671269
<div className="app-shell">
12681270
<div className="container">
@@ -1353,14 +1355,13 @@ export default function KeypadFlasherApp() {
13531355
)}
13541356

13551357
{statusBanner && (
1356-
<StatusBanner tone={statusBanner.tone} title={statusBanner.title} body={statusBanner.body} />
1357-
)}
1358-
1359-
{progress.total > 0 && (
1360-
<div className="progress">
1361-
<div className="progress-bar" style={{ width: `${Math.round((progress.current / progress.total) * 100)}%` }} />
1362-
<div className="muted small mt-1">{progress.phase} {progress.current} / {progress.total}</div>
1363-
</div>
1358+
<StatusBanner
1359+
tone={statusBanner.tone}
1360+
title={statusBanner.title}
1361+
body={statusBanner.body}
1362+
showSpinner={statusBanner.showSpinner}
1363+
progress={statusProgress}
1364+
/>
13641365
)}
13651366
</div>
13661367

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,35 @@
11
import type { ReactNode } from "react";
2+
import type { Progress } from "../lib/ch55x-bootloader";
23

34
type StatusTone = "info" | "success" | "warn" | "error";
45

56
type StatusBannerProps = {
67
tone: StatusTone;
78
title: string;
89
body?: ReactNode;
10+
progress?: Progress | null;
11+
showSpinner?: boolean;
912
};
1013

11-
export function StatusBanner({ tone, title, body }: StatusBannerProps) {
14+
export function StatusBanner({ tone, title, body, progress, showSpinner }: StatusBannerProps) {
15+
const hasProgress = Boolean(progress && progress.total > 0);
16+
const percent = hasProgress && progress ? Math.round((progress.current / progress.total) * 100) : 0;
17+
1218
return (
1319
<div className={`status-banner status-${tone}`}>
14-
<div className="status-title">{title}</div>
20+
<div className="status-header">
21+
<div className="status-title">{title}</div>
22+
{showSpinner && <span className="status-spinner" role="status" aria-live="polite" aria-label="Working" />}
23+
</div>
1524
{body && <div className="status-body">{body}</div>}
25+
{hasProgress && progress && (
26+
<div className="status-progress-block">
27+
<div className="status-progress">
28+
<div className="status-progress-bar" style={{ width: `${percent}%` }} />
29+
</div>
30+
<div className="status-progress-meta">{progress.phase} {progress.current} / {progress.total}</div>
31+
</div>
32+
)}
1633
</div>
1734
);
1835
}

Keypad.Flasher.Client/src/styles/base.css

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -516,6 +516,16 @@ input[type="color"] {
516516
background: var(--info-bg);
517517
color: var(--info-text);
518518
box-shadow: var(--shadow);
519+
--status-progress-track: rgba(0, 0, 0, 0.08);
520+
--status-progress-start: var(--progress-start);
521+
--status-progress-end: var(--progress-end);
522+
}
523+
524+
.status-header {
525+
display: flex;
526+
align-items: center;
527+
justify-content: space-between;
528+
gap: 10px;
519529
}
520530

521531
.status-title {
@@ -539,22 +549,77 @@ input[type="color"] {
539549
word-break: break-word;
540550
}
541551

552+
.status-spinner {
553+
display: inline-block;
554+
width: 16px;
555+
height: 16px;
556+
border-radius: 999px;
557+
border: 2px solid transparent;
558+
border-top-color: currentColor;
559+
border-right-color: currentColor;
560+
animation: status-spin 0.8s linear infinite;
561+
flex-shrink: 0;
562+
}
563+
564+
@keyframes status-spin {
565+
to { transform: rotate(360deg); }
566+
}
567+
568+
.status-progress-block {
569+
display: flex;
570+
flex-direction: column;
571+
gap: 6px;
572+
margin-top: 10px;
573+
}
574+
575+
.status-progress {
576+
width: 100%;
577+
height: 10px;
578+
border-radius: 999px;
579+
background: var(--status-progress-track);
580+
overflow: hidden;
581+
}
582+
583+
.status-progress-bar {
584+
height: 100%;
585+
border-radius: 999px;
586+
background: linear-gradient(90deg, var(--status-progress-start), var(--status-progress-end));
587+
}
588+
589+
.status-progress-meta {
590+
font-size: 12px;
591+
color: inherit;
592+
opacity: 0.85;
593+
}
594+
595+
@media (prefers-color-scheme: dark) {
596+
.status-banner {
597+
--status-progress-track: rgba(255, 255, 255, 0.14);
598+
}
599+
}
600+
542601
.status-success {
543602
border-color: var(--success-border);
544603
background: var(--success-bg);
545604
color: var(--success-text);
605+
--status-progress-start: #0ea15a;
606+
--status-progress-end: #34d399;
546607
}
547608

548609
.status-warn {
549610
border-color: var(--warn-border);
550611
background: var(--warn-bg);
551612
color: var(--warn-text);
613+
--status-progress-start: #d97706;
614+
--status-progress-end: #fbbf24;
552615
}
553616

554617
.status-error {
555618
border-color: var(--error-border);
556619
background: var(--error-bg);
557620
color: var(--error-text);
621+
--status-progress-start: #dc2626;
622+
--status-progress-end: #f87171;
558623
}
559624

560625
.clickable {

0 commit comments

Comments
 (0)