From d0a2733cb601c5b824ffa5ede5da83d6d15824e5 Mon Sep 17 00:00:00 2001 From: Bryan Van Deusen Date: Thu, 27 Aug 2026 21:38:09 -0400 Subject: [PATCH] fix(design): text on a tint of itself now clears AA app-wide, and the check gates it (#3141) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The badge fix (#3132) exposed the same defect everywhere: 48 rules painting a token as TEXT on an inline color-mix tint of that same token. Worst raw measurements, across every tint strength in use, both modes, over page/raised/hover: accent 1.53:1 · success 1.67:1 · text-tertiary 2.15:1 warning 2.32:1 · error 2.36:1 against AA's 4.5 THE DEFECT IS IN THE HOUSE, NOT IN SCRIBE. The semantic hues are shared family-wide, and the accent case was measured against every app's real accent, not assumed from Scribe's: Minstrel 1.81, Forge 1.87, Steward 1.65, Roundtable 3.01 — all failing. So the six -fg tokens are recorded on FabledSword (design system 1), where their parents live, rather than copied into each app. 45% toward --fs-text-primary clears AA for ALL FIVE accents (4.56-5.00), so this is one house token rather than five overrides, and it keeps deriving from --fs-accent — an app that overrides its accent still gets a legible tinted-text colour in its own colour, the same mechanism as --fs-accent-soft. The tokens are additive: a sibling app is unaffected until it regenerates its own stylesheet. One token is honestly redundant. --fs-text-secondary already passes at 4.82:1, and --fs-text-secondary-fg barely moves it. It exists so the rule has NO exceptions, because the alternative is a permanent allow-list entry for the one case that happens to pass — and a guard with an invisible exception is a guard that erodes. 46 substitutions across 18 files, each rewriting only the `color:` inside a block that tints its own background. THE CHECK NOW GATES BOTH SPELLINGS. It previously reported the inline form, because a gate nobody can satisfy on the day it lands gets switched off. Both are clean, so both fail the build now. And the check had a false-positive bug worth naming: its `color\s*:` regex matched the tail of `border-color`, `border-left-color` and `outline-color`, so it flagged seven rules that were already correct. A border is a non-text graphic with a 3:1 floor, not text at 4.5. A check that cries wolf on correct code is one that gets muted, so that mattered more than the noise. Verified by construction, not by passing: reintroduced each defect form (exit 1 each), and confirmed a legitimate border-only rule still exits 0. Co-Authored-By: Claude Opus 5 (1M context) --- frontend/src/assets/editor-shared.css | 6 +-- frontend/src/assets/theme.css | 6 +++ frontend/src/assets/viewer-shared.css | 2 +- frontend/src/components/AppHeader.vue | 4 +- frontend/src/components/DiffView.vue | 4 +- frontend/src/components/InlineAssistPanel.vue | 4 +- frontend/src/components/MarkdownToolbar.vue | 2 +- frontend/src/components/SystemsSection.vue | 4 +- frontend/src/components/TagInput.vue | 2 +- .../src/components/WorkspaceNoteEditor.vue | 4 +- .../src/components/WorkspaceTaskPanel.vue | 8 ++-- frontend/src/views/GraphView.vue | 4 +- frontend/src/views/KnowledgeView.vue | 4 +- frontend/src/views/NoteViewerView.vue | 2 +- frontend/src/views/ProjectView.vue | 8 ++-- frontend/src/views/SettingsView.vue | 16 ++++---- frontend/src/views/SharedWithMeView.vue | 6 +-- frontend/src/views/SnippetDetailView.vue | 2 +- frontend/src/views/SnippetListView.vue | 10 ++--- scripts/check_design_tokens.py | 37 +++++++++++-------- 20 files changed, 74 insertions(+), 61 deletions(-) diff --git a/frontend/src/assets/editor-shared.css b/frontend/src/assets/editor-shared.css index 9637035..7cd8ff3 100644 --- a/frontend/src/assets/editor-shared.css +++ b/frontend/src/assets/editor-shared.css @@ -127,7 +127,7 @@ border: 1px solid var(--fs-error); border-radius: var(--fs-radius-sm); font-size: 0.85rem; - color: var(--fs-error); + color: var(--fs-error-fg); } .diff-view { border: 1px solid var(--fs-border-color); @@ -147,11 +147,11 @@ } .diff-delete { background: color-mix(in srgb, var(--fs-error) 12%, transparent); - color: var(--fs-error); + color: var(--fs-error-fg); } .diff-insert { background: color-mix(in srgb, var(--fs-success) 12%, transparent); - color: var(--fs-success); + color: var(--fs-success-fg); } .diff-equal { color: var(--fs-text-tertiary); diff --git a/frontend/src/assets/theme.css b/frontend/src/assets/theme.css index c4d7d9d..f174443 100644 --- a/frontend/src/assets/theme.css +++ b/frontend/src/assets/theme.css @@ -31,6 +31,7 @@ --fs-accent-faint: color-mix(in srgb, var(--fs-accent) 8%, transparent); /* The faintest accent wash */ --fs-accent-deep: color-mix(in srgb, var(--fs-accent) 70%, black); /* The accent, darkened */ --fs-accent-wash: color-mix(in srgb, var(--fs-accent) 22%, transparent); /* Heaviest accent tint */ + --fs-accent-fg: color-mix(in srgb, var(--fs-accent) 45%, var(--fs-text-primary)); /* Accent TEXT on an accent tint */ --fs-gradient-cta: linear-gradient(135deg, var(--fs-accent), var(--fs-accent-deep)); --fs-glow-cta: 0 2px 10px color-mix(in srgb, var(--fs-accent) 35%, transparent); --fs-glow-cta-hover: 0 4px 24px color-mix(in srgb, var(--fs-accent) 65%, transparent); @@ -102,8 +103,11 @@ /* semantic */ --fs-success: var(--fs-action-primary); + --fs-success-fg: color-mix(in srgb, var(--fs-success) 45%, var(--fs-text-primary)); /* Success TEXT on a success tint */ --fs-warning: #8B6F1E; + --fs-warning-fg: color-mix(in srgb, var(--fs-warning) 50%, var(--fs-text-primary)); /* Warning TEXT on a warning tint */ --fs-error: #C04A1F; + --fs-error-fg: color-mix(in srgb, var(--fs-error) 50%, var(--fs-text-primary)); /* Error TEXT on an error tint */ --fs-info: #3D5A6E; --fs-destructive: #6B2118; /* irreversible — deliberately not the error colour */ @@ -148,7 +152,9 @@ /* text */ --fs-text-primary: #E8E4D8; /* body, headings, labels — inverts by mode */ --fs-text-secondary: #C2BFB4; + --fs-text-secondary-fg: color-mix(in srgb, var(--fs-text-secondary) 90%, var(--fs-text-primary)); /* Secondary TEXT on a secondary tint (barely moves; no exceptions) */ --fs-text-tertiary: #9C9A92; + --fs-text-tertiary-fg: color-mix(in srgb, var(--fs-text-tertiary) 55%, var(--fs-text-primary)); /* Tertiary TEXT on a tertiary tint */ --fs-text-on-action: #E8E4D8; /* text on a filled colour — NOT mode-dependent */ /* type */ diff --git a/frontend/src/assets/viewer-shared.css b/frontend/src/assets/viewer-shared.css index 644fe1d..864c62d 100644 --- a/frontend/src/assets/viewer-shared.css +++ b/frontend/src/assets/viewer-shared.css @@ -25,7 +25,7 @@ border-color: var(--fs-accent); } .ctx-crumb-project { - color: var(--fs-accent); + color: var(--fs-accent-fg); background: color-mix(in srgb, var(--fs-accent) 10%, transparent); border: 1px solid color-mix(in srgb, var(--fs-accent) 30%, transparent); text-decoration: none; diff --git a/frontend/src/components/AppHeader.vue b/frontend/src/components/AppHeader.vue index f12f472..0363f46 100644 --- a/frontend/src/components/AppHeader.vue +++ b/frontend/src/components/AppHeader.vue @@ -206,7 +206,7 @@ router.afterEach(() => { background: var(--fs-accent-soft); } .nav-link.router-link-active { - color: var(--fs-accent); + color: var(--fs-accent-fg); font-weight: 500; background: color-mix(in srgb, var(--fs-accent) 25%, transparent); box-shadow: 0 0 16px color-mix(in srgb, var(--fs-accent) 30%, transparent); @@ -257,7 +257,7 @@ router.afterEach(() => { font-weight: 500; text-transform: uppercase; letter-spacing: 0.05em; - color: var(--fs-accent); + color: var(--fs-accent-fg); background: color-mix(in srgb, var(--fs-accent) 15%, transparent); padding: 0.1rem 0.35rem; border-radius: var(--fs-radius-sm); diff --git a/frontend/src/components/DiffView.vue b/frontend/src/components/DiffView.vue index b2d4dde..7fb8754 100644 --- a/frontend/src/components/DiffView.vue +++ b/frontend/src/components/DiffView.vue @@ -137,12 +137,12 @@ function markerFor(type: DiffLine['type']): string { .diff-delete { background: color-mix(in srgb, var(--fs-error) 12%, transparent); - color: var(--fs-error); + color: var(--fs-error-fg); } .diff-insert { background: color-mix(in srgb, var(--fs-success) 12%, transparent); - color: var(--fs-success); + color: var(--fs-success-fg); } .diff-equal { diff --git a/frontend/src/components/InlineAssistPanel.vue b/frontend/src/components/InlineAssistPanel.vue index 313008b..60e89a5 100644 --- a/frontend/src/components/InlineAssistPanel.vue +++ b/frontend/src/components/InlineAssistPanel.vue @@ -227,11 +227,11 @@ const markers: Record = { .iap-diff-equal { color: var(--fs-text-tertiary); } .iap-diff-delete { background: color-mix(in srgb, var(--fs-error) 10%, transparent); - color: var(--fs-error); + color: var(--fs-error-fg); } .iap-diff-insert { background: color-mix(in srgb, var(--fs-success) 10%, transparent); - color: var(--fs-success); + color: var(--fs-success-fg); } .iap-diff-marker { diff --git a/frontend/src/components/MarkdownToolbar.vue b/frontend/src/components/MarkdownToolbar.vue index 7ba034a..8741855 100644 --- a/frontend/src/components/MarkdownToolbar.vue +++ b/frontend/src/components/MarkdownToolbar.vue @@ -156,7 +156,7 @@ const groups = [ .md-btn.active { background: color-mix(in srgb, var(--fs-accent) 14%, transparent); - color: var(--fs-accent); + color: var(--fs-accent-fg); box-shadow: 0 0 0 1px color-mix(in srgb, var(--fs-accent) 35%, transparent); } diff --git a/frontend/src/components/SystemsSection.vue b/frontend/src/components/SystemsSection.vue index 6bfbbf3..d3411f7 100644 --- a/frontend/src/components/SystemsSection.vue +++ b/frontend/src/components/SystemsSection.vue @@ -679,7 +679,7 @@ async function confirmDelete() { font-weight: 500; background: color-mix(in srgb, var(--fs-accent) 12%, transparent); border: 1px solid color-mix(in srgb, var(--fs-accent) 30%, transparent); - color: var(--fs-accent); + color: var(--fs-accent-fg); border-radius: 999px; padding: 0.05rem 0.45rem; flex-shrink: 0; @@ -689,7 +689,7 @@ async function confirmDelete() { font-weight: 500; text-transform: uppercase; letter-spacing: 0.04em; - color: var(--fs-text-tertiary); + color: var(--fs-text-tertiary-fg); background: color-mix(in srgb, var(--fs-text-tertiary) 12%, transparent); border-radius: 999px; padding: 0.05rem 0.45rem; diff --git a/frontend/src/components/TagInput.vue b/frontend/src/components/TagInput.vue index 986225b..00a4403 100644 --- a/frontend/src/components/TagInput.vue +++ b/frontend/src/components/TagInput.vue @@ -168,7 +168,7 @@ function focusInput() { border-radius: 999px; background: color-mix(in srgb, var(--fs-accent) 15%, transparent); border: 1px solid var(--fs-accent); - color: var(--fs-accent); + color: var(--fs-accent-fg); font-size: 0.8rem; white-space: nowrap; } diff --git a/frontend/src/components/WorkspaceNoteEditor.vue b/frontend/src/components/WorkspaceNoteEditor.vue index d54fb55..a3462a4 100644 --- a/frontend/src/components/WorkspaceNoteEditor.vue +++ b/frontend/src/components/WorkspaceNoteEditor.vue @@ -548,7 +548,7 @@ defineExpose({ reload: loadProjectNotes }); .note-tag-pill { font-size: 0.58rem; - color: var(--fs-accent); + color: var(--fs-accent-fg); background: color-mix(in srgb, var(--fs-accent) 10%, transparent); border-radius: 999px; padding: 0 0.3rem; @@ -645,7 +645,7 @@ defineExpose({ reload: loadProjectNotes }); .btn-tag-suggestion.applied { background: color-mix(in srgb, var(--fs-accent) 15%, transparent); border-color: var(--fs-accent); - color: var(--fs-accent); + color: var(--fs-accent-fg); } .link-suggest-strip { diff --git a/frontend/src/components/WorkspaceTaskPanel.vue b/frontend/src/components/WorkspaceTaskPanel.vue index f8b101e..d98bdae 100644 --- a/frontend/src/components/WorkspaceTaskPanel.vue +++ b/frontend/src/components/WorkspaceTaskPanel.vue @@ -424,8 +424,8 @@ defineExpose({ reload: loadAll }); border-radius: 10px; text-transform: capitalize; } -.ms-status-active { background: color-mix(in srgb, var(--fs-accent) 15%, transparent); color: var(--fs-accent); } -.ms-status-completed { background: color-mix(in srgb, var(--fs-success) 15%, transparent); color: var(--fs-success); } +.ms-status-active { background: color-mix(in srgb, var(--fs-accent) 15%, transparent); color: var(--fs-accent-fg); } +.ms-status-completed { background: color-mix(in srgb, var(--fs-success) 15%, transparent); color: var(--fs-success-fg); } .task-items { list-style: none; @@ -516,8 +516,8 @@ defineExpose({ reload: loadAll }); user-select: none; margin-left: auto; } -.status-cycler.status-in_progress { border-color: var(--fs-accent); color: var(--fs-accent); background: color-mix(in srgb, var(--fs-accent) 10%, transparent); } -.status-cycler.status-done { border-color: var(--fs-success); color: var(--fs-success); background: color-mix(in srgb, var(--fs-success) 10%, transparent); } +.status-cycler.status-in_progress { border-color: var(--fs-accent); color: var(--fs-accent-fg); background: color-mix(in srgb, var(--fs-accent) 10%, transparent); } +.status-cycler.status-done { border-color: var(--fs-success); color: var(--fs-success-fg); background: color-mix(in srgb, var(--fs-success) 10%, transparent); } .btn-edit-task { margin-left: 0.25rem; } .btn-edit-task:hover { text-decoration: underline; } diff --git a/frontend/src/views/GraphView.vue b/frontend/src/views/GraphView.vue index 6642522..e6eec38 100644 --- a/frontend/src/views/GraphView.vue +++ b/frontend/src/views/GraphView.vue @@ -774,7 +774,7 @@ onUnmounted(() => { .tag-chip { font-size: 0.7rem; background: color-mix(in srgb, var(--fs-accent) 15%, transparent); - color: var(--fs-accent); + color: var(--fs-accent-fg); border-radius: 999px; padding: 0.1rem 0.4rem; } @@ -928,7 +928,7 @@ onUnmounted(() => { } .peek-linked-item:hover { background: color-mix(in srgb, var(--fs-accent) 8%, var(--fs-surface-raised)); - color: var(--fs-accent); + color: var(--fs-accent-fg); } .peek-linked-type { diff --git a/frontend/src/views/KnowledgeView.vue b/frontend/src/views/KnowledgeView.vue index bb38f57..46ad3ed 100644 --- a/frontend/src/views/KnowledgeView.vue +++ b/frontend/src/views/KnowledgeView.vue @@ -753,7 +753,7 @@ onUnmounted(() => { } .filter-btn.active .filter-count { background: color-mix(in srgb, var(--fs-accent) 20%, transparent); - color: var(--fs-accent); + color: var(--fs-accent-fg); } .filter-tag { font-size: 0.78rem; } @@ -926,7 +926,7 @@ onUnmounted(() => { border-radius: 4px; white-space: nowrap; background: color-mix(in srgb, var(--fs-text-secondary) 15%, transparent); - color: var(--fs-text-secondary); + color: var(--fs-text-secondary-fg); } /* ── Task card ──────────────────────────────────────────── */ diff --git a/frontend/src/views/NoteViewerView.vue b/frontend/src/views/NoteViewerView.vue index 9c09f32..9f65ae4 100644 --- a/frontend/src/views/NoteViewerView.vue +++ b/frontend/src/views/NoteViewerView.vue @@ -416,7 +416,7 @@ async function convertToTask() { } .badge-note { background: color-mix(in srgb, var(--fs-accent) 12%, transparent); - color: var(--fs-accent); + color: var(--fs-accent-fg); border: 1px solid color-mix(in srgb, var(--fs-accent) 25%, transparent); } .badge-task { diff --git a/frontend/src/views/ProjectView.vue b/frontend/src/views/ProjectView.vue index 512d677..4901c3d 100644 --- a/frontend/src/views/ProjectView.vue +++ b/frontend/src/views/ProjectView.vue @@ -1287,8 +1287,8 @@ async function confirmDelete() { .stat-todo { background: color-mix(in srgb, var(--fs-text-tertiary) 8%, transparent); color: var(--fs-text-secondary); border-color: var(--fs-border-color); } .stat-inprogress { background: color-mix(in srgb, #3b82f6 10%, transparent); color: #3b82f6; border-color: color-mix(in srgb, #3b82f6 28%, transparent); } -.stat-done { background: color-mix(in srgb, var(--fs-success) 10%, transparent); color: var(--fs-success); border-color: color-mix(in srgb, var(--fs-success) 28%, transparent); } -.stat-notes { background: color-mix(in srgb, var(--fs-accent) 8%, transparent); color: var(--fs-accent); border-color: color-mix(in srgb, var(--fs-accent) 22%, transparent); } +.stat-done { background: color-mix(in srgb, var(--fs-success) 10%, transparent); color: var(--fs-success-fg); border-color: color-mix(in srgb, var(--fs-success) 28%, transparent); } +.stat-notes { background: color-mix(in srgb, var(--fs-accent) 8%, transparent); color: var(--fs-accent-fg); border-color: color-mix(in srgb, var(--fs-accent) 22%, transparent); } /* ── Pattern-library coverage card ───────────────────────────── */ .coverage-card { @@ -1471,7 +1471,7 @@ async function confirmDelete() { .tab-btn.active .tab-count { background: color-mix(in srgb, var(--fs-accent) 12%, transparent); border-color: color-mix(in srgb, var(--fs-accent) 30%, transparent); - color: var(--fs-accent); + color: var(--fs-accent-fg); } /* ── Tasks view ──────────────────────────────────────────────── */ @@ -1707,7 +1707,7 @@ async function confirmDelete() { border-radius: 3px; margin-left: auto; } -.col-add-btn:hover { color: var(--fs-accent); background: color-mix(in srgb, var(--fs-accent) 10%, transparent); } +.col-add-btn:hover { color: var(--fs-accent-fg); background: color-mix(in srgb, var(--fs-accent) 10%, transparent); } .kanban-cards { display: flex; flex-direction: column; gap: 0.3rem; } diff --git a/frontend/src/views/SettingsView.vue b/frontend/src/views/SettingsView.vue index 2d94d75..360447a 100644 --- a/frontend/src/views/SettingsView.vue +++ b/frontend/src/views/SettingsView.vue @@ -2732,7 +2732,7 @@ async function deleteUser(userId: number) { background: var(--fs-surface-raised); } .sidebar-item.active { - color: var(--fs-accent); + color: var(--fs-accent-fg); background: color-mix(in srgb, var(--fs-accent) 8%, transparent); border-left-color: var(--fs-accent); font-weight: 500; @@ -3099,7 +3099,7 @@ async function deleteUser(userId: number) { border-radius: var(--fs-radius-sm); } .role-admin { - color: var(--fs-accent); + color: var(--fs-accent-fg); background: color-mix(in srgb, var(--fs-accent) 15%, transparent); } .role-user { @@ -3179,9 +3179,9 @@ async function deleteUser(userId: number) { text-transform: uppercase; letter-spacing: 0.05em; padding: 0.1rem 0.35rem; border-radius: var(--fs-radius-sm); } -.cat-audit { color: var(--fs-accent); background: color-mix(in srgb, var(--fs-accent) 15%, transparent); } -.cat-usage { color: var(--fs-success); background: color-mix(in srgb, var(--fs-success) 15%, transparent); } -.cat-error { color: var(--fs-error); background: color-mix(in srgb, var(--fs-error) 15%, transparent); } +.cat-audit { color: var(--fs-accent-fg); background: color-mix(in srgb, var(--fs-accent) 15%, transparent); } +.cat-usage { color: var(--fs-success-fg); background: color-mix(in srgb, var(--fs-success) 15%, transparent); } +.cat-error { color: var(--fs-error-fg); background: color-mix(in srgb, var(--fs-error) 15%, transparent); } .method-tag { display: inline-block; font-size: 0.65rem; font-weight: 500; font-family: monospace; @@ -3346,8 +3346,8 @@ async function deleteUser(userId: number) { padding: 0.15rem 0.4rem; border-radius: 4px; } -.role-owner { background: color-mix(in srgb, var(--fs-warning) 15%, transparent); color: var(--fs-warning); } -.role-member { background: color-mix(in srgb, var(--fs-text-tertiary) 15%, transparent); color: var(--fs-text-tertiary); } +.role-owner { background: color-mix(in srgb, var(--fs-warning) 15%, transparent); color: var(--fs-warning-fg); } +.role-member { background: color-mix(in srgb, var(--fs-text-tertiary) 15%, transparent); color: var(--fs-text-tertiary-fg); } .members-empty { color: var(--fs-text-tertiary); @@ -3528,7 +3528,7 @@ async function deleteUser(userId: number) { .day-btn.active { background: color-mix(in srgb, var(--fs-accent) 15%, transparent); border-color: var(--fs-accent); - color: var(--fs-accent); + color: var(--fs-accent-fg); font-weight: 500; } diff --git a/frontend/src/views/SharedWithMeView.vue b/frontend/src/views/SharedWithMeView.vue index 13d7e75..fe5720b 100644 --- a/frontend/src/views/SharedWithMeView.vue +++ b/frontend/src/views/SharedWithMeView.vue @@ -242,9 +242,9 @@ onMounted(async () => { border-radius: 4px; white-space: nowrap; } -.perm-viewer { background: color-mix(in srgb, var(--fs-text-tertiary) 15%, transparent); color: var(--fs-text-tertiary); } -.perm-editor { background: color-mix(in srgb, var(--fs-accent) 15%, transparent); color: var(--fs-accent); } -.perm-admin { background: color-mix(in srgb, var(--fs-warning) 15%, transparent); color: var(--fs-warning); } +.perm-viewer { background: color-mix(in srgb, var(--fs-text-tertiary) 15%, transparent); color: var(--fs-text-tertiary-fg); } +.perm-editor { background: color-mix(in srgb, var(--fs-accent) 15%, transparent); color: var(--fs-accent-fg); } +.perm-admin { background: color-mix(in srgb, var(--fs-warning) 15%, transparent); color: var(--fs-warning-fg); } .empty-msg { margin: 0; diff --git a/frontend/src/views/SnippetDetailView.vue b/frontend/src/views/SnippetDetailView.vue index 08657dc..f7d2720 100644 --- a/frontend/src/views/SnippetDetailView.vue +++ b/frontend/src/views/SnippetDetailView.vue @@ -288,7 +288,7 @@ async function confirmDelete() { font-family: var(--fs-font-mono); font-size: 0.82rem; background: color-mix(in srgb, var(--fs-accent) 12%, transparent); - color: var(--fs-accent); + color: var(--fs-accent-fg); padding: 0.08rem 0.35rem; border-radius: var(--fs-radius-sm); word-break: break-all; diff --git a/frontend/src/views/SnippetListView.vue b/frontend/src/views/SnippetListView.vue index 4a739ad..99541e9 100644 --- a/frontend/src/views/SnippetListView.vue +++ b/frontend/src/views/SnippetListView.vue @@ -710,7 +710,7 @@ function usageTitle(s: SnippetListItem): string { flex-shrink: 0; white-space: nowrap; background: color-mix(in srgb, var(--fs-accent) 15%, transparent); - color: var(--fs-accent); + color: var(--fs-accent-fg); } .snippet-when { @@ -739,7 +739,7 @@ function usageTitle(s: SnippetListItem): string { border-radius: 4px; white-space: nowrap; background: color-mix(in srgb, var(--fs-text-tertiary) 15%, transparent); - color: var(--fs-text-tertiary); + color: var(--fs-text-tertiary-fg); } .dup-action { @@ -754,7 +754,7 @@ function usageTitle(s: SnippetListItem): string { border-radius: 4px; white-space: nowrap; background: color-mix(in srgb, var(--fs-error) 15%, transparent); - color: var(--fs-error); + color: var(--fs-error-fg); } .usage-tag { @@ -764,14 +764,14 @@ function usageTitle(s: SnippetListItem): string { white-space: nowrap; font-variant-numeric: tabular-nums; background: color-mix(in srgb, var(--fs-text-tertiary) 15%, transparent); - color: var(--fs-text-tertiary); + color: var(--fs-text-tertiary-fg); } /* Dead weight is a nudge, not an error — it warns in the warning colour rather than the danger one, because the record isn't broken, just unearned. */ .usage-tag.usage-dead { background: color-mix(in srgb, var(--fs-warning) 18%, transparent); - color: var(--fs-warning); + color: var(--fs-warning-fg); } /* Header + select-mode */ diff --git a/scripts/check_design_tokens.py b/scripts/check_design_tokens.py index 6670c7e..f6db544 100644 --- a/scripts/check_design_tokens.py +++ b/scripts/check_design_tokens.py @@ -119,15 +119,20 @@ def same_hue_text_on_tint(css: str) -> tuple[list[str], list[str]]: dangerous of the two — it does not even name a `-bg` token, so nothing about it looks like the pattern until you measure it. - Returned separately because they are at different stages. The token form - is CLEAN and therefore gates. The inline form has a live backlog (48 sites - when this split was written, 26 of them --fs-accent), so it reports with a - count: a gate nobody can satisfy today gets switched off, and then it - guards nothing. + Returned separately because they were paid down separately — the token + form first (7 badge pairs), then the inline form (46 sites across 18 + files, 26 of them --fs-accent). Both are clean now, so BOTH gate. The + split is kept because the two spellings need different error text: one + names a -bg token you can search for, the other names nothing at all. """ token_form, inline_form = [], [] for body in re.findall(r"\{([^{}]*)\}", css): - fg = set(re.findall(r"color\s*:\s*var\(\s*(--fs-[\w-]+?)\s*\)", body)) + # (? int: print("OK — no text painted with a token on a tint of its own -bg.\n") if inline_tint_hits: - by_tok: dict[str, int] = {} - for _p, tok in inline_tint_hits: - by_tok[tok] = by_tok.get(tok, 0) + 1 - print(f"REPORT — {len(inline_tint_hits)} rule(s) paint text with a token on " - f"an INLINE color-mix tint of that same token.") - print(" Same defect, spelled without a -bg token so it does not gate yet.") - print(" Worst offenders: " + ", ".join( - f"{t} x{n}" for t, n in sorted(by_tok.items(), key=lambda kv: -kv[1])[:4]) + "\n") + print(f"FAIL — {len(inline_tint_hits)} rule(s) paint text with a token on an " + f"INLINE color-mix tint of that same token.") + print(" Identical defect to the block above, spelled without a -bg token —") + print(" which is what let it hide: nothing about it LOOKS like the pattern.") + print(" Use the token's -fg sibling.\n") + for path, tok in inline_tint_hits: + print(f" {path}: color: var({tok}) on an inline tint -> var({tok}-fg)") + print() + else: + print("OK — no text painted with a token on an inline tint of itself.\n") if args.report_literals: print(f"REPORT — {literal_count} raw colour literal(s) in component CSS.") print(" Advisory: a literal is a value stated outside the system, so it " "cannot follow a palette change.\n") - return 1 if (unresolved or same_hue_hits) else 0 + return 1 if (unresolved or same_hue_hits or inline_tint_hits) else 0 if __name__ == "__main__":