Fluent-next: validate role assignment across all components - #35107
Open
EugeniyKiyashko wants to merge 23 commits into
Open
Fluent-next: validate role assignment across all components#35107EugeniyKiyashko wants to merge 23 commits into
EugeniyKiyashko wants to merge 23 commits into
Conversation
… (tools/review/roles.mjs)
Nothing checked the CHOICE of role: the enforcer checks the name, the resolve diff checks
that a value did not move, the reachability audit checks delivery, the screenshots check the
cascade. A role that is wrong but plausible passes all five and shows up only in dark mode,
where every etalon is .light.
The report cross-tabulates role -> slot per component against
components/{core,vnext,blazor,wpf} of the token package (measured on 262.16.0: its colour
leaves are ~99% plain references to the semantic roles, so it carries the mapping and no
value of its own), and resolves both sides per mode to say whether a correction would move
a pixel.
714 colour declarations: 247 agree with the package, 39 agree on a kindred slot, 24 read a
role the package uses only for another kind of slot, 11 sit in a family the package never
uses there, 277 have no counterpart to compare against.
Ten of them are corrected here - all value-neutral in both modes, all moving a role into the
family its CSS property demands, with the package agreeing:
checkBox invalid mark background: content-danger-shared* -> bg-danger-shared*
switch on border, rest+hovered: bg-primary-shared* -> border-primary-shared*
tabs selected tab border x4: content-primary* -> border-primary-shared*
textEditor invalid text: border-danger-shared -> content-danger
switch's on-border-focused is left alone on purpose: the border role of the right family
differs in dark (#003c70 -> #5397), so it is a design decision, not hygiene.
Resolve diff 0 on 6428 declarations across both modes; jest 170/170, naming:check 0,
stylelint 0.
…nnot go stale The report alone rots: a role nobody has looked at joins the list silently, and a role somebody fixed leaves it without a trace of the decision. So the list is banked and compared for exact equality - a new disagreement and a resolved one both fail, and both are re-banked on purpose with UPDATE_ROLES_BASELINE=1 pnpm test. Two self-checks keep a green gate meaningful: the tool takes --theme= and is driven over a synthetic tree that plants a wrong-family role (must be caught) and a right one (must pass). Without them a scan that matched nothing would read as a clean bill. jest 174/174.
…the step reads
The marker says "the package has no role for this case" and leaves it there. Measured against the
role grid, the claim splits in two, and only one half is what the marker describes:
37 of 68 typography step reads are genuinely off the grid - font-weight 500, font-size
110/180/220/260/360, line-height 120/180. The report now names the nearest roles and their
steps, so each is a choice between the legacy value and the grid rather than an open question.
31 read a step the grid DOES name. Routing them is value-neutral by construction - a role that
names step X resolves to step X - but which role (caption or base or title at the same step)
is a decision, so none is applied here.
None of the four neighbours has this class: their component sets reference the typography roles and
a bare step three times in total, all on {font-weight.400}, which has a role. So the default reading
is not "the package is missing a role" - it is legacy fluent's value meeting the design system's
grid, and 13 hard font-weight: 500 in the legacy theme are where most of it comes from.
13 of the 68 carry no marker at all: px-audit only looks at literals and a step read is not a
literal, so they never reached SCALES.md or design. Banked so the next one cannot arrive silently.
jest 175/175, stylelint 0.
…st the built CSS
Adjudicating the first list turned up more instrument than finding: seven of the eleven family
conflicts were the tool's own noise. A shadow that lives on a content area was compared against
content roles, a scroll bar's thumb matched everything because it is ambiguous by design, an empty
candidate set read as a conflict, and `separator.color` - the package naming a component that IS
the slot - fell out of the comparison entirely, so every -separator-border in the theme looked like
a border nobody named. Fixed, and `cross-family` now means what it says: the families differ.
17 disagreements survive, each with a decision in the baseline:
confirmed 3 the package assigns this very role here - the switch knob is its `trigger`, painted
from content roles exactly as we do, and only our slot word says bg
rule-5 5 one value in two properties, named after the dominant role
naming 1 colorView's handle is a fill; color-bg is right and `content` is not
bridge 1 switch handle ring - rgb(from ... / .1), no shadow role exists
package-gap 1 loadIndicator's inner border wants color-border-primary-subtle, which does not exist
design 6 a correction moves a pixel. Two are defects in one mode only: valid-badge text is
#107c10 in dark where content-success gives #509f4b, and fieldset invalid text is
#ee726a in light where content-danger gives #c50f1f
Then the slot itself became checkable. NAMING.md says the CSS property decides it, and the built
bundle says which property each tier name reaches: 42 names disagree with their own code. 21 are
the name and not the role - fourteen filterBuilder `-content` variables reach base as
button-color(), which sets background-color, and their bg roles were right all along. 11 are a
deliberate idiom worth keeping: a hairline drawn with background-color stays a border role, the
same way in tabs, splitterBar and gridBase. 7 are rule-5 pairs, 1 is an inventoried divergence, and
2 want a call - notably list's disabled text, painted from a border role two steps paler than the
content one.
Both lists are banked with the reasoning and gated on exact equality; a banked entry with no
decision fails too. jest 178/178.
…t climb
The 105 role-new findings looked like a pile until they were split: 18 are a state the package does
not model, 87 are a different shade of the same family. Reading those turned up a class no package
comparison can see, because it is the theme against itself - a slot whose states resolve to one
role has a state in its name that the eye cannot find.
24 such slots. The honest question is not whether the package ladders that slot: its per-state role
sets are a union over every variant of a component, so a button's rest and selected differ merely
because different variants live in each, and the first two attempts at this check reported eight
buttons that were nothing. The question is about OUR role - we paint two states from role R, does
the design system ship R for the second state? That needs no component mapping, so it answers for
all 86 folders including the 22 the package has never heard of.
Two do:
accordion-title-bg hovered and active are both color-bg-hovered; color-bg-active exists
(#e1e1e1 light / #1d1d1d dark) and the theme uses it elsewhere. Pressing an
accordion header looks exactly like hovering it
tile-view-bg rest and hovered are one value while active is not - the slot means to
ladder and the hovered rung is flat
The other 22 collapse where the system collapses too, which is a decision rather than a gap:
disabled and read-only share a colour in checkBox, textEditor and radioButton; a selected tab keeps
its text colour because the indicator carries the selection. A focused state reusing hovered is
accepted convention and never listed at all.
jest 180/180, stylelint 0.
…th its background The gap this report keeps circling is dark mode: every screenshot etalon is .light and the axe rule reads text only, so a role that is right in light and wrong in dark has nothing watching it. Two earlier findings were exactly that shape - valid-badge text and the fieldset invalid message - and both were found by eye. This measures the class. No guessing which surface a text sits on: only pairs the bundle puts in ONE rule, resolved per mode from the tokens. 59 such pairs; 4 fall below AA and all four pass comfortably in light. Two exclusions, each because including them invents a number nobody sees. An alpha-bridge value renders as a tint over whatever is behind it, not as its role's opaque hex - the html editor's code block, rgb(from color-content-subtle r g b / .15), read as 1.62 against its own text before this was fixed, and it is the reason the rule exists. Disabled selectors are exempt from WCAG 1.4.3 and gated by disabled-paint.test.ts; eight of them were burying the live pairs. Both thresholds are reported and neither is chosen for you - 4.5:1 for text, 3:1 for a glyph: .dx-splitter .dx-resize-handle 2.8 dark - a grip, so 3:1 applies. Misses ...context-menu .dx-icon-trash item, focused 3.05 dark - a menu label, so 4.5:1. Misses .dx-checkbox-checked .dx-checkbox-icon 3.36 dark - a checkmark, clears 3:1 .dx-field-value.dx-attention::before 3.68 dark - a glyph, clears 3:1 The two that clear are banked too, so a drop below the threshold is caught rather than rediscovered. jest 182/182, naming:check 0.
Every check so far reads a single declaration. This one asks what the task is named after - does the
theme paint the same thing the same way everywhere? Group by what the name says the thing IS
(modifiers + slot + state, sub-elements dropped) and compare across components.
24 concepts get more than one role, 9 of them across families. The headline:
invalid bg rest - six components, six roles, three families
checkBox bg-danger-shared #c50f1f / #e4554f
common content-danger-shared #c50f1f / #e4554f the invalid badge
radioButton border-danger-shared #c50f1f / #e4554f
slider border-danger #ee726a / #e4554f pale in light only
progressBar bg-danger #c50f1f / #c50f1f does not lighten in dark
gridBase bg-danger-subtler #fceae7 / #470004 a cell tint, legitimately other
Three of those six paint the identical colour and spell it from three different families. That costs
nothing to unify and, until it is unified, the next palette change moves some of them and not the
others - which is the failure the 262.10.1 remap already described and could not act on. Two more
concepts have the same cluster, both in the invalid ladder.
The distinction is the point and took two passes to get right: a concept whose roles resolve to one
colour is a spelling problem, one whose roles differ by a shade is a difference two components can
honestly have, and one that differs across families with different values is a design call. Banked
as spelling 3, shade 15, design 6.
Worth noting what this catches that the per-declaration checks cannot: radioButton's border role on
a bg slot is banked as an accepted rule-5 pair, and common's content role on the invalid badge sits
in another bucket entirely - only grouping by concept shows they are the same paint.
jest 184/184, stylelint 0.
I had banked six items as "design" on the grounds that a correction moves a pixel. That conflated
two different things. Where the current role fails an objective threshold and the role the design
system names for that property passes it, the decision belongs to the standard.
Two are exactly that, and each changes one mode only - the other is already identical:
valid-badge-content color: from a bg role. light 5.37 either way; dark 2.89 -> 4.73
(#107c10 on #242424 against content-success #509f4b). Below 3:1, so it
failed even the graphic floor, as green text on the dark surface
fieldset-...-invalid color: from a border role. dark 4.22 either way; light 2.9 -> 6.07
(#ee726a on white against content-danger #c50f1f)
Resolve diff over both bundles: exactly one value moved per mode, each the failing one. Nothing
else in 6428 declarations.
Measuring the rest changed two more records without changing code:
the delete item in the message-list menu is not our choice to fix - 3.05 in dark against 4.5, and
content-danger-hovered, the strongest role in the family, reaches only 3.87 on the same surface.
No danger content role meets AA on its own hovered surface in dark. Reclassified to package-gap
the splitter grip keeps its design flag but now carries the floor and the option: 2.8 against 3:1,
color-content clears it at 4.54, and choosing it also darkens the grip in light from 6 to 11.15
One measurement I withdrew rather than used: I read the switch's focused border against the page and
got 1.39 in dark, which would be damning for a boundary - but an ON switch is filled, so its border
sits on its own fill and the number answers a question nobody asked.
Side effect worth having: the concept "invalid content rest" drops from three families to two, and fieldset now
agrees with stepper on content-danger.
jest 184/184, stylelint 0.
…ff a source
Asked where the roles came from, the answer turned out to be three different answers, and one of
them needed saying out loud.
Seven of the twelve corrections match what another product already assigns, exactly:
switch on-border, rest + hovered core AND vnext: switch.color.checked.border.* =
border-primary-shared / -hovered
tabs selected bar, four core: tabs.item.color.selector.selected* =
border-primary-shared / -hovered / -active. The package calls
the bar a `selector`, we call it a border - the role is the
same one, and our slot word is the part that differs
textEditor invalid text core AND vnext: text-input.color.invalid.content.rest =
content-danger, with border-danger-shared kept for the border
Two are decided by a threshold, not by anyone's taste: the valid badge and the fieldset invalid
message, each failing AA in one mode and identical in the other.
Three are mine, and they go AGAINST the package. The checkBox invalid mark reads background-color,
so NAMING.md's rule puts it in the bg family - but core and blazor both model that element as an
icon and paint it from content roles. Both spellings resolve to the same colour in both modes, so
nothing renders differently; what differs is which name moves at the next palette change. Recorded
as open rather than settled.
Also recorded, because a fixed item leaves the lists above and its reasoning leaves with it: the
hairline idiom (independently confirmed afterwards by core's `selector` slot), the fact that the
classifiers are my construction and have already been revised three times to remove false findings,
and the bound on the whole report - no Microsoft Fluent documentation was consulted at any point.
Everything Fluent here arrives through the DevExpress token package and the three sibling products.
Where all four are silent - 277 declarations across 22 folders - the report has no external
authority at all, only the theme against itself.
jest 185/185.
… what it contradicts Everything in this audit so far leaned on the DevExpress token package and the three sibling products. Fluent 2's own sources answer four of the open questions, and one answer is against me. Typography, the largest open cluster. The Fluent 2 web ramp has three weights - Regular, Semibold, Bold - and no Medium at all; its sizes are 10/12/14/16/20/24/28/32/40/68px, which the package grid mirrors exactly bar Display. So font-weight 500 and sizes 11/18/22/26/36px are legacy fluent values with no home in Fluent 2, and the package is faithful rather than short of steps. design#1555 asks which steps deserve roles; the question is the other way round - whether to keep the legacy values. 37 places, 17 of them the 500 weight. Focus. Fluent 2 draws it as a two-tone stroke, colorStrokeFocus1 #ffffff inside colorStrokeFocus2 #000000, precisely so it survives any background. The four focus roles the package ships and the theme reads nowhere are that, mirrored. Painting focus from an accent border role is not a shade away from Fluent 2, it is a different mechanism. The checkBox invalid mark - reverted. Fluent 2 builds a checkbox from colorCompoundBrandBackground for the box and colorNeutralForegroundInverted for the check: the mark is foreground. The package models it the same way, as an icon. I had moved it to a bg role on the strength of NAMING.md, but that rule decides the SLOT WORD from the CSS property and says nothing about the role family - reading it as both was mine. Value-neutral either way, and reverting also restores the checkbox's own consistency, since its icon fill was left on a content role. Resolve diff 0 on both bundles. Accordion hover and pressed. colorSubtleBackgroundHover #f5f5f5 and colorSubtleBackgroundPressed #e0e0e0 are separate tokens in Fluent 2, and the package carries both values already. Not applied: conforming moves a pixel away from legacy fluent, and NFR-1 is the product's call. Recorded in the baseline as a fluent2 section, with the bound stated - four spot checks, not a sweep, and the 277 declarations no product describes are still without external authority. jest 185/185, stylelint 0.
…comparison Asked whether the two handoff documents covered everything, they did not, and the reason was a bug rather than an omission. A package token path is <component>.<sub-elements>.color.<variants>.<slot>.<state>, and the slot is the rightmost segment matching one of our parts. `focus-rect.color.default` has no such segment, and the fallback - treat the component name as the slot - only fires when that name is itself one of our parts. `focus-rect` is not. So the entire component, four roles for the focus indicator, was dropped as an unknown slot and never entered the comparison. The finding that opened this whole audit was one I had made by hand; the tool could not have produced it. Fixed with an explicit alias for the package components whose name IS the slot but is spelled differently: focus-rect -> outline, skeleton -> bg, empty-item -> content. That surfaced a class no check had: what the package offers and the theme never takes. Every other check starts from a declaration we wrote and asks whether its role is right; a whole family can be missing without any single declaration looking wrong. Counted from the package inward: 175 roles the four sets assign 82 the theme reads 83 exist in the semantic layer and go unread - capability we never reached for 10 no layer declares at all - stale names inside the neighbours' own sets Two of the unread families are worth naming. All four focus roles - focus, -inverted, -static, -static-inverted - are unread, which is the same finding as before, now produced by the tool instead of by hand. And the whole info intent is unread: bg-info, content-info, border-info all exist, while the components with an `info` modifier paint it from neutral roles. Banked on exact equality both ways - a role leaving the list means the theme started using it, and that is a diff worth seeing. jest 186/186.
…ey print Two pages, one generator, same source as the gate: tools/review/roles-pages.mjs reads roles.mjs --json plus tests/roles.baseline.json and writes ROLES_QUESTIONS.html and ROLES_TYPOGRAPHY.html next to SCALES.html and BRIDGES.html. Every question carries a number so an answer can arrive as "Д3 - second option" without quoting it back, and nothing is filtered out - what needs no decision is listed too, with the reason, so the set is closed rather than curated. The pages are Russian while the baseline stays English: the baseline is read by the gate and lives beside the code, the pages go to design. The duplication is held by a check - a banked item with no Russian text throws during generation instead of shipping in English. Two bugs found while building them. The report was printing wrong line numbers. Block comments were stripped with a plain replace, which removes their newlines too, so every reference after the first comment in a file was short by its length - typography/_sizes.scss:69 came out as :65. Blanked instead of removed now. This affected every line reference the audit has produced, including the two documents already handed over. And the note explaining that fix contained a literal comment terminator inside a block comment, which closed it early and broke the module. Rewritten without one. Both pages regenerated and read end to end; jest 186/186, stylelint 0.
…nterpart
Asked how the 39% with nothing to compare against could be reduced, the first answer turned out to
need no new source at all. The folder-to-component map was matching on name similarity, and that
does not survive contact with another product's vocabulary: cardView is the package's `grid` - the
way tools/review/package-disabled.mjs has mapped it all along - a speed-dial action is a button, a
lookup is a listbox, an action sheet is a popup, validation is the `field` component.
Six mappings added. no-counterpart 277 -> 226, and not one new disagreement: all 51 newly compared
declarations landed in agrees or agrees-kin. That is worth stating plainly, because a mapping that
produced no findings could equally mean it is wrong - here it means cardView really does share the
grid's roles and a FAB really is painted like a button.
Measured for the record, so the next step is a choice rather than a guess. Of the 226 left:
110 have a counterpart in Blazor's implementation SCSS, which the token package does not carry.
dxvcs/.../ds-themes/components has 57 folders against the 20 in components/blazor, and the
names differ again - `rollers` is our dateView, `edit-dropdown` our dropDownEditor. It reads
roles through the same `ds.$` bridge we do, so the comparison needs no name map:
scheduler 43 (38 roles there), filterBuilder 28 (59), pivotGrid 19, fileUploader 10,
dateView 5, splitterBar 3, dropDownEditor 2
13 slider - Fluent UI React ships one, and its styles name nine colour tokens per part. That
source needs a Fluent-2-to-dxds name map, since neither the names nor the brand values match
103 have no external authority anywhere: stepper, diagram, fileManager, gantt, tileView,
sortable, widget, and htmlEditor whose Blazor counterpart reads a single role
Measured and rejected: devextreme-vnext's implementation adds nothing - 18 components, every one
already described by the token package.
So the reachable floor is about 14% of the theme rather than 39%, and what stays uncovered is the
widgets no design system describes at all.
jest 186/186; pages regenerated.
…adicting itself The reduction from 39% to under 8% existed only in a conversation, and the page meanwhile carried a hardcoded "39%" next to a computed "226 of 714" - which is 31.7%. A number written twice, once by hand, is a number that will be wrong. The whole section is now derived. The count, the percentage and the per-folder breakdown come from the tool; the levers come from tests/roles.baseline.json, where they are banked with the date they were measured, because they were measured against repositories this tool cannot read. A ladder table subtracts them in order so the floor is arithmetic rather than assertion. 226 now 31.7% -110 Blazor's implementation SCSS 16.2% -13 Fluent UI React (slider) 14.4% -47 WPF, at family level only 7.8% =56 stepper, fileManager, tileView, sortable, widget The WPF entry is the one worth reading. Its Gantt, Diagram and RichEdit themes exist, but they are not tokenised: semantic keys like AccordionHeaderBackgroundRest reach only the components inside components/wpf, while Gantt paints from the legacy Lightweight palette where Color.Foreground.Primary is the literal #FF1A1A1A. What WPF does have is Lightweight/Common/PaletteSemantics.md, 142 keys described in prose - a family-level signal (border / bg / content, not which role), covering exactly the components nobody else describes. Measured and rejected, recorded so it is not re-investigated: vnext's implementation has 18 components, every one already described by the token package. A light gate holds the shape: a lever without a measured size or a stated cost is a suggestion, not a plan. jest 187/187.
The audit compares our roles against theirs, so it is worth what their own consistency is worth.
Nobody had measured that.
The first attempt said blazor and core agree on 6% of shared slots, which was nonsense produced by
my own key: it took the last path segment as the slot and dropped the variant, so
`switcher.color.danger.bg.rest` and `switcher.color.primary.bg.rest` collapsed into one entry and
the last one won. That is how a switch ended up with a danger-red background. Recomputed on the full
anatomy path.
583 slots where the anatomy path matches exactly
541 the role agrees 93%
42 it does not
9 of those are blazor still writing bg-none / border-none / content-none, pinned at 262.9.1,
where the current layer has a single `none` - not a disagreement, a stale spelling
33 real
core ↔ vnext 579 shared, 100% (the same set plus `field`, so it proves nothing)
blazor ↔ core 115 shared, 77%
core ↔ wpf 33 shared, 70%
blazor ↔ wpf 3 shared, 0% (three slots is not a sample)
The 33 are platform-shaped rather than arbitrary. WPF's ribbon is accent-coloured where the web's is
neutral - content-inverted against content, bg-primary-hovered against bg-hovered. WPF's grid sits on
bg-low and border where the web sits on bg and border-subtle. Blazor's outline button uses bg-none /
bg-hovered / bg-active where core uses the alpha ladder; button alone accounts for 19 of the 33.
The larger finding is not the disagreement, it is how little there is to disagree about. `button` has
401 anatomy paths across four products and zero shared by all four; checkbox 126 and zero; switch 57
and zero; toast 92 and zero. What diverges between the products is the anatomy, not the role
assignment.
That is also the justification for how this tool compares, written down at last: slot-level, not
path-level. A path-level comparison would find almost nothing, because our anatomy differs from
theirs exactly as much as theirs differ from each other. The price is honest - the tool answers "does
the package use this role for a slot of this kind in this component", not "does it use it here" -
and it is why cross-family is phrased as a question rather than a verdict.
jest 187/187.
…rk job red
Fair question, and the answer is structural rather than a suppression, so it applies to every future
finding of the same shape. Grounded in axe-core 4.12.1 source, not assumed.
axe reads DOM text nodes. visibleTextNodes() collects children with nodeType === 3; generated content
is not a child node. DevExtreme icon fonts render through ::before { content: "\fXXX" }, so an element
whose only text is a glyph never enters the color-contrast rule at all. The splitter grip is exactly
that - dx-icon-sizing puts a font-size on .dx-resize-handle-icon and the glyph arrives from the icon
font. Every chevron, checkmark, arrow, grip and spinner in the theme is in the same position.
WCAG 1.4.11 has no axe rule. Non-text contrast - 3:1 for control boundaries and graphics - is not
implemented, so borders, focus rings, indicators and switch tracks are outside the gate by
construction rather than by exception.
And the state has to be reached. The delete item in the message-list context menu is real text and
measures 3.05 in dark, but only while the menu is open and that item focused; the splitter suite runs
with color-contrast enabled and simply never gets there.
So the dark job runs and sees only real text in reachable states. The contrast table in this report
is the only place the rest is measured, which is the argument for keeping it gated.
jest 187/187.
Asked to regenerate the pages after every change. Better to make it impossible to forget: the pages are the deliverable, and a generated file regenerated only when somebody thinks of it will eventually disagree with the data it claims to show. That already happened once - a hardcoded "39%" sitting next to a computed 226 of 714. roles-pages.mjs grows a --check that renders into memory and compares with what is on disk, naming the stale file and pointing at the command. tests/roles.test.ts drives it, so a page that lags its data fails pnpm test and CI rather than shipping quietly. Proven in both directions: green on fresh pages, red with the filename when one is touched. jest 188/188.
…self fails in light Asked whether the resize-handle row needed acting on. Checking whether the threshold even applies moved the question. WCAG 1.4.11 asks about the visual information required to identify a component. For a splitter that is the bar against the panes, not the dots inside it - so the grip at 2.8 in dark may be out of scope entirely, an affordance within an already-identified control. The bar is not out of scope, and it is worse: bar on the pane color-border on color-bg light 1.62 dark 3.42 bar, hovered color-border-hovered on color-bg light 1.82 dark 6.76 bar, focused color-bg-primary on color-bg light 5.38 dark 2.88 grip (the old row) color-content-subtle on border light 6 dark 2.8 In light the splitter is not identifiable by contrast at all - 1.62 against a 3:1 floor - and hovering does not fix it. Focus fails the other way round, in dark. This is the mode that does have screenshot coverage, and it still went unseen: an etalon matches itself no matter how invisible the bar is, and axe implements no rule for 1.4.11. Nothing in the theme's docs mentions non-text contrast at all - grepped DIVERGENCES, the journal and FOCUS_RINGS. FOCUS_RINGS came closest, noting that axe misses focus rings because the rule reads text, but it read that as a fact about rings rather than about the whole non-text half of the requirement. Д7 is reframed on the page from "which role for the grip" to "is the splitter identifiable", and the four measurements are banked. Stated as what it is: a first sample. Only the splitter was measured this way, so there is no claim about the rest of the theme. jest 188/188.
…her is a colour to pick Asked for the options on the two "name promises one thing, paints another" rows. Reading each to the end dissolved both. Д8, list-border-disabled. Not a contrast question at all. The variable colours .dx-list-select-all-label::after, and exactly one rule in either bundle targets that pseudo-element - with no `content`, so it is never generated and the declaration paints nothing. The label's own disabled text is correct one line above, on --dx-list-content-disabled. My original framing said "disabled list text sits at #d7d7d7" and that was simply wrong: the text does not read this variable. Options recorded, recommendation is to delete rule and variable, which needs a changelog because the name is public. Д9, tabs-tab-border-disabled. The only member of its own ladder off a border role - selected-active, selected-hovered, selected-focused, active and hovered all read border-*, disabled alone reads content-disabled. border-disabled makes the ladder consistent and the indicator fainter, #ababab -> #d7d7d7 in light and #767676 -> #4c4c4c in dark, which is what a disabled state should do. That is following the theme's own ladder rather than choosing a taste, so the recommendation is stated plainly. Both rows on the page now carry numbered options and a recommendation rather than a description of the problem. jest 188/188.
Д8, decided: remove it outright. The variable coloured .dx-list-select-all-label::after, exactly one rule in either bundle touched that pseudo-element, and it carried no `content` - so the element was never generated and the declaration painted nothing. Gone: the rule in list/_index.scss, the declaration in list/_colors.scss, and the projection, which publish.mjs dropped on regeneration. Bundle diff is exactly the removal: one tier name gone in each mode, nothing added, no value moved, the ::after rule absent, -206 bytes per bundle. Both gates fired first, which is what they are for. The slotLies baseline is exact equality, so a resolved row is a deliberate re-bank rather than a silent shrink; and the page-freshness gate added an hour ago caught its first real staleness. Also reframed the rename section. I had written that the 21 renames need a changelog - they do not, yet. The fluent-next tier is not shipped, so renames and deletions are free until release, after which each name becomes a contract with applications. That is an argument for doing the wave before the release rather than after, and the page now says so. The legacy 38 are called out separately: --dx-toolbar-height and friends shipped in 25.2 and were frozen by the 27.08.2026 decision. One thing deliberately not reported: I tried to generalise the dead-pseudo-element check with a static scan and it found 74 suspects, of which the tabs indicator alone is a dozen false positives - .dx-tab::after gets its `content` from a separate rule, and comparing selector strings is not how CSS matches elements. The right instrument is the runtime audit page, which already walks the real DOM. The single case here was proven properly, by reading every rule that touches that one pseudo-element. jest 188/188, naming:check 0, stylelint 0.
EugeniyKiyashko
force-pushed
the
fluent-next/roles-audit
branch
from
September 8, 2026 23:46
be64bc0 to
71d3c5e
Compare
…all seven
Д9 asked whether border-disabled would be consistent with the rest of tabs' disabled state. It would
not, and asking the package settled the whole row:
core/tabs.color…selector.disabled = content-disabled
vnext/tabs.color…selector.disabled = content-disabled
Two products assign content-disabled to this exact slot, which is what we already read. So the value
was right and only the name was wrong - and the package's word for the element is `selector`, which
is in our own parts vocabulary too.
Measured before deciding, because the value looked like the interesting part:
within tabs all four disabled slots read content-disabled today; moving the indicator to
border-disabled would leave it fainter than the text and icon beside it
across theme splitter's bar, menu's separator, gallery's indicator and progressBar's track all
read border-disabled for the same shape of element - tabs was the outlier
visibility the indicator carries state, and 1.4.11 covers states. content-disabled reaches 2.3
light / 3.42 dark against the strip; border-disabled would drop it to 1.44 / 1.81
So all seven indicator variables are renamed border -> selector, values untouched. They are read only
through dx-tabs-indicator-background(), so none of them ever painted a real border. Nothing outside
the theme reads them - demos, e2e and the library were checked - and the tier is unshipped, so this
costs nothing today and would cost a deprecation after release.
Bundle diff: seven names out, seven in, zero values moved anywhere in either mode.
Left as a question rather than folded in: gallery's indicator on border-disabled reaches the same
1.44, while core and vnext put an indicator on content-disabled. It may be gallery that is off the
pattern rather than tabs, and that is a bigger question than this row.
jest 188/188.
…ll the others Caught by a reference that no longer pointed where it was written: Д8 and Д9 were the list dead declaration and the tabs indicator, both now closed, and the numbers had silently slid onto two concept rows underneath. Numbers are now issued once and banked in tests/roles.baseline.json under questionIds, keyed by the item rather than its place in the list. A closed question takes its number with it and the next new one gets the next free number - none are reused. A banked entry with no number fails generation instead of borrowing its neighbour's. Closed so far and never to be reissued: list-border-disabled (deleted) and tabs-tab-border-disabled (renamed to selector). jest 188/188.
…ts disabled overrides
Д4 asked which role the treeView checkbox border should read. The right answer was that treeView
should not be answering: a checkbox inside a tree is a checkbox.
Two blocks were overriding it, and reading them was worse than the row suggested. The general one
painted the checkbox border from --dx-tree-view-item-content-disabled - the tree ITEM's text colour
on a checkbox border. The second, nested inside the focused branch, re-specified background, border,
the check mark and the indeterminate mark from four treeView-only variables. Both won the cascade
purely on selector length and said the same thing as the checkbox with different values.
Removed: both blocks, four variables, their projections. checkBox already covers this - its rule is
`&.dx-state-disabled, .dx-state-disabled &`, so it reaches a checkbox inside a disabled tree on its
own. One rule now paints that border in the whole bundle:
.dx-checkbox.dx-state-disabled .dx-checkbox-icon,
.dx-state-disabled .dx-checkbox .dx-checkbox-icon { border-color: var(--dx-check-box-border-disabled) }
Four tier names out, none in, no declaration value moved anywhere, -1350 bytes per bundle. What
changes on screen is what Д4 recommended and one step further: the disabled checkbox in a tree now
renders exactly like every other disabled checkbox - border-disabled instead of content-disabled,
#ababab -> #d7d7d7 light and #767676 -> #4c4c4c dark. treeView etalons need re-shooting.
Two rows leave the open list with it: the border and the indeterminate background.
jest 188/188, stylelint 0.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.