refactor(ui): retire the raw dracula utilities for semantic tokens

Migrate every remaining *-dracula-* utility site (127 lint warnings,
193 class occurrences) onto the semantic vocabulary. The swaps are
pixel-identical in both themes — each target token aliases the same
primitive the utility read, including the light-theme AA overrides:

- green/orange/cyan/red → success/warning/info/destructive
- purple → primary (same value in both themes)
- pink → the new --brand-pink token (Coach gradient, Apple-Health
  badge, recording mic): dark #ff79c6 — 6.86:1 card / 5.97:1 bg;
  light #a3134d — 7.32:1 card / 6.83:1 bg (computed)
- yellow sites resolve to their status meaning: warn-level log icon,
  update-available pill and offline-geo hint → warning; the admin
  helpful-rate ramp → success/warning/destructive; the confidence
  meter mirrors the coverage-meter precedent (medium + low share the
  caution tint, the lit count carries the distinction)
- the confidence-badge low tier dropped dead *-dracula-comment/*
  classes (no generated utility) for border-border + bg-muted/50

Close two rule blind spots the sweep surfaced — directional borders
(border-l-dracula-red) and the parenthesised CSS-var shorthand
(bg-(--dracula-green)) — and flip healthlog/no-dracula-utility from
warn to error so the class cannot regrow. var(--dracula-*) references
in chart code stay: they are token references the light theme
overrides, and the chart palette is pinned visually.
This commit is contained in:
Marc-André Bombeck committed 2026-07-05 21:28:09 +02:00
1 parent 68b4dd08a7
commit a4b204ffe4
59 files changed
+272 -214

No files matched your search

@@ -89,9 +89,9 @@ describe("CoachFeedbackSection", () => {
// Two bucket rows in the table.
const matches = (html.match(/coach-feedback-bucket/g) ?? []).length;
expect(matches).toBe(2);
// Helpful-rate column carries the band tint.
expect(html).toContain("text-dracula-green"); // 80% bucket
expect(html).toContain("text-dracula-yellow"); // 50% bucket
// Helpful-rate column carries the band tint (semantic urgency ramp).
expect(html).toContain("text-success"); // 80% bucket
expect(html).toContain("text-warning"); // 50% bucket
// Numbers visible in the markup.
expect(html).toContain("80%");
expect(html).toContain("50%");
+8 -5
View File
@@ -122,7 +122,7 @@ export interface ApiTokenInfo {
export function ConfiguredBadge() {
const { t } = useTranslations();
return (
<Badge className="border-dracula-green/30 bg-dracula-green/15 text-dracula-green">
<Badge className="border-success/30 bg-success/15 text-success">
{t("admin.configured")}
</Badge>
);
@@ -488,11 +488,14 @@ export function useSystemStatus() {
}
/** Helpful-rate tint shared by the AI-quality + Coach-feedback tables.
* Green ≥80 %, yellow ≥50 %, orange below. */
* The standard urgency ramp (the medication-status-pill precedent):
* ≥80 % success, ≥50 % warning, below destructive — a sub-50 % helpful
* rate is a genuine quality alarm, and the percent label always carries
* the exact value alongside the tint. */
export function helpfulRateColour(rate: number): string {
if (rate >= 0.8) return "text-dracula-green";
if (rate >= 0.5) return "text-dracula-yellow";
return "text-dracula-orange";
if (rate >= 0.8) return "text-success";
if (rate >= 0.5) return "text-warning";
return "text-destructive";
}
export interface PublicVersion {
+4 -4
View File
@@ -46,10 +46,10 @@ interface FeedbackSummary {
}
const SEVERITY_TINT: Record<string, string> = {
info: "text-dracula-cyan",
suggestion: "text-dracula-purple",
important: "text-dracula-orange",
urgent: "text-dracula-red",
info: "text-info",
suggestion: "text-primary",
important: "text-warning",
urgent: "text-destructive",
};
export function AiQualitySection() {
@@ -92,9 +92,7 @@ function TokenStatusBadge({
);
}
return (
<Badge
className={`bg-dracula-green/15 text-dracula-green shrink-0 ${sizeClass}`}
>
<Badge className={`bg-success/15 text-success shrink-0 ${sizeClass}`}>
{t("common.active")}
</Badge>
);
@@ -75,12 +75,12 @@ function levelIcon(level: LogLevel) {
case "error":
return <AlertCircle className="text-destructive h-4 w-4" />;
case "warn":
return <AlertTriangle className="text-dracula-yellow h-4 w-4" />;
return <AlertTriangle className="text-warning h-4 w-4" />;
case "debug":
return <Info className="text-muted-foreground h-4 w-4" />;
case "info":
default:
return <CheckCircle2 className="text-dracula-green h-4 w-4" />;
return <CheckCircle2 className="text-success h-4 w-4" />;
}
}
+1 -1
View File
@@ -171,7 +171,7 @@ export function DangerZoneSection() {
{wipeMsg && (
<p
className={`mt-3 pl-7 text-sm ${
wipeAllData.isError ? "text-destructive" : "text-dracula-green"
wipeAllData.isError ? "text-destructive" : "text-success"
}`}
>
{wipeMsg}
+11 -11
View File
@@ -145,20 +145,20 @@ const STATUS_STYLES: Record<InviteStatus, { dot: string; chip: string }> = {
// Quiet tinted chips with a colour dot — the dot carries the state at
// a glance, the tint stays low so a table of mixed states reads calm.
active: {
dot: "bg-dracula-green",
chip: "border-dracula-green/30 bg-dracula-green/10 text-dracula-green",
dot: "bg-success",
chip: "border-success/30 bg-success/10 text-success",
},
expired: {
dot: "bg-muted-foreground/60",
chip: "border-border bg-muted/40 text-muted-foreground",
},
exhausted: {
dot: "bg-dracula-cyan",
chip: "border-dracula-cyan/30 bg-dracula-cyan/10 text-dracula-cyan",
dot: "bg-info",
chip: "border-info/30 bg-info/10 text-info",
},
revoked: {
dot: "bg-dracula-red",
chip: "border-dracula-red/30 bg-dracula-red/10 text-dracula-red",
dot: "bg-destructive",
chip: "border-destructive/30 bg-destructive/10 text-destructive",
},
};
@@ -451,7 +451,7 @@ export function InviteTokensSection() {
type="button"
variant="ghost"
size="sm"
className="text-dracula-red hover:text-dracula-red"
className="text-destructive hover:text-destructive"
aria-label={t("admin.invites.revokeAria")}
disabled={revoke.isPending}
onClick={() => setRevokeTarget(invite)}
@@ -519,7 +519,7 @@ export function InviteTokensSection() {
type="button"
variant="ghost"
size="sm"
className="text-dracula-red hover:text-dracula-red"
className="text-destructive hover:text-destructive"
disabled={revoke.isPending}
onClick={() => setRevokeTarget(invite)}
>
@@ -582,7 +582,7 @@ export function InviteTokensSection() {
className={cn(
"min-h-11 rounded-lg border px-3 py-2 text-sm font-medium transition-colors",
ttlDays === days
? "border-dracula-purple/50 bg-dracula-purple/10 text-dracula-purple"
? "border-primary/50 bg-primary/10 text-primary"
: "border-border text-foreground hover:bg-accent",
)}
>
@@ -647,7 +647,7 @@ export function InviteTokensSection() {
</DialogHeader>
{/* The ticket — dashed edge marks the one-time nature. */}
<div
className="border-dracula-purple/40 bg-background rounded-xl border border-dashed p-4"
className="border-primary/40 bg-background rounded-xl border border-dashed p-4"
data-testid="admin-invites-minted"
>
<div className="flex flex-col items-center gap-4">
@@ -671,7 +671,7 @@ export function InviteTokensSection() {
>
{copied ? (
<Check
className="text-dracula-green size-4"
className="text-success size-4"
aria-hidden="true"
/>
) : (
@@ -464,7 +464,7 @@ export function LoginOverviewSection() {
{isFailed ? (
<XCircle className="text-destructive h-4 w-4" />
) : (
<CheckCircle2 className="text-dracula-green h-4 w-4" />
<CheckCircle2 className="text-success h-4 w-4" />
)}
</td>
<td
@@ -503,7 +503,7 @@ export function LoginOverviewSection() {
</span>
{copiedIp === entry.id ? (
<Check
className="text-dracula-green h-3 w-3 shrink-0"
className="text-success h-3 w-3 shrink-0"
aria-hidden="true"
/>
) : (
@@ -115,7 +115,7 @@ export function RecentAuditPreview() {
{isFailed ? (
<XCircle className="text-destructive h-4 w-4" />
) : (
<CheckCircle2 className="text-dracula-green h-4 w-4" />
<CheckCircle2 className="text-success h-4 w-4" />
)}
</span>
<span
+6 -10
View File
@@ -70,7 +70,7 @@ export function SystemStatusSection() {
}
className={
status.database === "connected"
? "text-dracula-green"
? "text-success"
: "text-destructive"
}
/>
@@ -108,9 +108,7 @@ export function SystemStatusSection() {
: t("admin.workerStopped")
}
className={
status.worker.running
? "text-dracula-green"
: "text-destructive"
status.worker.running ? "text-success" : "text-destructive"
}
/>
{status.worker.lastReminderCheck && (
@@ -131,7 +129,7 @@ export function SystemStatusSection() {
}
className={
status.integrations.umami.enabled
? "text-dracula-green"
? "text-success"
: "text-destructive"
}
/>
@@ -147,7 +145,7 @@ export function SystemStatusSection() {
}
className={
status.integrations.glitchtip.enabled
? "text-dracula-green"
? "text-success"
: "text-destructive"
}
/>
@@ -157,7 +155,7 @@ export function SystemStatusSection() {
icon={BellRing}
label={t("admin.integrationWebPush")}
value={t("admin.configured")}
className="text-dracula-green"
className="text-success"
/>
)}
{/* v1.4.27 R5 — offline GeoLite2 availability. Renders only
@@ -175,9 +173,7 @@ export function SystemStatusSection() {
})
}
className={
version.offlineGeoEnabled
? "text-dracula-green"
: "text-dracula-yellow"
version.offlineGeoEnabled ? "text-success" : "text-warning"
}
/>
)}
@@ -61,7 +61,7 @@ export function SystemStatusSummary() {
}
className={
status.database === "connected"
? "text-dracula-green"
? "text-success"
: "text-destructive"
}
/>
@@ -74,7 +74,7 @@ export function SystemStatusSummary() {
: t("admin.workerStopped")
}
className={
status.worker.running ? "text-dracula-green" : "text-destructive"
status.worker.running ? "text-success" : "text-destructive"
}
/>
<StatusItem
@@ -112,9 +112,7 @@ export function SystemStatusSummary() {
})
}
className={
version.offlineGeoEnabled
? "text-dracula-green"
: "text-dracula-yellow"
version.offlineGeoEnabled ? "text-success" : "text-warning"
}
/>
)}
@@ -190,7 +190,7 @@ export function UserManagementSection() {
<Button
variant="ghost"
size="sm"
className={`min-h-11 min-w-11 px-3 text-xs ${u.mfaEnforced ? "text-dracula-green" : ""}`}
className={`min-h-11 min-w-11 px-3 text-xs ${u.mfaEnforced ? "text-success" : ""}`}
onClick={() =>
updateUser.mutate({
id: u.id,
@@ -554,7 +554,7 @@ export function UserManagementSection() {
</div>
{resetMsg && (
<p
className={`text-sm ${resetMsg === t("admin.passwordReset") ? "text-dracula-green" : "text-destructive"}`}
className={`text-sm ${resetMsg === t("admin.passwordReset") ? "text-success" : "text-destructive"}`}
>
{resetMsg}
</p>
@@ -97,7 +97,7 @@ export function VersionTileSection() {
}
target="_blank"
rel="noopener noreferrer"
className="bg-dracula-yellow/15 text-dracula-yellow border-dracula-yellow/30 inline-flex items-center gap-1.5 rounded-full border px-2.5 py-1 text-xs font-medium transition-opacity hover:opacity-80"
className="bg-warning/15 text-warning border-warning/30 inline-flex items-center gap-1.5 rounded-full border px-2.5 py-1 text-xs font-medium transition-opacity hover:opacity-80"
>
<ArrowUpCircle className="h-3.5 w-3.5" aria-hidden="true" />
{t("admin.overview.versionTileUpdateAvailable", {
@@ -106,7 +106,7 @@ export function VersionTileSection() {
})}
</a>
) : upToDate ? (
<span className="text-dracula-green inline-flex items-center gap-1.5 text-xs font-medium">
<span className="text-success inline-flex items-center gap-1.5 text-xs font-medium">
<CheckCircle2 className="h-3.5 w-3.5" aria-hidden="true" />
{t("admin.overview.versionTileUpToDate")}
</span>