Skip to content

🎨 fix: Draw Library Colours From Theme Roles and Record the Exceptions - #16482

Merged
berry-13 merged 6 commits into
canaryfrom
berry-13/theme-leakage-library
Sep 29, 2026
Merged

berry-13 merged 6 commits into
canaryfrom
berry-13/theme-leakage-library

Conversation

@berry-13

@berry-13 berry-13 commented Sep 29, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Part of the theme-leakage stack (merge from the bottom up). This link covers the component library in packages/client/src. The image generation placeholder (PixelCard) drew a fixed light slate palette onto its canvas, so in dark mode, and in any theme, it showed pale pixels over a dark surface. Its default palette now names theme channel variables (--surface-primary-alt, --surface-tertiary, --border-medium), resolved off the card when the pixels are laid out and again when the root's class or inline variables change, so a mode or theme switch repaints a mounted card. The opt-in blue, yellow and pink presets keep their palettes (no role reproduces them; recorded in the allowlist), and a caller's explicit colors still passes through. The SAML login glyph painted black on the dark login button and now takes the button's text colour; the unused Codey mark takes fill-text-primary instead of dark:fill-white.

It also adds packages/client/src/theme/allowlist.md, the record of every colour that deliberately stays outside the theme (brand marks, artwork, theme definitions, library elevation ink, and, in the later links, media scrims and exported documents), one reason per entry. Brand marks stay recorded in eslint-suppressions.json: the static checks reject inline design-rule disables, and the config-level allow entry belongs in eslint.config.mjs, which #16248 currently edits.

Baseline at origin/canary 1d5062c (TS/TSX outside specs, and CSS under client/src and packages/client/src), before to after this link: raw palette utilities 57 to 56, dark: colour utilities 33 to 32, hex/rgb/hsl literals in TS/TSX 796 to 793, raw CSS literals 89 (unchanged), palette variables in CSS 150 (unchanged, all in client/src/style.css). Suppressions, client/src / packages/client/src: no-raw-colors 47/47 to 47/46, no-unknown-classes 23/7, no-restyle 2054/0, no-inline-styles 211/39, no-arbitrary-values 162/0, require-static-classes 67/0 (unchanged). Only the SamlIcon entry changes in eslint-suppressions.json.

Type of change

  • Bug fix
  • Documentation

Testing

Tested environments/configuration:

  • Chromium via Playwright against this worktree's dev pair, default and ClickHouse themes (ClickHouse served through interface.theme on /api/config, the way the e2e specs do), light and dark.
  • Computed styles: fill-text-primary resolves to 33,33,33 (default light), 236,236,236 (default dark), 22,21,23 and 255,255,255 (ClickHouse light and dark).

Automated tests:

  • PixelCard.spec.tsx (new): the default palette draws theme variables, an explicit colors prop draws as given, and a theme change re-reads the palette (this case fails without the root observer).
  • reviewctl precheck against origin/canary: static checks and jest-packages-client (5 suites, 67 tests) pass.
  • @scenario:saml-login-glyph-follows-button-text (new, e2e/specs/mock/scenarios/saml-glyph.spec.ts): the SAML glyph's computed fill equals its button's text colour.
  • npx tsc --noEmit (incremental) for packages/client: clean.
  • npm run static-checks -- --against origin/canary: all affected checks pass, including design-rule suppressions.

Screenshots / recordings

No capture of the image generation placeholder: it only renders mid-generation of an OpenAI image tool call, which this environment has no provider for. The palette change is verified by computed style above.

Risk / compatibility

PixelCard's exported variant names are unchanged; a consumer that loads the component without the app stylesheet (no theme variables) gets currentColor pixels instead of nothing.

Checklist

  • I reviewed my own changes
  • Relevant tests have been added or updated
  • Existing relevant tests pass
  • The change does not introduce new warnings or errors
  • User-facing or complex behavior is documented where necessary

PixelCard resolves its variant palettes from theme channel variables when it lays
out pixels, so the image generation placeholder follows the active theme in dark
mode instead of drawing a fixed light slate palette. The SAML glyph and Codey mark
take the text colour.

