Skip to content

Improve theming API - #262

Open
dlrice wants to merge 1 commit into
nextfrom
theme-improvements
Open

Improve theming API#262
dlrice wants to merge 1 commit into
nextfrom
theme-improvements

Conversation

@dlrice

@dlrice dlrice commented Aug 25, 2026

Copy link
Copy Markdown
Contributor

Addresses #261

Purpose

theme.labelColor painted group and track labels the same colour, flattening the shipped grey/white hierarchy — and a dark value left near-black text, caret, and hover unreadable on it. This makes one-knob theming keep the hierarchy and stay legible with any colour.

Approach

  • labelColor becomes two-tone: the colour on group labels, a light tint (25% over white) on track labels; new groupLabelColor / trackLabelColor fields pin either surface exactly.
  • A new src/styles/color.ts resolves theme colours to rgb() via a browser probe (full CSS syntax in, unparseable values dropped — also an injection guard, and no color-mix() so the support matrix holds), then derives text, muted text, caret, and hover per surface by WCAG contrast (best of near-black/white). Results land on new --protvista-* tokens that default to current values.
  • The navigation label and credits cells are chrome, not rows: they move to --protvista-color-surface instead of taking the track-label tint (migration note in
    CHANGELOG).

Testing

  • src/styles/__spec__/color.spec.ts — colour maths: resolution/rejection (incl. injection attempts), alpha compositing, WCAG reference values.
  • src/__spec__/theme-config.spec.ts — token-level: two-tone mapping, overrides, fallbacks, stale-token clearing, AA sweep across light/dark themes, and the mid-grey (#808080) boundary where AA is unreachable.
  • src/__browser__/theme-chrome.browser.spec.ts — real Chromium: derived values actually reach the cells through the cascade; nav/credits stay on the surface colour.

Visual changes

Updated docs/src/assets/screenshots/theming-comparison.png and tutorial-themed.png show the new two-tone label panel. Unthemed viewers are pixel-identical (all new tokens default to the values they replace).

Checklist

  • My PR is scoped properly, and "does one thing only"
  • I have reviewed my own code
  • I have checked that linting checks pass and type safety is respected
  • I have checked that tests pass and coverage has at least improved, and if not explained the reasons why
  • If needed, the changes have been previewed by all interested parties.

@minjoonkim
minjoonkim self-requested a review August 26, 2026 09:03
the rows it describes, so they sit on the plain surface colour. */
protvista-uniprot .${p}-track-label {
background-color: var(--protvista-track-label-bg);
color: var(--protvista-track-label-color);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Adding color: var(--protvista-track-label-color) and --protvista-group-label-color pins label text to #222222, so an unthemed viewer no longer inherits the host page's text color, and the nav/credits cells in the same column still do, giving two text colors in one column.


protvista-uniprot .${p}-nav-track-label,
protvista-uniprot .${p}-credits {
background-color: var(--protvista-color-surface);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Repointing .nav-track-label and .credits at --protvista-color-surface couples the label column's neutral cells to popovers, tooltips, the customize panel and the datatable, making the documented workaround for a uniform column a page-wide repaint.

Comment thread src/styles/tokens.ts
description: 'Background of collapsible group labels.',
},
{
name: '--protvista-group-label-hover-bg',

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new tokens whose defaults are var(--global) are substituted at :where(:root), so a consumer override of the global token on the host or an ancestor no longer reaches the label hover/muted colors, a regression, because the two rules that changed previously read the global token directly at the element.

Comment thread src/protvista-uniprot.ts
* (WCAG 1.4.11), and its hover state, which would otherwise swap in the
* near-white global hover under text just flipped to white.
*/
private applyLabelSurface(surface: 'group' | 'track', bg: Rgb) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The readable-text derivation runs only in applyTheme (the config theme: path), so the CSS-token path, which is the primary documented theming lever, still produces near-black text on a dark label, the exact bug this PR claims to fix.

Comment thread src/protvista-uniprot.ts
// Nothing is derived from the accent, but it goes through the same
// resolution so every field of `theme` behaves alike: one syntax
// range, and one answer to an unparseable value.
const accent = theme.accentColor ? resolveColor(theme.accentColor) : null;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A translucent theme.accentColor is now flattened to an opaque colour composited over hardcoded white, changing rendering for a field from which nothing is derived.

Comment thread src/styles/color.ts
* Translucent colours are composited over {@link SURFACE}.
*/
export function resolveColor(value: string): Rgb | null {
const doc = globalThis.document;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

resolveColor resolves against globalThis.document rather than the element's own document, and returns null (dropping the entire theme) when document.body is absent.

Comment thread src/styles/color.ts
// — the only reliable "was that a colour?" test, since a computed style
// reports inherited black for an invalid value just as it would for a
// genuine `black`.
if (!probe.style.color) return null;

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The probe.style.color truthiness guard accepts CSS-wide keywords and var() references, so those resolve to whatever the probe inherits instead of being rejected.

Comment thread src/styles/color.ts
)?.default;
// The fallback only fires with no DOM to resolve against; the
// registry value is the source of truth in every real environment.
cachedDefaultText = (declared && resolveColor(declared)) ?? {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

defaultTextColor() caches permanently on first call, so a DOM-less first call locks in the hardcoded fallb

Comment thread src/styles/color.ts
if (!doc?.body) return null;

const probe = doc.createElement('span');
// `display: none` keeps the probe out of layout; `color` still resolves.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Each resolveColor call builds a throwaway element, appends it to document.body and calls getComputedStyle, up to four forced style flushes per applyTheme, repeated on every config apply.

);
}

afterEach(() => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Teardown asymmetry: the spec's afterEach restores the real fetch before mount.js's afterEach removes the mounted viewers, so the still-live components can issue real network requests.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants