refactor(design): retire /design — a surface that could only inspect itself
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 9s
CI & Build / integration (push) Successful in 29s
CI & Build / TypeScript typecheck (push) Successful in 33s
CI & Build / Python tests (push) Successful in 59s
CI & Build / Build & push image (push) Successful in 40s
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 9s
CI & Build / integration (push) Successful in 29s
CI & Build / TypeScript typecheck (push) Successful in 33s
CI & Build / Python tests (push) Successful in 59s
CI & Build / Build & push image (push) Successful in 40s
The design surface is for the projects an install tracks. /design read the running app's own stylesheet — names out of a bundled theme.css, values out of getComputedStyle(document.documentElement) — so it could only ever describe the instance serving the page. Scribe is one project among the projects Scribe tracks; it gets no view hardcoded into every install. The mechanism that makes this a tool rather than a mirror already existed and already covers Scribe: scripts/check_design_tokens.py runs in CI against a sheet path it knows nothing about, using check_code_against_tokens — the same engine behind check_snippets_against_system. /design was redundant even here. Removed: DesignView, DesignTabs (nothing left to tab between), api/design.ts, routes/design.py and its blueprint, the /design route, ui_design_system() and its setting, and the Settings picker that designated "this app's UI". utils/designTokens.ts and utils/designDrift.ts go with it — between them they were the browser-reading half. What survives is utils/designValues.ts, which works on a record rather than a document: valueForMode, modesPresent, and resolveDeclared. resolveDeclared gained real isolation in the move. Custom properties inherit and `all: initial` does not reset them, so a probe sitting in this page would resolve any reference a record leaves undeclared against the SURROUNDING app's tokens — previewing another project's system would quietly borrow this one's palette wherever that system was incomplete, and a token already reported under unknown_refs would render as though it were fine. Undeclared references are now blanked on the probe first, so they resolve to nothing, which is what the record says they are. Migration 0075 absorbs ui_design_system_id alongside design_rulebook_id rather than an 0076 undoing it: 0075 has not run anywhere, since dev is unmerged and deploys come from main. Both keys named a design source for the running install, and a project already carries its own pointer. This retires the agreement panel shipped yesterday. It asked whether the sheet was actually loaded and applied — the one question a record cannot answer about itself — but only ever about the app you are already inside. Nothing replaces it; recorded in #2430 rather than quietly dropped. Step 1 of milestone #274. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UaYUaouG9jjhATyuxCKrQs
This commit is contained in:
@@ -1,9 +1,13 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* Design systems — editing the stylesheet Scribe holds (milestone #254 step 5).
|
||||
* Design systems — the stylesheets this install RECORDS, for the projects it
|
||||
* tracks (milestone #254 step 5).
|
||||
*
|
||||
* Sibling of /design, which shows the system as the BROWSER has it. This page
|
||||
* shows it as the RECORD has it, which is the half you can change.
|
||||
* It had a sibling, `/design`, which showed the system as the BROWSER had it —
|
||||
* names out of a bundled stylesheet, values out of `getComputedStyle`. That
|
||||
* could only ever describe the install serving the page, so it was a mirror
|
||||
* rather than a tool and was retired (#274). Everything here works on a system
|
||||
* whose app this browser has never loaded.
|
||||
*
|
||||
* The layout follows the model rather than decorating it. A system with a
|
||||
* parent holds only what it changes, so this page has two lists and they are
|
||||
@@ -39,7 +43,6 @@ import {
|
||||
type SnippetCheck,
|
||||
type StylesheetResult,
|
||||
} from "@/api/designSystems";
|
||||
import DesignTabs from "@/components/DesignTabs.vue";
|
||||
import { ApiError } from "@/api/client";
|
||||
import { useToastStore } from "@/stores/toast";
|
||||
import StarterRolePicker from "@/components/StarterRolePicker.vue";
|
||||
@@ -496,7 +499,6 @@ function isColourish(value: string): boolean {
|
||||
|
||||
<template>
|
||||
<div class="ds-view">
|
||||
<DesignTabs />
|
||||
|
||||
<header class="ds-header">
|
||||
<h1>Design systems</h1>
|
||||
|
||||
@@ -1,680 +0,0 @@
|
||||
<script setup lang="ts">
|
||||
/**
|
||||
* Design explorer — the gallery (milestone #251 step 3).
|
||||
*
|
||||
* Renders the design system against the tokens that are actually live, read at
|
||||
* runtime rather than parsed from source, so what you see here is what the app
|
||||
* is using right now.
|
||||
*
|
||||
* HONESTY RULE, and the reason parts of this page can say "not implemented":
|
||||
* a gallery of hand-written look-alikes drifts from the app within a month and
|
||||
* then lies — which is the same failure this whole surface exists to catch. So
|
||||
* every specimen below is either a REAL component imported from the app, or a
|
||||
* real token read from the browser, or it is explicitly marked as missing.
|
||||
*
|
||||
* Buttons WERE the case where that bit: `.btn-primary` was defined five times
|
||||
* in five `<style scoped>` blocks, all five drifted, and this page reported it
|
||||
* as a gap because drawing a look-alike would have made it a sixth copy.
|
||||
* `assets/components.css` is now the single definition (#2273), so the
|
||||
* specimens below are the app's real classes — they cannot drift from the app
|
||||
* without drifting the app itself.
|
||||
*
|
||||
* The panel at the top applies the same rule one level up: the sheet the app
|
||||
* loads is generated from a design system, and nothing checked that the app
|
||||
* ever loaded it (#2419).
|
||||
*/
|
||||
import { computed, onMounted, ref, watch } from "vue";
|
||||
|
||||
import { fetchUiDesignSystem } from "@/api/design";
|
||||
import { fetchResolvedTokens, type ResolvedToken } from "@/api/designSystems";
|
||||
import DesignTabs from "@/components/DesignTabs.vue";
|
||||
import PriorityBadge from "@/components/PriorityBadge.vue";
|
||||
import StatusBadge from "@/components/StatusBadge.vue";
|
||||
import TagPill from "@/components/TagPill.vue";
|
||||
import { useTheme } from "@/composables/useTheme";
|
||||
import {
|
||||
compareToApp,
|
||||
rankAgreements,
|
||||
summarise,
|
||||
valueForMode,
|
||||
type Agreement,
|
||||
type RecordedToken,
|
||||
} from "@/utils/designDrift";
|
||||
import {
|
||||
groupTokens,
|
||||
readTokens,
|
||||
resolveDeclared,
|
||||
type DesignToken,
|
||||
type TokenGroup,
|
||||
} from "@/utils/designTokens";
|
||||
|
||||
const { theme } = useTheme();
|
||||
|
||||
const tokens = ref<DesignToken[]>([]);
|
||||
|
||||
/** The designation, and the two ways it can be absent — see api/design.ts. */
|
||||
const systemId = ref<number | null>(null);
|
||||
const systemTitle = ref<string | null>(null);
|
||||
const checkLoaded = ref(false);
|
||||
const checkFailed = ref(false);
|
||||
const showAgreeingRows = ref(false);
|
||||
|
||||
/** The record, as fetched. Kept raw because the mode narrowing has to be redone
|
||||
* whenever the theme changes — a comparison against the wrong mode's values
|
||||
* would report every mode-aware token as drift. */
|
||||
const records = ref<ResolvedToken[]>([]);
|
||||
const recorded = ref<RecordedToken[]>([]);
|
||||
const resolved = ref<Map<string, string>>(new Map());
|
||||
|
||||
/** Narrow the record to the live mode and re-read the app. Idempotent. */
|
||||
function recheck() {
|
||||
tokens.value = readTokens();
|
||||
recorded.value = records.value.map((t) => ({
|
||||
name: t.name,
|
||||
value: valueForMode(t.value_by_mode, theme.value),
|
||||
groupName: t.group_name,
|
||||
}));
|
||||
const declared = new Map<string, string>();
|
||||
for (const token of recorded.value) {
|
||||
if (token.value) declared.set(token.name, token.value);
|
||||
}
|
||||
resolved.value = resolveDeclared(declared);
|
||||
}
|
||||
|
||||
/**
|
||||
* Read on mount, not at module scope: the values depend on the live cascade,
|
||||
* which needs the app's stylesheets applied and the theme attribute set.
|
||||
*/
|
||||
onMounted(async () => {
|
||||
recheck();
|
||||
try {
|
||||
const designation = await fetchUiDesignSystem();
|
||||
systemId.value = designation.design_system_id;
|
||||
systemTitle.value = designation.title;
|
||||
if (designation.design_system_id !== null && designation.title !== null) {
|
||||
records.value = (await fetchResolvedTokens(designation.design_system_id)).tokens;
|
||||
recheck();
|
||||
}
|
||||
} catch {
|
||||
// The gallery is useful without the panel, so a failed fetch degrades to
|
||||
// "couldn't check" rather than taking the page down with it. It says so
|
||||
// rather than showing the same empty state as "nothing designated" — that
|
||||
// conflation is what let this feature sit dead (#2419).
|
||||
checkFailed.value = true;
|
||||
} finally {
|
||||
checkLoaded.value = true;
|
||||
}
|
||||
});
|
||||
|
||||
// Toggling the theme on this page is the most likely thing anyone does here.
|
||||
watch(theme, () => recheck());
|
||||
|
||||
const agreements = computed<Agreement[]>(() =>
|
||||
rankAgreements(compareToApp(recorded.value, resolved.value, tokens.value)),
|
||||
);
|
||||
const summary = computed(() => summarise(agreements.value));
|
||||
const visibleAgreements = computed(() =>
|
||||
showAgreeingRows.value
|
||||
? agreements.value
|
||||
: agreements.value.filter((a) => a.status !== "ok"),
|
||||
);
|
||||
/** Named but unvalued roles — skipped by the comparison, worth stating once. */
|
||||
const unvaluedCount = computed(
|
||||
() => records.value.filter((t) => !valueForMode(t.value_by_mode, theme.value)).length,
|
||||
);
|
||||
|
||||
const STATUS_LABEL: Record<Agreement["status"], string> = {
|
||||
absent: "not in the app",
|
||||
differs: "app renders another value",
|
||||
unrecorded: "not in the record",
|
||||
ok: "agrees",
|
||||
};
|
||||
|
||||
/* Gallery ------------------------------------------------------------------ */
|
||||
|
||||
/**
|
||||
* Groups come from the RECORD where there is one, and from the name prefix
|
||||
* otherwise. The prefix table in designTokens.ts can only know the families
|
||||
* that shipped with the product; the record knows the ones this install
|
||||
* authored, and grouping 110 tokens under "other" because a table never heard
|
||||
* of their prefix is a gallery nobody reads.
|
||||
*/
|
||||
const recordGroups = computed(() => {
|
||||
const out = new Map<string, string>();
|
||||
for (const token of records.value) {
|
||||
if (token.group_name) out.set(token.name, token.group_name);
|
||||
}
|
||||
return out;
|
||||
});
|
||||
|
||||
const grouped = computed(() => groupTokens(tokens.value, recordGroups.value));
|
||||
|
||||
/** Order for the prefix-derived groups only; record groups sort by name. */
|
||||
const GROUP_ORDER: TokenGroup[] = ["color", "radius", "glow", "gradient", "focus", "layout", "other"];
|
||||
|
||||
const orderedGroups = computed(() => {
|
||||
const fromRecord = new Set(recordGroups.value.values());
|
||||
const rank = (g: string) => {
|
||||
const i = GROUP_ORDER.indexOf(g as TokenGroup);
|
||||
return i < 0 ? GROUP_ORDER.length : i;
|
||||
};
|
||||
const keys = [...grouped.value.keys()].sort((a, b) => {
|
||||
// The record's own groups lead: they are the system, and the prefix-derived
|
||||
// ones are whatever else the sheet happens to carry.
|
||||
const byOrigin = Number(!fromRecord.has(a)) - Number(!fromRecord.has(b));
|
||||
if (byOrigin !== 0) return byOrigin;
|
||||
if (fromRecord.has(a)) return a.localeCompare(b);
|
||||
return rank(a) - rank(b);
|
||||
});
|
||||
return keys.map((group) => ({ group, tokens: grouped.value.get(group)! }));
|
||||
});
|
||||
|
||||
/** A token whose value reads as a colour is worth showing as a swatch. */
|
||||
function isColourish(value: string): boolean {
|
||||
return /^(#|rgba?\(|hsla?\(|color-mix\()/.test(value.trim());
|
||||
}
|
||||
|
||||
/** The button family as shared classes, described by the roles they reach for. */
|
||||
const BUTTON_VARIANTS = [
|
||||
{ name: "Primary", spec: "action-primary background, text-on-action label, no border" },
|
||||
{ name: "Secondary", spec: "action-secondary background, text-on-action label, no border" },
|
||||
{ name: "Ghost", spec: "transparent, primary text, one-pixel border" },
|
||||
{ name: "Danger", spec: "action-destructive background, text-on-action label" },
|
||||
];
|
||||
|
||||
/**
|
||||
* The type scale, read from whatever size tokens the sheet actually declares.
|
||||
*
|
||||
* Was a hand-written table of nine sizes marked "no token" — true when written,
|
||||
* and false since the scale was recorded. Deriving it from the live tokens is
|
||||
* what stops it going stale a second time: if the scale is removed, this
|
||||
* section empties out and says so.
|
||||
*/
|
||||
const sizeTokens = computed(() => tokens.value.filter((t) => /-size(-|$)/.test(t.name)));
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="design-view">
|
||||
<DesignTabs />
|
||||
|
||||
<header class="design-header">
|
||||
<h1>Live tokens</h1>
|
||||
<p class="lede">
|
||||
The system as it actually is. Token values are read from the browser at
|
||||
runtime, so this page reflects the live cascade rather than what the
|
||||
stylesheet says. Components shown are the real ones — where a piece of
|
||||
the system has no shared implementation, it is marked missing rather
|
||||
than mocked up.
|
||||
</p>
|
||||
</header>
|
||||
|
||||
<!-- Does the running app match the design system it was generated from? -->
|
||||
<section class="design-section">
|
||||
<h2>Agreement with the record</h2>
|
||||
|
||||
<p v-if="!checkLoaded" class="muted">
|
||||
Checking the running app against its design system…
|
||||
</p>
|
||||
|
||||
<div v-else-if="checkFailed" class="gap-notice">
|
||||
<strong>The design system couldn't be read.</strong>
|
||||
<p>
|
||||
The gallery below is still live — it comes from the browser, not the
|
||||
server — but nothing is being compared against the record right now.
|
||||
This is a failure, not an empty result.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div v-else-if="systemId === null" class="gap-notice">
|
||||
<strong>No design system designated for this UI.</strong>
|
||||
<p>
|
||||
This install hasn't said which design system its own interface is
|
||||
built from, so there is nothing to check the running app against.
|
||||
Designate one in <router-link to="/settings">Settings</router-link>
|
||||
and this panel will report every token the record declares that the
|
||||
app doesn't have, renders differently, or has never heard of.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div v-else-if="systemTitle === null" class="gap-notice">
|
||||
<strong>Design system #{{ systemId }} could not be read.</strong>
|
||||
<p>
|
||||
It is designated in <router-link to="/settings">Settings</router-link>,
|
||||
but it has since been deleted or is no longer shared with you. Nothing
|
||||
is being checked — this is a misconfiguration, not a clean result.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<template v-else>
|
||||
<p class="section-note">
|
||||
<strong>{{ summary.absent }}</strong> not in the app ·
|
||||
<strong>{{ summary.differs }}</strong> rendering another value ·
|
||||
{{ summary.unrecorded }} not in the record ·
|
||||
{{ summary.ok }} agreeing, across {{ summary.total }} tokens checked
|
||||
against <strong>{{ systemTitle }}</strong> in
|
||||
<code>{{ theme }}</code> mode.
|
||||
<template v-if="unvaluedCount">
|
||||
{{ unvaluedCount }} recorded {{ unvaluedCount === 1 ? "role has" : "roles have" }}
|
||||
no value yet and {{ unvaluedCount === 1 ? "was" : "were" }} not checked.
|
||||
</template>
|
||||
</p>
|
||||
|
||||
<div class="gap-notice">
|
||||
<strong>This compares the record against the TOKENS only.</strong>
|
||||
<p>
|
||||
A value hardcoded in a component — where a token should have been
|
||||
referenced — is invisible here, because the drift isn't in the
|
||||
tokens at all. Reading it would mean bundling every component's
|
||||
source into the app. That check belongs in CI and is tracked
|
||||
separately, so treat a clean panel as "the tokens agree", not "the
|
||||
app agrees".
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<p v-if="!agreements.length" class="muted">
|
||||
The record declares no valued tokens, so there is nothing to compare
|
||||
yet. Give its roles values and this panel starts reporting.
|
||||
</p>
|
||||
|
||||
<ul v-else class="spec-list">
|
||||
<li v-for="row in visibleAgreements" :key="row.name">
|
||||
<span class="spec-name">
|
||||
<span
|
||||
v-if="isColourish(row.live || row.recorded)"
|
||||
class="swatch"
|
||||
:style="{ background: row.live || row.recorded }"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<code>{{ row.name }}</code>
|
||||
</span>
|
||||
<span class="spec-detail">
|
||||
<template v-if="row.status === 'differs'">
|
||||
record <code>{{ row.recorded }}</code> · app
|
||||
<code>{{ row.live }}</code>
|
||||
</template>
|
||||
<template v-else-if="row.status === 'unrecorded'">
|
||||
app <code>{{ row.live }}</code>
|
||||
</template>
|
||||
<template v-else>
|
||||
record <code>{{ row.recorded }}</code>
|
||||
</template>
|
||||
<span v-if="row.groupName" class="matches"> · {{ row.groupName }}</span>
|
||||
</span>
|
||||
<span class="spec-status" :class="row.status">{{ STATUS_LABEL[row.status] }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
<button
|
||||
v-if="agreements.length && summary.ok"
|
||||
class="reveal-toggle"
|
||||
@click="showAgreeingRows = !showAgreeingRows"
|
||||
>
|
||||
{{ showAgreeingRows ? "Hide" : "Show" }} the {{ summary.ok }} agreeing tokens
|
||||
</button>
|
||||
</template>
|
||||
</section>
|
||||
|
||||
<!-- Real components: these are imported, not recreated. -->
|
||||
<section class="design-section">
|
||||
<h2>Components</h2>
|
||||
<p class="section-note">Imported from the app. What you see is what ships.</p>
|
||||
|
||||
<div class="specimen">
|
||||
<span class="specimen-label">Status badge</span>
|
||||
<div class="specimen-row">
|
||||
<StatusBadge status="todo" />
|
||||
<StatusBadge status="in_progress" />
|
||||
<StatusBadge status="done" />
|
||||
<StatusBadge status="cancelled" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="specimen">
|
||||
<span class="specimen-label">Priority badge</span>
|
||||
<div class="specimen-row">
|
||||
<PriorityBadge priority="low" />
|
||||
<PriorityBadge priority="medium" />
|
||||
<PriorityBadge priority="high" />
|
||||
<span class="muted">(<code>none</code> renders nothing, by design)</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="specimen">
|
||||
<span class="specimen-label">Tag pill</span>
|
||||
<div class="specimen-row">
|
||||
<TagPill tag="design-system" />
|
||||
<TagPill tag="dismissible" dismissible />
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<!-- No longer a gap: these are the app's real classes, from the shared
|
||||
sheet. Nothing here is a look-alike — change components.css and these
|
||||
specimens change with it, which is the only way this page stays true. -->
|
||||
<section class="design-section">
|
||||
<h2>Buttons</h2>
|
||||
<div class="button-specimens">
|
||||
<button class="btn-primary">Save</button>
|
||||
<button class="btn-secondary">Detect</button>
|
||||
<button class="btn-ghost">Cancel</button>
|
||||
<button class="btn-danger">Delete</button>
|
||||
<button class="btn-primary" disabled>Disabled</button>
|
||||
</div>
|
||||
<p class="spec-caption">
|
||||
Three sizes, because the app has three kinds of button: a page action, a
|
||||
row action, and an affordance that sits inside a card without disturbing
|
||||
its rhythm.
|
||||
</p>
|
||||
<div class="button-specimens">
|
||||
<button class="btn-primary">Default — page action</button>
|
||||
<button class="btn-primary btn-compact">Compact — row action</button>
|
||||
<button class="btn-primary btn-inline">Inline</button>
|
||||
</div>
|
||||
<ul class="spec-list">
|
||||
<li v-for="b in BUTTON_VARIANTS" :key="b.name">
|
||||
<span class="spec-name">{{ b.name }}</span>
|
||||
<span class="spec-detail">{{ b.spec }}</span>
|
||||
<span class="spec-status ok">shared</span>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<!-- Type: rendered at the sizes the sheet declares, not described. -->
|
||||
<section class="design-section">
|
||||
<h2>Type scale</h2>
|
||||
<div v-if="!sizeTokens.length" class="gap-notice">
|
||||
<strong>The scale has no tokens.</strong>
|
||||
<p>
|
||||
Nothing in the stylesheet declares a size token, so sizes are being set
|
||||
ad hoc per component. There is nothing live to show here.
|
||||
</p>
|
||||
</div>
|
||||
<ul v-else class="spec-list">
|
||||
<li v-for="t in sizeTokens" :key="t.name">
|
||||
<span class="spec-name" :style="{ fontSize: t.value }">Ag</span>
|
||||
<span class="spec-detail"><code>{{ t.name }}</code></span>
|
||||
<span class="spec-status ok">{{ t.value || "—" }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<!-- Tokens: entirely real, read live. -->
|
||||
<section v-for="{ group, tokens: groupTokenList } in orderedGroups" :key="group" class="design-section">
|
||||
<h2 class="token-group-heading">{{ group }} <span class="count">{{ groupTokenList.length }}</span></h2>
|
||||
<ul class="token-list">
|
||||
<li v-for="token in groupTokenList" :key="token.name" class="token-row">
|
||||
<span
|
||||
v-if="isColourish(token.value)"
|
||||
class="swatch"
|
||||
:style="{ background: token.value }"
|
||||
aria-hidden="true"
|
||||
/>
|
||||
<span v-else class="swatch swatch-none" aria-hidden="true" />
|
||||
<code class="token-name">{{ token.name }}</code>
|
||||
<code class="token-value">{{ token.value || "—" }}</code>
|
||||
<span v-if="token.modeAware" class="token-flag" title="Re-declared under a mode selector">
|
||||
mode-aware
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<p v-if="!tokens.length" class="muted">Reading tokens…</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.design-view {
|
||||
max-width: var(--page-max-width);
|
||||
margin: 0 auto;
|
||||
padding: 1.5rem var(--page-padding-x) 4rem;
|
||||
}
|
||||
|
||||
.design-header {
|
||||
margin-bottom: 2rem;
|
||||
}
|
||||
|
||||
.lede {
|
||||
color: var(--color-text-secondary);
|
||||
max-width: 60ch;
|
||||
line-height: 1.7;
|
||||
}
|
||||
|
||||
.design-section {
|
||||
margin-bottom: 2.5rem;
|
||||
}
|
||||
|
||||
.design-section h2 {
|
||||
margin-bottom: 0.25rem;
|
||||
}
|
||||
|
||||
.token-group-heading {
|
||||
text-transform: capitalize;
|
||||
}
|
||||
|
||||
.count {
|
||||
color: var(--color-text-muted);
|
||||
font-size: 0.8rem;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.section-note,
|
||||
.muted {
|
||||
color: var(--color-text-muted);
|
||||
font-size: 0.85rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
/* Specimens -------------------------------------------------------------- */
|
||||
|
||||
.specimen {
|
||||
padding: 0.75rem 0;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
}
|
||||
|
||||
.specimen-label {
|
||||
display: block;
|
||||
font-size: 0.75rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
color: var(--color-text-muted);
|
||||
margin-bottom: 0.5rem;
|
||||
}
|
||||
|
||||
.specimen-row {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 0.5rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
/* Gaps ------------------------------------------------------------------- */
|
||||
|
||||
.spec-caption {
|
||||
margin: 0 0 0.75rem;
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 0.85rem;
|
||||
line-height: 1.5;
|
||||
max-width: 60ch;
|
||||
}
|
||||
|
||||
/* Layout only. The buttons inside style themselves from the shared sheet —
|
||||
adding any appearance rule here would recreate the copy this section
|
||||
just stopped being. */
|
||||
.button-specimens {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: var(--fs-space-3);
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.gap-notice {
|
||||
background: var(--color-surface);
|
||||
border: 1px solid var(--color-border);
|
||||
border-left: 3px solid var(--color-warning);
|
||||
border-radius: var(--radius-sm);
|
||||
padding: 0.75rem 1rem;
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.gap-notice p {
|
||||
margin: 0.5rem 0 0;
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.6;
|
||||
max-width: 70ch;
|
||||
}
|
||||
|
||||
.spec-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.spec-list li {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: 0.75rem;
|
||||
padding: 0.4rem 0;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.spec-name {
|
||||
min-width: 8rem;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.spec-detail {
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 0.85rem;
|
||||
flex: 1;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.spec-status {
|
||||
font-size: 0.7rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
padding: 0.1rem 0.45rem;
|
||||
border-radius: var(--radius-sm);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
/* `absent` is the one status that can mean the whole sheet failed to load, so
|
||||
it carries the same weight as a high-priority finding; `differs` is a wrong
|
||||
value on screen right now; `unrecorded` is bookkeeping and reads quietest. */
|
||||
.spec-status.absent {
|
||||
background: var(--color-priority-high-bg);
|
||||
color: var(--color-priority-high);
|
||||
}
|
||||
|
||||
.spec-status.differs {
|
||||
background: var(--color-priority-medium-bg);
|
||||
color: var(--color-priority-medium);
|
||||
}
|
||||
|
||||
.spec-status.unrecorded {
|
||||
background: var(--color-surface);
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.spec-status.missing {
|
||||
background: var(--color-priority-medium-bg);
|
||||
color: var(--color-priority-medium);
|
||||
}
|
||||
|
||||
.spec-status.ok {
|
||||
background: var(--color-status-done-bg);
|
||||
color: var(--color-status-done);
|
||||
}
|
||||
|
||||
.spec-name .swatch {
|
||||
vertical-align: middle;
|
||||
margin-right: 0.4rem;
|
||||
}
|
||||
|
||||
.matches {
|
||||
color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
.reveal-toggle {
|
||||
margin-top: 0.75rem;
|
||||
padding: 0.35rem 0.75rem;
|
||||
background: transparent;
|
||||
color: var(--color-text-secondary);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-sm);
|
||||
cursor: pointer;
|
||||
font-family: inherit;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.reveal-toggle:hover {
|
||||
border-color: var(--color-text-muted);
|
||||
}
|
||||
|
||||
/* Tokens ----------------------------------------------------------------- */
|
||||
|
||||
.token-list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.token-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.75rem;
|
||||
padding: 0.3rem 0;
|
||||
border-bottom: 1px solid var(--color-border);
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.swatch {
|
||||
width: 1.25rem;
|
||||
height: 1.25rem;
|
||||
flex: none;
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-sm);
|
||||
}
|
||||
|
||||
.swatch-none {
|
||||
background: repeating-linear-gradient(
|
||||
45deg,
|
||||
transparent,
|
||||
transparent 3px,
|
||||
var(--color-border) 3px,
|
||||
var(--color-border) 4px
|
||||
);
|
||||
}
|
||||
|
||||
.token-name {
|
||||
min-width: 16rem;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.token-value {
|
||||
color: var(--color-text-secondary);
|
||||
font-size: 0.8rem;
|
||||
flex: 1;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.token-flag {
|
||||
font-size: 0.65rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.08em;
|
||||
color: var(--color-text-muted);
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-sm);
|
||||
padding: 0.05rem 0.35rem;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.token-name {
|
||||
min-width: 0;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -4,7 +4,6 @@ import { useSettingsStore } from "@/stores/settings";
|
||||
import { useAuthStore } from "@/stores/auth";
|
||||
import { useToastStore } from "@/stores/toast";
|
||||
import { apiGet, apiPost, apiPut, apiDelete, listGroups, createGroup, deleteGroup, listGroupMembers, addGroupMember, removeGroupMember, searchUsers, listApiKeys, createApiKey as apiCreateApiKey, revokeApiKey as apiRevokeApiKey, getProfile, updateProfile, type ApiKeyEntry, type GroupEntry, type GroupMember, type UserSearchResult, type UserProfile } from "@/api/client";
|
||||
import { fetchDesignSystems } from "@/api/designSystems";
|
||||
import type { User } from "@/types/auth";
|
||||
import PaginationBar from "@/components/PaginationBar.vue";
|
||||
import TagInput from "@/components/TagInput.vue";
|
||||
@@ -32,12 +31,6 @@ const kbWritePathThreshold = ref("0.68");
|
||||
// gate: that one BLOCKS a create and must be unforgiving of noise, this one only
|
||||
// suggests a merge the operator reviews (services/dedup.py).
|
||||
const kbDuplicateThreshold = ref("0.82");
|
||||
// Which design system this install's own UI is built from, for the /design
|
||||
// agreement panel. Empty = none designated, which is the normal state for a
|
||||
// fresh install rather than a misconfiguration — the panel explains itself when
|
||||
// unset. Replaced design_rulebook_id when the rulebook was retired (#2419).
|
||||
const uiDesignSystemId = ref("");
|
||||
const designSystems = ref<{ id: number; title: string }[]>([]);
|
||||
const savingKbInject = ref(false);
|
||||
const kbInjectSaved = ref(false);
|
||||
|
||||
@@ -107,9 +100,6 @@ async function saveKbInject() {
|
||||
kb_writepath_enabled: kbWritePathEnabled.value ? 'true' : 'false',
|
||||
kb_writepath_threshold: String(wpT),
|
||||
kb_duplicate_threshold: String(dupT),
|
||||
// Empty string DELETES the setting (see routes/settings.py), which is
|
||||
// exactly right for "no design system" — absent rather than zero.
|
||||
ui_design_system_id: uiDesignSystemId.value,
|
||||
});
|
||||
kbInjectSaved.value = true;
|
||||
setTimeout(() => (kbInjectSaved.value = false), 2000);
|
||||
@@ -500,16 +490,6 @@ onMounted(async () => {
|
||||
if (allSettings.kb_duplicate_threshold !== undefined) {
|
||||
kbDuplicateThreshold.value = allSettings.kb_duplicate_threshold;
|
||||
}
|
||||
uiDesignSystemId.value = allSettings.ui_design_system_id ?? "";
|
||||
// Best-effort: the picker degrades to "none available" rather than blocking
|
||||
// the whole settings page if design systems can't be listed.
|
||||
try {
|
||||
designSystems.value = (await fetchDesignSystems()).design_systems.map(
|
||||
(s) => ({ id: s.id, title: s.title }),
|
||||
);
|
||||
} catch {
|
||||
designSystems.value = [];
|
||||
}
|
||||
if (allSettings.notify_task_reminders !== undefined) {
|
||||
notifyTaskReminders.value = allSettings.notify_task_reminders !== "false";
|
||||
}
|
||||
@@ -1280,25 +1260,12 @@ function formatUserDate(iso: string): string {
|
||||
location, not by resemblance.
|
||||
</p>
|
||||
</div>
|
||||
<div class="field">
|
||||
<label for="ui-design-system">This app's design system</label>
|
||||
<select id="ui-design-system" v-model="uiDesignSystemId" class="input" style="max-width: 22rem">
|
||||
<option value="">None — don't check the interface against a record</option>
|
||||
<option v-for="ds in designSystems" :key="ds.id" :value="String(ds.id)">
|
||||
{{ ds.title }}
|
||||
</option>
|
||||
</select>
|
||||
<p class="field-hint">
|
||||
Which design system this interface is supposed to be built from. Once
|
||||
set, the <router-link to="/design">Design</router-link> page compares
|
||||
every token the system declares against what the browser has actually
|
||||
resolved, and reports the ones the app is missing, renders differently,
|
||||
or has never heard of. That catches a stylesheet that was regenerated
|
||||
but never shipped — which the record alone cannot tell you, since the
|
||||
sheet is generated from it. Leave it as None if this install's
|
||||
interface isn't described by one of your design systems.
|
||||
</p>
|
||||
</div>
|
||||
<!-- A design system belongs to a PROJECT, and the picker for it lives on
|
||||
the project. There was a setting here that designated the system
|
||||
this install's own interface was built from; it only ever described
|
||||
the app you were already looking at, which is not what the feature
|
||||
is for (#274). -->
|
||||
|
||||
<div class="field">
|
||||
<label for="kb-duplicate-threshold">Near-duplicate report threshold</label>
|
||||
<input
|
||||
|
||||
Reference in New Issue
Block a user