The new theme/allowlist.md records every colour that deliberately sits outside the
theme (brand marks, artwork, theme definitions, library elevation ink) and why.
@chatgpt-codex-connector

chatgpt-codex-connector Bot commented Sep 29, 2026 •

Copy link
Copy Markdown

Codex Review Summary

This comment shows the latest Codex review activity on this pull request.

Review Status Commit Review trigger
📝 Code Review ✅ Completed 2026-09-29T07:23:11.781364Z 27ced4f New commits
🔒 Security Review ✅ Completed 2026-09-29T06:13:13.787669Z b9ef2cc PR opened
ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review" or "@codex security review".

Codex reacts with 👀 while any review is running, comments if it has suggestions, and reacts with 👍 once all reviews finish with no findings.

@berry-13 berry-13 changed the title berry-13/theme-leakage-library 🎨 fix: Draw Library Colours From Theme Roles and Record the Exceptions Sep 29, 2026
@github-actions

Copy link
Copy Markdown
Contributor

Lighthouse CI failed. The last 80 log lines contain the measured budgets and assertion failures.

│ 23      │ 'http://localhost:3080/api/permissions/mcpServer/effective/all'                                                 │ 2987.55799999999   │ 3747.603999999992  │ 200    │
│ 24      │ 'http://localhost:3080/api/prompts/groups?limit=10'                                                             │ 2988.8150000000023 │ 4252.74099999998   │ 200    │
│ 25      │ 'http://localhost:3080/api/keys?name=openAI'                                                                    │ 3272.4919999999984 │ 3915.79800000001   │ 200    │
│ 26      │ 'http://localhost:3080/api/presets'                                                                             │ 3273.048999999999  │ 3920.792000000016  │ 200    │
│ 27      │ 'http://localhost:3080/api/tags'                                                                                │ 3273.7090000000026 │ 3924.214000000007  │ 200    │
│ 28      │ 'http://localhost:3080/api/share/link/16390000-0000-4000-8000-000000000001'                                     │ 3273.883999999991  │ 4254.515000000014  │ 200    │
│ 29      │ 'http://localhost:3080/api/messages/16390000-0000-4000-8000-000000000001'                                       │ 3275.0409999999974 │ 4425.088999999978  │ 200    │
│ 30      │ 'http://localhost:3080/api/files/config'                                                                        │ 3275.4060000000172 │ 4176.432000000001  │ 200    │
│ 31      │ 'http://localhost:3080/api/user/settings/favorites/tools'                                                       │ 3275.5869999999995 │ 4428.0929999999935 │ 200    │
│ 32      │ 'http://localhost:3080/api/endpoints/token-config'                                                              │ 3276.0009999999893 │ 4432.534000000014  │ 200    │
│ 33      │ 'http://localhost:3080/api/user/settings/skills/active'                                                         │ 3276.7729999999865 │ 4761.253999999986  │ 200    │
│ 34      │ 'http://localhost:3080/api/agents/tools/web_search/auth'                                                        │ 3277.338999999978  │ 7274.270000000019  │ 200    │
│ 35      │ 'http://localhost:3080/api/agents/tools/calls?conversationId=16390000-0000-4000-8000-000000000001'              │ 3277.592000000004  │ 4763.298999999999  │ 200    │
│ 36      │ 'http://localhost:3080/api/agents/chat/status/16390000-0000-4000-8000-000000000001?generationProtocolVersion=2' │ 4534.807000000001  │ 4791.057000000001  │ 200    │
└─────────┴─────────────────────────────────────────────────────────────────────────────────────────────────────────────────┴────────────────────┴────────────────────┴────────┘

Inspect .lighthouse HTML/JSON and e2e/lighthouse/README.md. Reuse loaded user/config data; overlap independent reads without bypassing authorization.

