414. A rank applies to anything, so its name is reserved
Date: 2026-09-19
Status
Accepted. Closes the TODO.md entry opened by
ADR-0153.
Context
The entry is one incident and one prediction:
A widget’s CSS classes share a namespace with the design system’s. A
hudreading nameddisplaypicked up §1.4’s.displaytype rank and rendered at 28px. Renamed, and nothing prevents the next one: there is no prefix convention, no check, and the two sets of names are written in different files by different people.
The prediction was right, twice over. Both are fixed here, and neither was visible in a sheet, a test or a review.
tree-row.heading, which drew at the wrong size for as long as it existed
TreeRow.classes() adds heading to a row that cannot be chosen — a parent in a
leaf-only tree, which is §3’s default, so this is every branch of every ordinary
tree. controls.css styles it:
/* A parent in a leaf-only tree: still a row, still openable, and not an answer.
Dimmed rather than disabled, because disabled would say it is inert. */
tree-row.heading {
color: var(--gb-text-muted);
}
One property. Meanwhile .heading — §1.4’s rank, written with no type on it
because a rank applies to anything — set font-size: 15px, line-height: 20px
and font-weight: 600 on the same element, and all three inherit into
tree-label. So “Europe” and “United Kingdom” drew larger and bolder than
“Norway” and “Scotland” beneath them, inside a row whose height is
--gb-list-row-height and does not grow. The rule that was supposed to make a
branch quieter than a leaf made it louder.
Three goldens have been drawing it since the widget shipped.
skeleton-bar.title, which was waiting
Skeleton.Shape.TITLE.cssClass() was title, put on the skeleton and on each
of its bars, and skeleton-bar.title sets a height. .title set 20px/26px/600
on both nodes behind it. Nothing drew text in either, and the bar’s height is an
absolute var(--gb-font-title), so no pixel moved — which is precisely why
it survived. It is the same defect with the consequence not yet attached, and the
first dimension anybody writes in em attaches it.
Decision
The convention is reservation, not a prefix
The entry guesses a prefix and the guess is wrong, for a reason worth stating: both sets of names are published vocabulary.
- A rank is what an application writes:
text class="heading",text style="title",TextRank.HEADING, and §1.4’s table. Prefixing it to.gds-headingrenames the toolkit’s most-typed word in four places at once and breaks every application stylesheet that mentions it. - A widget’s class is what a stylesheet targets:
tree-row.selected,button.primary,hud-reading.paint. Prefixing those to.gb-selectedrenames forty-odd published modifiers across 188 files and several thousand lines of sheet.
And a prefix on the widget side buys nothing, because a widget’s class is
already namespaced — by its type. selected means nothing on its own;
tree-row.selected does. That is the asymmetry the whole convention rests on:
§1.4’s seven type ranks are the only class names the base layer styles unqualified. Those seven names are reserved. Every other class a toolkit widget mints is a modifier of its own type, is written with that type in front of it, and may not be one of the seven.
A gb- prefix would be a second namespacing mechanism stacked on one that
already works, paid for with a breaking rename of one of the two vocabularies.
Reserving seven words costs one rename per collision, and there were two.
The check reads both sets rather than listing either
ClassNamespaceTest, four assertions, and the reserved set is never written down:
-
The base sheet’s unqualified class rules are exactly
TextRank’s values. Parsed by the real parser, matched as “one compound, one class, no type, no id, no pseudo-class” — which is what “applies to anything” means operationally rather than a proxy for it. An eighth unqualified rule is either a rank, and belongs inTextRankand in §1.4’s table, or a widget class that forgot its type. This is the convention as an assertion, and it is what stops the check rotting when §1.4 grows. -
No class is written both unqualified and beside a type. This is the
tree-row.headingfinding, read straight out of the sheet with no reflection and no instances — and it is the only one of the four that reaches a widget part.tree-rowis not a registered node name and no inflater builds one. -
No registered widget’s
classes()orclasses(FrameStats)is a reserved name. The case the sheet cannot answer: a collision nobody styled, where there is no rule to read and the widget simply draws at the rank’s size. -
No enum that mints a CSS class mints a reserved one, except
TextRank. This is the shape thehud’sdisplayactually had, and the one the first three would still miss: the name was not a literal in aclasses()body and the widget was not a registered node — it was an enum constant on a part, read throughcssClass(). It is also howskeleton-bar.titlewas found.
TextRank is the one exception in (4), and it is the definition rather than a
weakening: an enum whose entire job is to spell §1.4’s ranks has to spell them.
Alternatives considered
- Prefix the ranks. Above. It is the cheapest change to make and the most expensive to have made, because the ranks are the vocabulary a document author types.
- Prefix every widget class. 188 files, ~5 000 lines of sheet, and it duplicates the type qualifier that already separates them.
- Check the CSS only. Assertion (2) alone would have caught both of today’s collisions and costs nothing to run. It also cannot see a collision nobody styled, which is the more dangerous one — a widget wearing a rank with no rule of its own has nothing pointing at the problem at all.
- Enumerate widget classes by scanning class-file constant pools. Tried in
outline and rejected:
Dialog,GroupBoxandCollapseall carry the string"title"because it is a KDL property name, so the pool cannot tell a class the widget mints from an attribute it reads. A check whose first three findings are false is a check somebody adds an exclusion list to. - Leave
skeleton-bar.title, since no pixel moves. It is a collision that has not cashed in yet, in a widget whose whole purpose is to be “sized from the typography token it stands in for” — the one place in the catalog most likely to grow a relative unit.
Consequences
- Three tree goldens move:
tree-dark,tree-lightandtree-cascade-dark, 3.38 % of pixels each. Branch labels drop from 15px/600 to the 13px/400 of the leaves beside them, and stay dimmed, which is whattree-row.heading’s comment said it was doing all along. The goldens are not re-blessed here; they are the deliverable of this ADR and want a human to look at the diff. tree-row.headingistree-row.group. Neither the class nor the selector appears indocs/or anywhere inbook/, and no test asserted on it, so nothing outside the two files changed.Skeleton.Shapemintsshape-text,shape-title,shape-circleandshape-rect. The whole family, not the one that collided: three of them are safe only because §1.4 happens not to have a rank calledcircle, which is not a property anybody is maintaining, andskeleton-bar.textread like thetexttype into the bargain. §5’sshape="title"is unchanged — what an application writes is a published word, what a widget puts in a class set is the widget’s own business, and that asymmetry is the whole reason this rename cost nothing.SkeletonTestasserts both halves.- A widget may no longer name a class
display,title,heading,body,body-strong,captionormono. Seven words, and the failure message says which one and why. - The check does not police an application’s own classes, and cannot. An
application that writes
class="caption"on a node is using the rank, which is what ranks are for. What this constrains is the toolkit. .md-*and.html-*were already right. The two view modules invent the most class names of anything here — some fifty between them — and every one is prefixed, including.html-caption, which is one letter from a collision and does not have one. The convention this writes down is one two modules had already arrived at; what was missing was anything that said so.