Tool disambiguators, kind badges, and a badge layer that clears AA (#3123, #3124, #3132) #134

Merged
bvandeusen merged 4 commits from dev into main 2026-08-27 21:28:48 -04:00
8 changed files with 87 additions and 26 deletions
Showing only changes of commit 0c74dc8275 - Show all commits
+14
View File
@@ -12,6 +12,13 @@
file used to read, and it is deliberate: the light palette was never specified file used to read, and it is deliberate: the light palette was never specified
by any rule, so it is recorded as a departure rather than as the default. by any rule, so it is recorded as a departure rather than as the default.
The -fg tokens are a badge's TEXT colour, added because the ladder used its
raw hue as text on a 12% tint of the same hue — measured 1.60-2.97:1 on the
dark palette against the kit's AA floor of 4.5. Each is 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 is what makes one
declaration cover both: it inverts, so the text follows the mode.
Only 12 tokens differ between modes. Everything else — spacing, type, motion, Only 12 tokens differ between modes. Everything else — spacing, type, motion,
radius, and every derived colour — is stated once, because a value built with radius, and every derived colour — is stated once, because a value built with
var() resolves where it is USED, not where it is written. var() resolves where it is USED, not where it is written.
@@ -78,10 +85,13 @@
/* priority */ /* priority */
--fs-priority-low: var(--fs-info); --fs-priority-low: var(--fs-info);
--fs-priority-low-bg: color-mix(in srgb, var(--fs-priority-low) 12%, transparent); --fs-priority-low-bg: color-mix(in srgb, var(--fs-priority-low) 12%, transparent);
--fs-priority-low-fg: color-mix(in srgb, var(--fs-priority-low) 45%, var(--fs-text-primary)); /* Badge TEXT for low priority — the readable partner of the -bg tint */
--fs-priority-medium: var(--fs-warning); --fs-priority-medium: var(--fs-warning);
--fs-priority-medium-bg: color-mix(in srgb, var(--fs-priority-medium) 12%, transparent); --fs-priority-medium-bg: color-mix(in srgb, var(--fs-priority-medium) 12%, transparent);
--fs-priority-medium-fg: color-mix(in srgb, var(--fs-priority-medium) 55%, var(--fs-text-primary)); /* Badge TEXT for medium priority */
--fs-priority-high: var(--fs-error); --fs-priority-high: var(--fs-error);
--fs-priority-high-bg: color-mix(in srgb, var(--fs-priority-high) 12%, transparent); --fs-priority-high-bg: color-mix(in srgb, var(--fs-priority-high) 12%, transparent);
--fs-priority-high-fg: color-mix(in srgb, var(--fs-priority-high) 55%, var(--fs-text-primary)); /* Badge TEXT for high priority */
/* radius */ /* radius */
--fs-radius-sm: 4px; /* pills, tags, code spans */ --fs-radius-sm: 4px; /* pills, tags, code spans */
@@ -116,12 +126,16 @@
/* status */ /* status */
--fs-status-todo: var(--fs-border-color); --fs-status-todo: var(--fs-border-color);
--fs-status-todo-bg: color-mix(in srgb, var(--fs-status-todo) 12%, transparent); --fs-status-todo-bg: color-mix(in srgb, var(--fs-status-todo) 12%, transparent);
--fs-status-todo-fg: color-mix(in srgb, var(--fs-status-todo) 40%, var(--fs-text-primary)); /* Badge TEXT for a not-started task */
--fs-status-in-progress: var(--fs-accent); --fs-status-in-progress: var(--fs-accent);
--fs-status-in-progress-bg: color-mix(in srgb, var(--fs-status-in-progress) 12%, transparent); --fs-status-in-progress-bg: color-mix(in srgb, var(--fs-status-in-progress) 12%, transparent);
--fs-status-in-progress-fg: color-mix(in srgb, var(--fs-status-in-progress) 45%, var(--fs-text-primary)); /* Badge TEXT for a task underway */
--fs-status-done: var(--fs-success); --fs-status-done: var(--fs-success);
--fs-status-done-bg: color-mix(in srgb, var(--fs-status-done) 12%, transparent); --fs-status-done-bg: color-mix(in srgb, var(--fs-status-done) 12%, transparent);
--fs-status-done-fg: color-mix(in srgb, var(--fs-status-done) 50%, var(--fs-text-primary)); /* Badge TEXT for a completed task */
--fs-overdue: var(--fs-error); --fs-overdue: var(--fs-error);
--fs-status-cancelled: var(--fs-text-tertiary); /* set aside, not failed */ --fs-status-cancelled: var(--fs-text-tertiary); /* set aside, not failed */
--fs-status-cancelled-fg: color-mix(in srgb, var(--fs-status-cancelled) 60%, var(--fs-text-primary)); /* Badge TEXT for a cancelled task */
/* surface */ /* surface */
--fs-surface-page: #14171A; /* page bg, deepest surface */ --fs-surface-page: #14171A; /* page bg, deepest surface */
+3 -3
View File
@@ -34,14 +34,14 @@ const labels: Record<TaskPriority, string> = {
} }
.priority-low { .priority-low {
background: var(--fs-priority-low-bg); background: var(--fs-priority-low-bg);
color: var(--fs-priority-low); color: var(--fs-priority-low-fg);
} }
.priority-medium { .priority-medium {
background: var(--fs-priority-medium-bg); background: var(--fs-priority-medium-bg);
color: var(--fs-priority-medium); color: var(--fs-priority-medium-fg);
} }
.priority-high { .priority-high {
background: var(--fs-priority-high-bg); background: var(--fs-priority-high-bg);
color: var(--fs-priority-high); color: var(--fs-priority-high-fg);
} }
</style> </style>
+3 -3
View File
@@ -153,7 +153,7 @@ watch(() => [props.projectId, props.designSystemId], run);
} }
.pdt-clean { .pdt-clean {
color: var(--fs-status-done); color: var(--fs-status-done-fg);
} }
.pdt-summary { .pdt-summary {
@@ -206,12 +206,12 @@ watch(() => [props.projectId, props.designSystemId], run);
.pdt-tag.unknown { .pdt-tag.unknown {
background: var(--fs-priority-high-bg); background: var(--fs-priority-high-bg);
color: var(--fs-priority-high); color: var(--fs-priority-high-fg);
} }
.pdt-tag.local { .pdt-tag.local {
background: var(--fs-priority-medium-bg); background: var(--fs-priority-medium-bg);
color: var(--fs-priority-medium); color: var(--fs-priority-medium-fg);
} }
.pdt-tag.superseded { .pdt-tag.superseded {
+12 -8
View File
@@ -37,21 +37,25 @@ const labels: Record<TaskStatus, string> = {
text-transform: uppercase; text-transform: uppercase;
letter-spacing: 0.025em; letter-spacing: 0.025em;
} }
/* Text comes from the -fg tokens, which are the hue mixed toward
--fs-text-primary until they clear AA. The old spelling darkened the hue
with `#000 15%` — a light-mode instinct that made these WORSE on the dark
palette, where the surface is already near-black, and a literal besides. */
.status-todo { .status-todo {
background: color-mix(in srgb, var(--fs-status-todo-bg) 78%, var(--fs-status-todo) 22%); background: var(--fs-status-todo-bg);
color: color-mix(in srgb, var(--fs-status-todo) 85%, #000 15%); color: var(--fs-status-todo-fg);
} }
.status-in_progress { .status-in_progress {
background: color-mix(in srgb, var(--fs-status-in-progress-bg) 78%, var(--fs-status-in-progress) 22%); background: var(--fs-status-in-progress-bg);
color: color-mix(in srgb, var(--fs-status-in-progress) 85%, #000 15%); color: var(--fs-status-in-progress-fg);
} }
.status-done { .status-done {
background: color-mix(in srgb, var(--fs-status-done-bg) 78%, var(--fs-status-done) 22%); background: var(--fs-status-done-bg);
color: color-mix(in srgb, var(--fs-status-done) 85%, #000 15%); color: var(--fs-status-done-fg);
} }
.status-cancelled { .status-cancelled {
background: color-mix(in srgb, var(--fs-surface-raised) 78%, var(--fs-text-tertiary) 22%); background: var(--fs-status-todo-bg);
color: var(--fs-text-tertiary); color: var(--fs-status-cancelled-fg);
} }
.clickable { .clickable {
cursor: pointer; cursor: pointer;
+2 -2
View File
@@ -554,8 +554,8 @@ async function confirmDelete() {
/* The two bases must never look alike — one is mechanical, the other is the /* The two bases must never look alike — one is mechanical, the other is the
reviewer's judgment, and that difference is the whole decision. */ reviewer's judgment, and that difference is the whole decision. */
.area-basis { font-size: 0.68rem; border-radius: var(--fs-radius-sm); padding: 0.05rem 0.4rem; } .area-basis { font-size: 0.68rem; border-radius: var(--fs-radius-sm); padding: 0.05rem 0.4rem; }
.area-basis--exact { background: var(--fs-status-done-bg); color: var(--fs-status-done); } .area-basis--exact { background: var(--fs-status-done-bg); color: var(--fs-status-done-fg); }
.area-basis--overlap { background: var(--fs-priority-medium-bg); color: var(--fs-priority-medium); } .area-basis--overlap { background: var(--fs-priority-medium-bg); color: var(--fs-priority-medium-fg); }
.area-offer { .area-offer {
display: flex; display: flex;
+2 -2
View File
@@ -1427,12 +1427,12 @@ textarea.input {
.spec-status.violated { .spec-status.violated {
background: var(--fs-priority-high-bg); background: var(--fs-priority-high-bg);
color: var(--fs-priority-high); color: var(--fs-priority-high-fg);
} }
.spec-status.missing { .spec-status.missing {
background: var(--fs-priority-medium-bg); background: var(--fs-priority-medium-bg);
color: var(--fs-priority-medium); color: var(--fs-priority-medium-fg);
} }
.sheet { .sheet {
+7 -7
View File
@@ -946,10 +946,10 @@ onUnmounted(() => {
border-radius: 8px; border-radius: 8px;
font-weight: 500; font-weight: 500;
} }
.status--todo { background: var(--fs-status-todo-bg); color: var(--fs-status-todo); } .status--todo { background: var(--fs-status-todo-bg); color: var(--fs-status-todo-fg); }
.status--in_progress { background: var(--fs-status-in-progress-bg); color: var(--fs-status-in-progress); } .status--in_progress { background: var(--fs-status-in-progress-bg); color: var(--fs-status-in-progress-fg); }
.status--done { background: var(--fs-status-done-bg); color: var(--fs-status-done); } .status--done { background: var(--fs-status-done-bg); color: var(--fs-status-done-fg); }
.status--cancelled { background: var(--fs-status-todo-bg); color: var(--fs-status-todo); text-decoration: line-through; } .status--cancelled { background: var(--fs-status-todo-bg); color: var(--fs-status-cancelled-fg); text-decoration: line-through; }
.priority-badge { .priority-badge {
font-size: 0.7rem; font-size: 0.7rem;
@@ -957,9 +957,9 @@ onUnmounted(() => {
border-radius: 8px; border-radius: 8px;
font-weight: 500; font-weight: 500;
} }
.priority--low { background: var(--fs-priority-low-bg); color: var(--fs-priority-low); } .priority--low { background: var(--fs-priority-low-bg); color: var(--fs-priority-low-fg); }
.priority--normal { background: var(--fs-priority-medium-bg); color: var(--fs-priority-medium); } .priority--normal { background: var(--fs-priority-medium-bg); color: var(--fs-priority-medium-fg); }
.priority--high { background: var(--fs-priority-high-bg); color: var(--fs-priority-high); } .priority--high { background: var(--fs-priority-high-bg); color: var(--fs-priority-high-fg); }
.task-due { .task-due {
font-size: 0.78rem; font-size: 0.78rem;
+44 -1
View File
@@ -90,6 +90,32 @@ def style_source(path: pathlib.Path) -> str:
return CSS_COMMENT.sub(" ", css) return CSS_COMMENT.sub(" ", css)
# A rule that paints text with a colour token AND its own -bg tint of the same
# token. The pair looks harmonious and is close to illegible: a 12% tint of a
# hue sits near the surface, so the hue as text on it lands around 2:1 against
# an AA floor of 4.5. Measured across the whole Scribe ladder in 2026-08:
# every one of the six pairs failed on the dark palette, worst 1.60:1.
#
# The fix is always the same and always available — the token's `-fg` sibling,
# which is the hue mixed toward --fs-text-primary far enough to clear AA. So
# this FAILS rather than reports: unlike a raw literal, there is nothing to
# weigh up.
SAME_TOKEN_PAIR = re.compile(
r"color\s*:\s*var\(\s*(--fs-[\w-]+?)\s*\)" # color: var(--fs-X)
r"|background(?:-color)?\s*:\s*var\(\s*(--fs-[\w-]+?)-bg\s*\)"
)
def same_hue_text_on_tint(css: str) -> list[str]:
"""Tokens used as TEXT on a tint of themselves, within one rule block."""
hits = []
for body in re.findall(r"\{([^{}]*)\}", css):
fg = set(re.findall(r"color\s*:\s*var\(\s*(--fs-[\w-]+?)\s*\)", body))
bg = set(re.findall(r"background(?:-color)?\s*:\s*var\(\s*(--fs-[\w-]+?)-bg\s*\)", body))
hits.extend(sorted(fg & bg))
return hits
def main() -> int: def main() -> int:
parser = argparse.ArgumentParser(description=__doc__) parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--sheet", default="frontend/src/assets/theme.css") parser.add_argument("--sheet", default="frontend/src/assets/theme.css")
@@ -117,6 +143,7 @@ def main() -> int:
) )
unresolved: list[tuple[pathlib.Path, str]] = [] unresolved: list[tuple[pathlib.Path, str]] = []
same_hue_hits: list[tuple[pathlib.Path, str]] = []
superseded_hits: list[tuple[pathlib.Path, str, str]] = [] superseded_hits: list[tuple[pathlib.Path, str, str]] = []
literal_count = 0 literal_count = 0
@@ -140,6 +167,9 @@ def main() -> int:
literal_count += len(HEX_LITERAL.findall(css)) literal_count += len(HEX_LITERAL.findall(css))
for tok in same_hue_text_on_tint(css):
same_hue_hits.append((path, tok))
if unresolved: if unresolved:
print(f"FAIL — {len(unresolved)} unresolvable var() reference(s).") print(f"FAIL — {len(unresolved)} unresolvable var() reference(s).")
print(" These render as the fallback if given one, or as nothing at all.") print(" These render as the fallback if given one, or as nothing at all.")
@@ -162,12 +192,25 @@ def main() -> int:
print(f" {path}: {literal} -> {token}") print(f" {path}: {literal} -> {token}")
print() print()
if same_hue_hits:
print(f"FAIL — {len(same_hue_hits)} rule(s) paint text with a token on a "
f"tint of that same token.")
print(" A 12% tint sits near the surface, so the hue as text on it lands "
"around 2:1 against AA's 4.5.")
print(" Use the token's -fg sibling, which is mixed toward "
"--fs-text-primary until it clears the floor.\n")
for path, tok in same_hue_hits:
print(f" {path}: color: var({tok}) on var({tok}-bg) -> var({tok}-fg)")
print()
else:
print("OK — no text painted with a token on a tint of itself.\n")
if args.report_literals: if args.report_literals:
print(f"REPORT — {literal_count} raw colour literal(s) in component CSS.") 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 " print(" Advisory: a literal is a value stated outside the system, so it "
"cannot follow a palette change.\n") "cannot follow a palette change.\n")
return 1 if unresolved else 0 return 1 if (unresolved or same_hue_hits) else 0
if __name__ == "__main__": if __name__ == "__main__":