┌─────────┬────────────────────────────┬─────────────────────┬───────┐
│ (index) │ audit                      │ median              │ limit │
├─────────┼────────────────────────────┼─────────────────────┼───────┤
│ 0       │ 'largest-contentful-paint' │ 4558.921            │ 4500  │
│ 1       │ 'cumulative-layout-shift'  │ 0.01790353201704462 │ 0.1   │
│ 2       │ 'total-blocking-time'      │ 296.09700000000066  │ 500   │
└─────────┴────────────────────────────┴─────────────────────┴───────┘

  1) [chrome] › e2e/lighthouse/load.spec.ts:10:5 › serial database latency stays within web-vitals budgets 

    Error: Median largest-contentful-paint must stay within 4500

    expect(received).toBeLessThanOrEqual(expected)

    Expected: <= 4500
    Received:    4558.921

       at audit.ts:159

      157 |   console.table(measured);
      158 |   for (const { audit, median, limit } of measured) {
    > 159 |     expect(median, `Median ${audit} must stay within ${limit}`).toBeLessThanOrEqual(limit);
          |                                                                 ^
      160 |   }
      161 |   return results;
      162 | }
        at auditPage (/home/runner/work/LibreChat/LibreChat/e2e/lighthouse/audit.ts:159:65)
        at /home/runner/work/LibreChat/LibreChat/e2e/lighthouse/load.spec.ts:33:19

    attachment #1: screenshot (image/png) ──────────────────────────────────────────────────────────
    e2e/lighthouse/.test-results/load-serial-database-latency-stays-within-web-vitals-budgets-chrome/test-failed-1.png
    ────────────────────────────────────────────────────────────────────────────────────────────────

    Error Context: e2e/lighthouse/.test-results/load-serial-database-latency-stays-within-web-vitals-budgets-chrome/error-context.md

    attachment #3: trace (application/zip) ─────────────────────────────────────────────────────────
    e2e/lighthouse/.test-results/load-serial-database-latency-stays-within-web-vitals-budgets-chrome/trace.zip
    Usage:

        npx playwright show-trace e2e/lighthouse/.test-results/load-serial-database-latency-stays-within-web-vitals-budgets-chrome/trace.zip

    ────────────────────────────────────────────────────────────────────────────────────────────────


