refactor(ui): the badge layer gets one owner per shape (#3132 items 1-3)
CI & Build / Python lint (push) Successful in 5s
CI & Build / Plugin hooks (push) Successful in 9s
CI & Build / integration (push) Successful in 33s
CI & Build / TypeScript typecheck (push) Successful in 40s
CI & Build / Python tests (push) Successful in 1m6s
CI & Build / Build & push image (push) Successful in 39s

ITEM 1 — the dead canon. StatusBadge is recorded canon (#2960) and its only
consumer, TaskCard, has been unreachable since 2026-04-08, when
TasksListView was deleted in favour of the Knowledge view. Four and a half
months of a canon that rendered nowhere, which is worse than no canon: a
session pulls #2960, builds from it, and matches a component nobody has
seen. TaskCard is deleted (rule 22), and the canon is made real by adoption
rather than by being left as a museum piece.

ITEM 2 — MY OWN ISSUE OVERSTATED THIS, and the correction is the finding.
"Three scoped re-spellings" assumed one shape spelled thrice. Reading them:

  KnowledgeView   a task-status chip, just smaller     -> a real duplicate
  WorkspaceTaskPanel  a CLICKABLE cycler: pointer,
                  outlined, transparent background     -> a control, not a chip
  ProjectView     PROJECT lifecycle (active/paused/
                  completed/archived)                  -> a different vocabulary

Only the first was ever a duplicate. The others shared a class NAME and
nothing else — which is exactly what would make a future consolidation merge
three unrelated things. So: KnowledgeView adopts StatusBadge/PriorityBadge
via the `compact` variant the canon already anticipated ("interactive/compact
re-spellings are variants of it"); the cycler becomes `.status-cycler`; and
project status becomes its own vocabulary.

And there was a FOURTH, in ProjectListView — the genuine duplicate of
ProjectView's project pill, differing by the amounts two hands differ by:
0.68rem vs 0.7rem, a 14% tint vs 15%, one bordered and one not. Both now use
one ProjectStatusBadge. `statusLabel` went with its only caller.

ITEM 3 — weight. StatusBadge and PriorityBadge used font-weight 600; the
house style allows 400 and 500 only. Also "In Progress" -> "In progress",
which was invisible under `text-transform: uppercase` and becomes visible the
moment the compact variant turns that off.

THE GUARD MISSED FOUR LIVE SITES, which is the part worth keeping. The
project pills painted a hue on an inline `color-mix` tint of itself —
measured 1.61-2.39:1 — and the checker only knew the `--fs-X-bg` token form.
Widened, it finds 48 across the app, 26 of them --fs-accent.

That backlog is not this task, so the check now splits: it GATES the token
form, which is clean, and REPORTS the inline form with a count and its worst
offenders. A gate nobody can satisfy today gets switched off, and then it
guards nothing. Gate re-verified by reintroducing a defect — exit 1 with it,
exit 0 without.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-27 20:41:58 -04:00
co-authored by Claude Opus 5
parent 0c74dc8275
commit ce1376edc9
9 changed files with 160 additions and 330 deletions
+2 -32
View File
@@ -2,6 +2,7 @@
import { ref, computed, onMounted } from "vue";
import { useRouter } from "vue-router";
import { apiGet, apiPost, apiErrorMessage } from "@/api/client";
import ProjectStatusBadge from "@/components/ProjectStatusBadge.vue";
import { emptyChoices, type InceptionChoices } from "@/api/inception";
import InceptionCard from "@/components/InceptionCard.vue";
import { useToastStore } from "@/stores/toast";
@@ -109,13 +110,6 @@ async function createProject() {
}
}
function statusLabel(status: Project["status"]): string {
if (status === "active") return "Active";
if (status === "paused") return "Paused";
if (status === "completed") return "Completed";
if (status === "archived") return "Archived";
return status;
}
function truncate(text: string | null, max = 120): string {
if (!text) return "";
@@ -210,9 +204,7 @@ function overallPct(project: Project): { total: number; pct: number } {
>
<div class="card-header">
<span class="project-title">{{ project.title }}</span>
<span
:class="['status-badge', `status-${project.status}`]"
>{{ statusLabel(project.status) }}</span>
<ProjectStatusBadge :status="project.status" />
</div>
<p v-if="project.goal" class="project-goal">
<span class="field-label">Goal:</span> {{ truncate(project.goal) }}
@@ -430,28 +422,6 @@ function overallPct(project: Project): { total: number; pct: number } {
word-break: break-word;
}
.status-badge {
font-size: 0.7rem;
font-weight: 500;
text-transform: uppercase;
letter-spacing: 0.04em;
padding: 0.15rem 0.45rem;
border-radius: 999px;
flex-shrink: 0;
white-space: nowrap;
}
.status-active {
background: color-mix(in srgb, var(--fs-success) 15%, transparent);
color: var(--fs-success);
}
.status-completed {
background: color-mix(in srgb, var(--fs-accent) 15%, transparent);
color: var(--fs-accent);
}
.status-archived {
background: color-mix(in srgb, var(--fs-text-tertiary) 15%, transparent);
color: var(--fs-text-tertiary);
}
.project-goal {
font-size: 0.875rem;