CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 8s
CI & Build / integration (push) Successful in 29s
CI & Build / TypeScript typecheck (push) Successful in 33s
CI & Build / Python tests (push) Successful in 1m4s
CI & Build / Build & push image (push) Successful in 36s
Every status and priority badge used its raw hue as TEXT on a 12% tint of that same hue. Measured on the dark palette, all six pairs failed the kit's own AA floor: todo 1.60:1, in-progress 1.97:1, done 2.06:1, low 2.02:1, high 2.92:1, medium 2.97:1, against 4.5. Four also failed in light mode. The cause is structural, not a bad colour pick. A 12% tint sits near the surface it composites over, so the hue as text on it has almost nowhere to go. Strengthening the tint was measured and REJECTED: on a dark palette a heavier tint moves the chip toward the light text and makes it worse. 12% was already optimal. So each pair gains a `-fg` sibling: the hue mixed toward --fs-text-primary until it clears 4.5:1 worst-case over surface-raised AND surface-hover in BOTH modes. Mixing toward that token rather than a literal is what makes one declaration cover both — it inverts by mode, so the text follows. Recorded in the DESIGN SYSTEM, not hand-written into theme.css: seven tokens on design system 2, each carrying its measurement and its reasoning, then the sheet regenerated. theme.css says not to hand-edit the --fs-* block and it is right — a hand-edit would be silently reverted by the next regeneration. The ladder keeps its shape. High priority still holds 52% saturation and medium 31% — the rungs that need to shout still shout. Low, todo and done wash toward neutral, which is what their own rationales ask for: status-todo is derived from the border colour precisely so not-yet-started recedes. Receding and illegible are different things and the old value was the second. --fs-status-cancelled-fg was found by measuring, not by reasoning. Cancelled derives from --fs-text-tertiary, which looks like the obviously-correct "quiet" choice and is a HINT colour tuned for plain surfaces — 2.63:1 on a badge tint in light mode. StatusBadge additionally dropped a `color-mix(..., #000 15%)` that darkened the hue: a light-mode instinct that made these worse on a near-black surface, and a literal besides. THE GUARD IS THE POINT. check_design_tokens.py now FAILS on any rule that paints text with a token on a tint of that same token, and names the -fg sibling as the fix. Verified by reintroducing the defect: exit 1 with it, exit 0 without. Unlike a raw literal there is nothing to weigh up, so it gates rather than reports. Two `border-top-color` uses keep the raw hue, correctly — a border is a non-text graphic and needs 3:1, which is what the hue is for. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
235 lines
6.9 KiB
Vue
235 lines
6.9 KiB
Vue
<script setup lang="ts">
|
|
/**
|
|
* A project's own code, checked against the design system it is bound to (#2432).
|
|
*
|
|
* This is what the design surface is FOR: a project's recorded components
|
|
* measured against the sheet they are supposed to use. The check itself is not
|
|
* new — `check_snippets_against_system` has taken a project id since it was
|
|
* written, and the route has always read `?project_id=`. Nothing on this side
|
|
* ever passed one, so the capability shipped and stayed unreachable.
|
|
*
|
|
* The finding that matters most is the quiet one. `local_definitions` is a
|
|
* snippet minting its own custom property instead of reaching for the shared
|
|
* one — the codebase re-solving a solved problem, one component at a time.
|
|
* Nothing breaks, no test fails, and the duplication only becomes visible when
|
|
* someone changes the shared value and half the components don't move.
|
|
*
|
|
* SCOPE, and it is a limit rather than an omission: this reads RECORDED code —
|
|
* snippets — because that is the code Scribe holds. A repository's own sources
|
|
* are checked where they live, by that project's CI.
|
|
*/
|
|
import { onMounted, ref, watch } from "vue";
|
|
|
|
import { checkSnippets, type SnippetCheck } from "@/api/designSystems";
|
|
|
|
const props = defineProps<{ projectId: number; designSystemId: number | null }>();
|
|
|
|
const check = ref<SnippetCheck | null>(null);
|
|
const loading = ref(false);
|
|
const failed = ref(false);
|
|
|
|
async function run() {
|
|
check.value = null;
|
|
failed.value = false;
|
|
if (props.designSystemId === null) return;
|
|
loading.value = true;
|
|
try {
|
|
check.value = await checkSnippets(props.designSystemId, props.projectId);
|
|
} catch {
|
|
// Said out loud rather than rendered as an empty result. "Couldn't check"
|
|
// and "nothing to report" look identical if you let them, and that is how
|
|
// a check comes to sit dead without anyone noticing (#2419).
|
|
failed.value = true;
|
|
} finally {
|
|
loading.value = false;
|
|
}
|
|
}
|
|
|
|
onMounted(run);
|
|
watch(() => [props.projectId, props.designSystemId], run);
|
|
</script>
|
|
|
|
<template>
|
|
<div class="pdt">
|
|
<div v-if="designSystemId === null" class="pdt-note">
|
|
<strong>No design system for this project.</strong>
|
|
<p>
|
|
Bind one in the sidebar and this tab reports where the project's recorded
|
|
components disagree with it — references to tokens the system doesn't
|
|
have, literals it says to stop writing, and properties a component mints
|
|
for itself instead of reusing.
|
|
</p>
|
|
</div>
|
|
|
|
<p v-else-if="loading" class="pdt-muted">Checking this project's snippets…</p>
|
|
|
|
<div v-else-if="failed" class="pdt-note">
|
|
<strong>The check couldn't run.</strong>
|
|
<p>Nothing was compared — this is a failure, not a clean result.</p>
|
|
</div>
|
|
|
|
<template v-else-if="check">
|
|
<p v-if="!check.checked" class="pdt-muted">
|
|
This project has no recorded snippets, so nothing was checked. Record the
|
|
components you reuse and they get measured against the sheet.
|
|
</p>
|
|
|
|
<p v-else-if="!check.findings.length" class="pdt-clean">
|
|
{{ check.checked }} snippet{{ check.checked === 1 ? "" : "s" }} checked —
|
|
every reference resolves, and none mints a property of its own.
|
|
</p>
|
|
|
|
<template v-else>
|
|
<p class="pdt-summary">
|
|
<strong>{{ check.findings.length }}</strong> of {{ check.checked }}
|
|
snippet{{ check.checked === 1 ? "" : "s" }} disagree with the sheet.
|
|
</p>
|
|
|
|
<ul class="pdt-list">
|
|
<li v-for="f in check.findings" :key="f.snippet_id" class="pdt-finding">
|
|
<router-link :to="`/snippets/${f.snippet_id}`" class="pdt-title">
|
|
{{ f.title || "Untitled snippet" }}
|
|
</router-link>
|
|
|
|
<!-- Renders as nothing at all: no error, no failing test, just an
|
|
element that quietly isn't styled. Leads for that reason. -->
|
|
<div v-if="f.unknown.length" class="pdt-row">
|
|
<span class="pdt-tag unknown">no such token</span>
|
|
<span class="pdt-detail">
|
|
<code v-for="name in f.unknown" :key="name">{{ name }}</code>
|
|
</span>
|
|
</div>
|
|
|
|
<div v-if="f.local_definitions.length" class="pdt-row">
|
|
<span class="pdt-tag local">defines its own</span>
|
|
<span class="pdt-detail">
|
|
<code v-for="name in f.local_definitions" :key="name">{{ name }}</code>
|
|
</span>
|
|
</div>
|
|
|
|
<div v-if="f.superseded_literals.length" class="pdt-row">
|
|
<span class="pdt-tag superseded">write the token</span>
|
|
<span class="pdt-detail">
|
|
<span v-for="s in f.superseded_literals" :key="s.literal" class="pdt-swap">
|
|
<code>{{ s.literal }}</code> → <code>{{ s.use_instead }}</code>
|
|
</span>
|
|
</span>
|
|
</div>
|
|
</li>
|
|
</ul>
|
|
</template>
|
|
</template>
|
|
</div>
|
|
</template>
|
|
|
|
<style scoped>
|
|
.pdt {
|
|
padding: var(--fs-space-2) 0;
|
|
}
|
|
|
|
.pdt-note {
|
|
background: var(--fs-surface-hover);
|
|
border: 1px solid var(--fs-border-color);
|
|
border-left: 3px solid var(--fs-warning);
|
|
border-radius: var(--fs-radius-sm);
|
|
padding: var(--fs-space-3) var(--fs-space-4);
|
|
}
|
|
|
|
.pdt-note p {
|
|
margin: var(--fs-space-2) 0 0;
|
|
color: var(--fs-text-secondary);
|
|
font-size: var(--fs-size-body-sm);
|
|
line-height: var(--fs-leading-body);
|
|
max-width: 70ch;
|
|
}
|
|
|
|
.pdt-muted,
|
|
.pdt-clean,
|
|
.pdt-summary {
|
|
color: var(--fs-text-tertiary);
|
|
font-size: var(--fs-size-body-sm);
|
|
margin: 0 0 var(--fs-space-3);
|
|
max-width: 70ch;
|
|
}
|
|
|
|
.pdt-clean {
|
|
color: var(--fs-status-done-fg);
|
|
}
|
|
|
|
.pdt-summary {
|
|
color: var(--fs-text-secondary);
|
|
}
|
|
|
|
.pdt-list {
|
|
list-style: none;
|
|
padding: 0;
|
|
margin: 0;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: var(--fs-space-3);
|
|
}
|
|
|
|
.pdt-finding {
|
|
border: 1px solid var(--fs-border-color);
|
|
border-radius: var(--fs-radius-md);
|
|
padding: var(--fs-space-3);
|
|
min-width: 0;
|
|
}
|
|
|
|
.pdt-title {
|
|
display: block;
|
|
font-weight: var(--fs-weight-medium);
|
|
color: var(--fs-text-primary);
|
|
text-decoration: none;
|
|
margin-bottom: var(--fs-space-2);
|
|
}
|
|
.pdt-title:hover { color: var(--fs-accent); }
|
|
|
|
.pdt-row {
|
|
display: flex;
|
|
align-items: baseline;
|
|
gap: var(--fs-space-2);
|
|
flex-wrap: wrap;
|
|
padding: 0.15rem 0;
|
|
min-width: 0;
|
|
}
|
|
|
|
.pdt-tag {
|
|
font-size: var(--fs-size-tiny);
|
|
text-transform: uppercase;
|
|
letter-spacing: var(--fs-tracking-tiny);
|
|
padding: 0.1rem 0.45rem;
|
|
border-radius: var(--fs-radius-sm);
|
|
white-space: nowrap;
|
|
flex: none;
|
|
}
|
|
|
|
.pdt-tag.unknown {
|
|
background: var(--fs-priority-high-bg);
|
|
color: var(--fs-priority-high-fg);
|
|
}
|
|
|
|
.pdt-tag.local {
|
|
background: var(--fs-priority-medium-bg);
|
|
color: var(--fs-priority-medium-fg);
|
|
}
|
|
|
|
.pdt-tag.superseded {
|
|
background: var(--fs-surface-hover);
|
|
color: var(--fs-text-tertiary);
|
|
}
|
|
|
|
.pdt-detail {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: var(--fs-space-2);
|
|
font-size: var(--fs-size-code);
|
|
color: var(--fs-text-secondary);
|
|
min-width: 0;
|
|
}
|
|
|
|
.pdt-swap {
|
|
white-space: nowrap;
|
|
}
|
|
</style>
|