🤖: global teardown has been started
2026-09-29 06:13:59 �[32minfo�[39m: �[32mMongo Connection options�[39m
2026-09-29 06:13:59 �[32minfo�[39m: �[32m{�[39m
�[32m  "bufferCommands": false�[39m
�[32m}�[39m
🤖:  ✅  Connected to Database
🤖:  ✅  Found user in Database
🤖:  ✅  Deleted 1 convos & 2 messages
🤖:  ✅  Deleted user from Database
🤖: global teardown has been started
2026-09-29 06:13:59 �[32minfo�[39m: �[32mMongo Connection options�[39m
2026-09-29 06:13:59 �[32minfo�[39m: �[32m{�[39m
�[32m  "bufferCommands": false�[39m
�[32m}�[39m
🤖:  ✅  Connected to Database
🤖:  ⚠️  User not found in Database
  1 failed
    [chrome] › e2e/lighthouse/load.spec.ts:10:5 › serial database latency stays within web-vitals budgets 

Open the full run

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: b9ef2ccf95

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

blue: {
gap: 10,
speed: 25,
colors: '--status-info-subtle,--status-info-border,--status-info',

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge Preserve PixelCard's existing default palettes

For any consumer rendering the exported blue, yellow, or pink variants under the stock light theme, these substitutions visibly redesign the component rather than only making it theme-aware. For example, blue changes from the sky palette #e0f2fe/#7dd3fc/#0ea5e9 to the substantially different status-info values #eff6ff/#93c5fd/#2563eb; the warning and error roles similarly replace the yellow and pink palettes. Preserve the existing colors as the default values—such as through dedicated semantic roles—while allowing other themes to override them.

AGENTS.md reference: AGENTS.md:L42-L45

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in bbcdb29: the blue, yellow and pink presets keep their original palettes (no role reproduces them) and are recorded in theme/allowlist.md; only the default palette, the one the app renders, reads theme roles.

canvasRef.current.height = Math.floor(ch);

const cols = palette.split(',');
const cols = resolvePalette(palette, containerRef.current);

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Refresh the canvas palette when the theme changes

When a user switches light/dark mode or applies another runtime theme while an image-generation card remains mounted, ThemeProvider changes root classes and custom properties without resizing this element. Because this line resolves the variables into fixed RGB strings only during initPixels, every existing Pixel.color retains the previous theme until the card resizes or remounts, leaving an active canvas painted with stale colors. Reinitialize or re-resolve the palette when the theme variables change.

AGENTS.md reference: AGENTS.md:L90-L97

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in d327847: the card lays its pixels out again when the root's class or inline variables change. PixelCard.spec.tsx 're-reads the palette when the theme changes' fails without the observer and passes with it.

The palette is read from theme variables when the pixels are laid out, so a mode
switch or an applied theme left the previous theme's pixels on the canvas. The card
now lays its pixels out again when the root's class or inline variables change.
The blue, yellow and pink presets have no theme role that reproduces them, so they
keep their original palettes and are recorded in the allowlist; only the default
palette, the one the app renders, reads theme roles.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 8d52628ec2

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread packages/client/src/components/PixelCard.tsx
Comment thread packages/client/src/theme/allowlist.md Outdated
Comment on lines +3 to +6
Every colour in `client/src` and `packages/client/src` comes from a semantic theme role, except
the ones below. Each is a colour that must not follow the theme: a brand mark, artwork, a third
party's surface, a document that leaves the app, or the theme definitions themselves. An entry
names the file, what the literal paints and why the theme does not own it.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Make the color-exception record exhaustive

This new document claims to record every non-semantic color in both source trees, but existing exceptions are absent—for example, client/src/mobile.css still contains fixed light/dark link colors at lines 179–194, and packages/client/src/components/Avatar.tsx contains fixed RGB/RGBA values at lines 54–57. Because the document is presented as the authoritative record for literals that lint cannot see, subsequent audits will incorrectly treat these omissions as covered; either enumerate all current exceptions with reasons or explicitly scope the document as partial.

AGENTS.md reference: AGENTS.md:L92-L97

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in c64ccda: the document now states it covers what the sweep reached and points at berry-13#195 (files an open PR held, with counts, including client/src/mobile.css) and berry-13#194 (the avatar colours in Avatar.tsx).

Comment on lines +347 to +350
themeObs.observe(document.documentElement, {
attributes: true,
attributeFilter: ['class', 'style'],
});

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P2 Badge Ignore unrelated root style mutations

Observing the entire root style attribute makes non-theme updates repaint and reset an in-progress card: for example, useScrollbarGutter writes --message-scrollbar-gutter to document.documentElement.style when the message column changes, and the font-size utility writes --markdown-font-size there as well. Either mutation calls initPixels, which clears the canvas by assigning its dimensions, replaces every pixel and restarts the delayed progress animation, producing a blink or replay while an image is generating even though none of the three palette channels changed. Compare the resolved palette before reinitializing or subscribe to a theme-specific signal instead.

AGENTS.md reference: AGENTS.md:L42-L45

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Fixed in c64ccda: the root observer now compares the resolved palette and lays the pixels out again only when it changed, so scrollbar-gutter and font-size writes no longer blink the card. PixelCard.spec.tsx 'keeps its pixels when an unrelated root variable changes' fails without the comparison.

The root's inline style also carries unrelated variables such as the scrollbar gutter
and markdown font size, and relaying the pixels on every write blinked and replayed
an in-progress card. The allowlist now states what it covers and where the skipped
files are tracked.
@berry-13
berry-13 added this pull request to stack #16505 September 29, 2026 11:36
@berry-13
berry-13 merged commit f546dc2 into canary Sep 29, 2026
43 checks passed
@berry-13
berry-13 deleted the berry-13/theme-leakage-library branch September 29, 2026 14:14
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.

1 participant