Files
FabledScribe/frontend/src/components/ProjectDesignTab.vue
T
bvandeusen 4ba544e2af
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 7s
CI & Build / integration (push) Successful in 17s
CI & Build / TypeScript typecheck (push) Successful in 34s
CI & Build / Python tests (push) Successful in 48s
CI & Build / Build & push image (push) Successful in 37s
refactor(theme): retire the --color-* shim — the sweep it promised, run
#2533. theme.css claimed "removing this block is a rename sweep across the
components, tracked separately" — written in 67a529a, never filed, which made
the comment itself an instance of the survey's presence-without-reference
pattern. This is that sweep.

73 alias declarations deleted; 69 files rewritten; every --color-*-style name
now references its --fs-* token directly. Mechanical by construction: the map
IS the alias block, applied longest-name-first with a boundary guard so
--color-text never matched inside --color-text-muted. Zero survivors outside
theme.css, verified by grep rather than assumed.

One deliberate survivor: --color-shadow stays DECLARED, because it was never
an alias — it is a literal value the design system has no token for. Marked
in place as a recorded gap: promote it to an --fs-* token when a second app
needs it, don't copy the line.

Nothing is lost mode-wise: the aliases' resolve-at-use-time trick (which
absorbed 48 dark-mode overrides) lives one layer down in the --fs-* tokens'
own derivations, which is why the sweep is a pure rename. Both CSS checkers
green.

Why now rather than never: check_snippets_against_design_system reports every
--color-* reference as "unknown — renders as NOTHING", and nine recipe
snippets recorded from components.css carried the deprecated names, making
them prior art pointing the wrong way. With the sweep in, the checker's
report over re-recorded snippets should be EMPTY — the acceptance test that
proves the checker was right all along (#2517's correction).

Refs #2533
2026-08-08 22:42:37 -04:00

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);
}
.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);
}
.pdt-tag.local {
background: var(--fs-priority-medium-bg);
color: var(--fs-priority-medium);
}
.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>