enhancement
major
minor
major
minor
minor
#29606
TL Views: a `<color token>` naming a token the React theme does not emit renders the pill untinted, silently
Problem
<color token="…"/> (TLColor, #29546) names a design token of the UI theme; the pill is rendered with var(--<token>) as its color. The React UI emits the tokens of tl-react-theme.config.xml (UIThemeService) as CSS custom properties: support-error, support-success, support-warning, interactive, text-secondary, … The classic theme settings (com.top_logic.themes.core, theme-settings.xml) define further names such as support-info and support-danger, which the React theme does not emit.
An annotation naming one of the latter (found while coloring the priority literals of the issue tracker tl-dev with support-info and support-danger) is accepted without any message: the model loads, the doclet and the configuration checker say nothing, the pill renders with background: var(--support-info), which resolves to nothing, so the value reads as plain text. Nothing in the log points at the cause; it was found by getComputedStyle in the browser.
Solution
The token name is validated against the design tokens the application actually emits, and those tokens are offered for selection.
Two registries of the same kind
An application has two registries of CSS custom properties, and they are the same mechanism implemented twice:
- the classic theme settings - ThemeSetting / ThemeSettings, read from WEB-INF/themes/<id>/theme-settings.xml, merged along the theme chain, emitted by CSSBuffer as a :root { --<name>: <value>; } block at the end of the generated theme stylesheet, one setting kind per tag (<color>, <dim>, <int>, <size>, <string>, …, plus the server-only kinds that are not CSS variables);
- the UI theme tokens - ThemeToken / UITheme / UIThemeService in com.top_logic.layout.react, emitted as an inline <style> scoped by data-theme.
61 names occur in both, with the same value in 57 of them - the token set of the React UI is a subset of the classic one. The two are nevertheless disjoint at runtime: a page of the React UI receives only the tokens of UIThemeService (ViewServlet never touches Theme), a classic page only the block of its theme. A name therefore resolves in one UI and not in the other, which is what makes the defect possible.
The seam
ColorSpec lives in the core module com.top_logic, so it can read the classic settings but not the tokens of the React module above it. The core module therefore declares DesignTokenService, the seam answering the names of the design tokens the user interface emits, per kind (color, length, number, text). ThemeDesignTokens implements it in the core module over the CSS-relevant settings of the classic themes; UIThemeDesignTokens replaces it in com.top_logic.layout.react with the tokens of the UI themes, the way com.top_logic.element replaces CompatibilityService. A kind no registry supplies is empty, and nothing is then checked.
The check and the choice
ColorSpec#getToken() reads that service twice over:
- The options. The color tokens of the application are the options of the property, so the model editor offers a drop-down of the names that are emitted instead of a free-text field. A token the application does not emit is kept beside them: the form shows it, and saving writes it back unchanged, so an annotation written against a theme the editing application does not install survives. A name can therefore be chosen or kept, but not newly typed.
- The check. A token no theme emits is reported as a warning naming the token and the names that are emitted. DynamicModelService runs the ConstraintChecker over the whole loaded model at startup, so an annotation of a *.model.xml is reported in the boot log, and the application still starts.
Only color tokens are offered and accepted - a pill colored with spacing-04 or shadow-menu makes no sense. A design token therefore names its kind: ThemeToken answers it and UITheme keeps it through the inheritance resolution. A token that aliases another one (<ref>) takes the kind of the token it names, followed along the chain of references and across the theme inheritance. Two silent failures of that mechanism are reported as well: a <ref> naming no token, and a cycle of references.
The classic settings carry their kind already, in the tag that writes them.
support-info
The React theme carries support-error, support-success and support-warning but no info color, while the classic theme does - which is what led to the annotation that could not work. The family is completed with support-info, and the demo colors a third ticket status with it.
Not covered
The warning is not shown in the model editor - #29619. ConfigValidation evaluates the constraint and keeps only the failures that are not warnings, because what it collects blocks the form from being applied. A warning-level constraint is therefore dropped, although the field control of the React UI carries a hasWarnings state to the client already. The drop-down makes an unknown token hard to enter in the first place, so what remains unreported there is a token that was already stored.
The two registries stay two. Unifying them is a work of its own: the classic CSS name is the last dot-separated segment of the setting name and collisions are possible, the classic side has a second, Java-declared tier (ThemeVar), and the two emit their block at different times (baked into the generated stylesheet vs. written per request, scoped by data-theme). The name drift the comparison exposes - support-error against support-danger, layer-01 against layer, border-color against border-subtle - is left as it is.
A model read back from the database instead of the configuration (DynamicModelService loading the stored annotations) skips the ConstraintChecker altogether; that path reports nothing, for this annotation as for every other configuration constraint.