Files
SnapOtter/apps/web/src/components/common/progress-card.tsx
T
SnapOtterandGitHub 51022628dc fix(a11y): WCAG AA contrast retune for the Otter Orange palette (#567)
Fixes #557. Vivid fill, ink label: brand #E07832 stays on fills while primary-foreground flips to #1A1814 (5.83:1); new theme-aware ink tokens carry orange, destructive, and success text roles; opacity-modified text purged; landing, demo, and the docs fund button retuned. Guarded by a CSS-parsing unit contrast test, rebuilt axe baselines with zero contrast entries, a new landing axe smoke, and fully regenerated darwin visual baselines.
2026-07-18 12:56:48 +08:00

77 lines
2.6 KiB
TypeScript

import { Loader2, Upload, X } from "lucide-react";
import { useState } from "react";
import { useTranslation } from "@/contexts/i18n-context";
import { useFileStore } from "@/stores/file-store";
interface ProgressCardProps {
active: boolean;
phase: "uploading" | "processing" | "complete";
label: string;
stage?: string;
percent: number;
elapsed: number;
}
export function ProgressCard({ active, phase, label, stage, percent, elapsed }: ProgressCardProps) {
const { t } = useTranslation();
const activeJobId = useFileStore((s) => s.activeJobId);
const cancelCurrentJob = useFileStore((s) => s.cancelCurrentJob);
const [canceling, setCanceling] = useState(false);
if (!active) return null;
const icon =
phase === "uploading" ? (
<Upload className="h-4 w-4 text-primary" />
) : (
<Loader2 className="h-4 w-4 text-primary animate-spin" />
);
const sublabel = [stage, `${elapsed}s`].filter(Boolean).join(" · ");
return (
<div
role="status"
aria-live="polite"
className="bg-muted/80 border border-border rounded-xl p-3 space-y-2.5"
>
<div className="flex items-center gap-2.5">
<div className="w-8 h-8 rounded-lg bg-primary/10 flex items-center justify-center shrink-0">
{icon}
</div>
<div className="flex-1 min-w-0">
<div className="text-sm font-medium text-foreground truncate">{label}</div>
<div className="text-[11px] text-muted-foreground truncate">{sublabel}</div>
</div>
<span className="text-sm font-semibold text-primary-ink font-mono tabular-nums">
{Math.round(percent)}%
</span>
</div>
<div className="w-full h-1 bg-muted rounded-full overflow-hidden">
<div
className="h-full bg-primary rounded-full transition-all duration-500 ease-out"
style={{ width: `${Math.min(100, percent)}%` }}
/>
</div>
{activeJobId && cancelCurrentJob && (
<button
type="button"
disabled={canceling}
onClick={async () => {
setCanceling(true);
try {
await cancelCurrentJob();
} finally {
setCanceling(false);
}
}}
className="w-full py-1.5 rounded-lg border border-border text-xs text-muted-foreground hover:text-foreground hover:bg-muted flex items-center justify-center gap-1.5 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
<X className="h-3 w-3" />
{t.common.cancel}
</button>
)}
</div>
);
}