From b9ef2ccf9558e9113f7fab34bd01b06a27720954 Mon Sep 17 00:00:00 2001
From: Marco Beretta <81851188+berry-13@users.noreply.github.com>
Date: Tue, 29 Sep 2026 08:02:34 +0200
Subject: [PATCH 1/6] fix: Draw Library Colours From Theme Roles and Record the
Exceptions
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.
---
eslint-suppressions.json | 5 --
packages/client/src/components/PixelCard.tsx | 49 +++++++++++++++++---
packages/client/src/svgs/CodeyIcon.tsx | 2 +-
packages/client/src/svgs/SamlIcon.tsx | 2 +-
packages/client/src/theme/allowlist.md | 41 ++++++++++++++++
5 files changed, 86 insertions(+), 13 deletions(-)
create mode 100644 packages/client/src/theme/allowlist.md
diff --git a/eslint-suppressions.json b/eslint-suppressions.json
index c356e3cb194..27d7fcfa3a8 100644
--- a/eslint-suppressions.json
+++ b/eslint-suppressions.json
@@ -2385,11 +2385,6 @@
"count": 7
}
},
- "packages/client/src/svgs/SamlIcon.tsx": {
- "shadcn/no-raw-colors": {
- "count": 1
- }
- },
"packages/client/src/svgs/SendMessageIcon.tsx": {
"shadcn/no-inline-styles": {
"count": 1
diff --git a/packages/client/src/components/PixelCard.tsx b/packages/client/src/components/PixelCard.tsx
index 904e2fc05b5..a539da3f680 100644
--- a/packages/client/src/components/PixelCard.tsx
+++ b/packages/client/src/components/PixelCard.tsx
@@ -139,13 +139,50 @@ const getEffectiveSpeed = (value: number, reducedMotion: boolean) => {
const clamp = (n: number, min = 0, max = 1) => Math.min(Math.max(n, min), max);
+/** Variant palettes name theme channel variables, read off the card when the pixels are laid
+ * out, so the canvas draws the active theme instead of a fixed light palette. */
const VARIANTS = {
- default: { gap: 5, speed: 35, colors: '#f8fafc,#f1f5f9,#cbd5e1', noFocus: false },
- blue: { gap: 10, speed: 25, colors: '#e0f2fe,#7dd3fc,#0ea5e9', noFocus: false },
- yellow: { gap: 3, speed: 20, colors: '#fef08a,#fde047,#eab308', noFocus: false },
- pink: { gap: 6, speed: 80, colors: '#fecdd3,#fda4af,#e11d48', noFocus: true },
+ default: {
+ gap: 5,
+ speed: 35,
+ colors: '--surface-primary-alt,--surface-tertiary,--border-medium',
+ noFocus: false,
+ },
+ blue: {
+ gap: 10,
+ speed: 25,
+ colors: '--status-info-subtle,--status-info-border,--status-info',
+ noFocus: false,
+ },
+ yellow: {
+ gap: 3,
+ speed: 20,
+ colors: '--status-warning-subtle,--status-warning-border,--status-warning',
+ noFocus: false,
+ },
+ pink: {
+ gap: 6,
+ speed: 80,
+ colors: '--status-error-subtle,--status-error-border,--status-error',
+ noFocus: true,
+ },
} as const;
+/** Resolves `--token` entries to the element's channel triplet; any other entry is a CSS color
+ * the caller supplied and passes through. A token the page does not define falls back to the
+ * canvas ink rather than drawing nothing. */
+const resolvePalette = (palette: string, element: Element): string[] => {
+ const style = getComputedStyle(element);
+ return palette.split(',').map((entry) => {
+ const color = entry.trim();
+ if (!color.startsWith('--')) {
+ return color;
+ }
+ const channels = style.getPropertyValue(color).trim();
+ return channels ? `rgb(${channels})` : 'currentColor';
+ });
+};
+
interface PixelCardProps {
variant?: keyof typeof VARIANTS;
gap?: number;
@@ -262,7 +299,7 @@ export default function PixelCard({
canvasRef.current.width = Math.floor(cw);
canvasRef.current.height = Math.floor(ch);
- const cols = palette.split(',');
+ const cols = resolvePalette(palette, containerRef.current);
const px: Pixel[] = [];
const cx = cw / 2;
@@ -355,7 +392,7 @@ export default function PixelCard({
>
-
+
diff --git a/packages/client/src/theme/allowlist.md b/packages/client/src/theme/allowlist.md
new file mode 100644
index 00000000000..e5bdd9625fd
--- /dev/null
+++ b/packages/client/src/theme/allowlist.md
@@ -0,0 +1,41 @@
+# Colours outside the theme
+
+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.
+
+A literal the design lint can see stays recorded in `eslint-suppressions.json` (inline disables
+of the design rules are rejected by the static checks), so its count there is the exception, not
+debt; the file-scoped allow entry for these paths belongs in `eslint.config.mjs`. Everything else
+is invisible to the lint (CSS, strings passed to a canvas or an iframe), so this list is the record.
+
+Adding an entry needs the same bar: if a theme author would reasonably want to recolour it, it
+is a role, not an exception.
+
+## Theme definitions
+
+| File | Why |
+| -------------------------------------------------------------- | --------------------------------------------------------------- |
+| `packages/client/src/theme/themes/*.ts`, `themes/clickui.json` | The palettes themselves: these are the values roles resolve to. |
+| `packages/client/src/theme/registry.ts` | Theme resolution and fallbacks written in channel triplets. |
+| `packages/client/src/theme/tokens.css` | Maps each role to `rgb(var(--role))`; no literal colour. |
+
+## Brand marks and artwork
+
+| File | Why |
+| ----------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
+| `packages/client/src/svgs/GoogleIcon.tsx` | Google's multicolour mark, fixed by its brand guidelines. |
+| `packages/client/src/svgs/FacebookIcon.tsx` | Facebook's mark, fixed by its brand guidelines. |
+| `packages/client/src/svgs/DiscordIcon.tsx` | Discord's mark, fixed by its brand guidelines. |
+| `packages/client/src/svgs/GeminiIcon.tsx` | Gemini's gradient mark, fixed by its brand guidelines. |
+| `packages/client/src/svgs/PaLMIcon.tsx` | PaLM's multicolour mark, fixed by its brand guidelines. |
+| `packages/client/src/svgs/BirthdayIcon.tsx` | A multicolour illustration; the palette is artwork, not a UI role. |
+| `packages/client/src/icons/provider/registry.ts`, `icons/provider/Avatar.tsx` | Provider brand colours, each already behind a `--provider-*` variable a deployment can override; the literal is only the fallback. |
+
+## Elevation ink in library stylesheets
+
+| File | Why |
+| --------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `packages/client/src/components/Dropdown.css` | Popover shadows in black alpha. The app paints `.popover-ui` from its own copy of these rules in `client/src/style.css`, so moving either copy to `--theme-shadow-lg` needs both edited together. |
+| `packages/client/src/components/Tooltip.css` | Tooltip shadows are smaller than any step of the theme shadow scale; black alpha ink, dropped in high contrast where the border takes over. |
From d327847ba05c06ed0c1e5df25b1e7896ebac87c5 Mon Sep 17 00:00:00 2001
From: Marco Beretta <81851188+berry-13@users.noreply.github.com>
Date: Tue, 29 Sep 2026 08:16:13 +0200
Subject: [PATCH 2/6] fix: Repaint the Pixel Card When the Theme Changes
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.
---
.../client/src/components/PixelCard.spec.tsx | 66 +++++++++++++++++++
packages/client/src/components/PixelCard.tsx | 8 +++
2 files changed, 74 insertions(+)
create mode 100644 packages/client/src/components/PixelCard.spec.tsx
diff --git a/packages/client/src/components/PixelCard.spec.tsx b/packages/client/src/components/PixelCard.spec.tsx
new file mode 100644
index 00000000000..75fce77682e
--- /dev/null
+++ b/packages/client/src/components/PixelCard.spec.tsx
@@ -0,0 +1,66 @@
+import { render, waitFor } from '@testing-library/react';
+import PixelCard from './PixelCard';
+
+/** The canvas draws with whatever the palette resolved to, so the test reads what the card
+ * asked the page for: which theme variables, and whether it asks again after a theme change. */
+describe('PixelCard palette', () => {
+ let fillStyles: string[];
+ let channels: string;
+
+ beforeEach(() => {
+ fillStyles = [];
+ jest.spyOn(HTMLCanvasElement.prototype, 'getContext').mockImplementation(function () {
+ return {
+ clearRect: jest.fn(),
+ fillRect: jest.fn(),
+ set fillStyle(value: string) {
+ fillStyles.push(value);
+ },
+ } as unknown as CanvasRenderingContext2D;
+ });
+ jest.spyOn(HTMLElement.prototype, 'getBoundingClientRect').mockReturnValue({
+ width: 10,
+ height: 10,
+ } as DOMRect);
+ channels = '1 2 3';
+ const computed = window.getComputedStyle.bind(window);
+ jest.spyOn(window, 'getComputedStyle').mockImplementation((element) => {
+ const style = computed(element);
+ return {
+ ...style,
+ getPropertyValue: (name: string) =>
+ name.startsWith('--') ? channels : style.getPropertyValue(name),
+ } as CSSStyleDeclaration;
+ });
+ });
+
+ afterEach(() => {
+ jest.restoreAllMocks();
+ document.documentElement.className = '';
+ });
+
+ it('draws the default variant from theme variables', async () => {
+ render();
+
+ await waitFor(() => expect(fillStyles).toContain('rgb(1 2 3)'));
+ expect(fillStyles.every((style) => style === 'rgb(1 2 3)')).toBe(true);
+ });
+
+ it('keeps an explicit colors prop as given', async () => {
+ render();
+
+ await waitFor(() => expect(fillStyles).toContain('#123456'));
+ expect(fillStyles.every((style) => style === '#123456')).toBe(true);
+ });
+
+ it('re-reads the palette when the theme changes', async () => {
+ render();
+ await waitFor(() => expect(fillStyles).toContain('rgb(1 2 3)'));
+
+ fillStyles = [];
+ channels = '9 9 9';
+ document.documentElement.classList.add('dark');
+
+ await waitFor(() => expect(fillStyles).toContain('rgb(9 9 9)'));
+ });
+});
diff --git a/packages/client/src/components/PixelCard.tsx b/packages/client/src/components/PixelCard.tsx
index a539da3f680..4017a899816 100644
--- a/packages/client/src/components/PixelCard.tsx
+++ b/packages/client/src/components/PixelCard.tsx
@@ -355,8 +355,16 @@ export default function PixelCard({
if (containerRef.current) {
obs.observe(containerRef.current);
}
+ /** A mode switch or an applied theme rewrites the root's class or inline variables, which
+ * the resolved palette has already been read from, so lay the pixels out again. */
+ const themeObs = new MutationObserver(initPixels);
+ themeObs.observe(document.documentElement, {
+ attributes: true,
+ attributeFilter: ['class', 'style'],
+ });
return () => {
obs.disconnect();
+ themeObs.disconnect();
cancelAnimationFrame(animationRef.current!);
};
}, [initPixels]);
From bbcdb293eb6fa0a09a10075b08220078de9215f8 Mon Sep 17 00:00:00 2001
From: Marco Beretta <81851188+berry-13@users.noreply.github.com>
Date: Tue, 29 Sep 2026 08:19:15 +0200
Subject: [PATCH 3/6] fix: Keep the Pixel Card's Decorative Presets as Given
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.
---
packages/client/src/components/PixelCard.tsx | 26 +++++---------------
packages/client/src/theme/allowlist.md | 19 +++++++-------
2 files changed, 16 insertions(+), 29 deletions(-)
diff --git a/packages/client/src/components/PixelCard.tsx b/packages/client/src/components/PixelCard.tsx
index 4017a899816..e82416d75b7 100644
--- a/packages/client/src/components/PixelCard.tsx
+++ b/packages/client/src/components/PixelCard.tsx
@@ -139,8 +139,8 @@ const getEffectiveSpeed = (value: number, reducedMotion: boolean) => {
const clamp = (n: number, min = 0, max = 1) => Math.min(Math.max(n, min), max);
-/** Variant palettes name theme channel variables, read off the card when the pixels are laid
- * out, so the canvas draws the active theme instead of a fixed light palette. */
+/** The default palette names theme channel variables, read off the card when the pixels are
+ * laid out, so the canvas draws the active theme instead of a fixed light palette. */
const VARIANTS = {
default: {
gap: 5,
@@ -148,24 +148,10 @@ const VARIANTS = {
colors: '--surface-primary-alt,--surface-tertiary,--border-medium',
noFocus: false,
},
- blue: {
- gap: 10,
- speed: 25,
- colors: '--status-info-subtle,--status-info-border,--status-info',
- noFocus: false,
- },
- yellow: {
- gap: 3,
- speed: 20,
- colors: '--status-warning-subtle,--status-warning-border,--status-warning',
- noFocus: false,
- },
- pink: {
- gap: 6,
- speed: 80,
- colors: '--status-error-subtle,--status-error-border,--status-error',
- noFocus: true,
- },
+ /** Decorative presets no theme role reproduces; kept as given, like a `colors` prop. */
+ blue: { gap: 10, speed: 25, colors: '#e0f2fe,#7dd3fc,#0ea5e9', noFocus: false },
+ yellow: { gap: 3, speed: 20, colors: '#fef08a,#fde047,#eab308', noFocus: false },
+ pink: { gap: 6, speed: 80, colors: '#fecdd3,#fda4af,#e11d48', noFocus: true },
} as const;
/** Resolves `--token` entries to the element's channel triplet; any other entry is a CSS color
diff --git a/packages/client/src/theme/allowlist.md b/packages/client/src/theme/allowlist.md
index e5bdd9625fd..fa4329ba06b 100644
--- a/packages/client/src/theme/allowlist.md
+++ b/packages/client/src/theme/allowlist.md
@@ -23,15 +23,16 @@ is a role, not an exception.
## Brand marks and artwork
-| File | Why |
-| ----------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
-| `packages/client/src/svgs/GoogleIcon.tsx` | Google's multicolour mark, fixed by its brand guidelines. |
-| `packages/client/src/svgs/FacebookIcon.tsx` | Facebook's mark, fixed by its brand guidelines. |
-| `packages/client/src/svgs/DiscordIcon.tsx` | Discord's mark, fixed by its brand guidelines. |
-| `packages/client/src/svgs/GeminiIcon.tsx` | Gemini's gradient mark, fixed by its brand guidelines. |
-| `packages/client/src/svgs/PaLMIcon.tsx` | PaLM's multicolour mark, fixed by its brand guidelines. |
-| `packages/client/src/svgs/BirthdayIcon.tsx` | A multicolour illustration; the palette is artwork, not a UI role. |
-| `packages/client/src/icons/provider/registry.ts`, `icons/provider/Avatar.tsx` | Provider brand colours, each already behind a `--provider-*` variable a deployment can override; the literal is only the fallback. |
+| File | Why |
+| ----------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| `packages/client/src/svgs/GoogleIcon.tsx` | Google's multicolour mark, fixed by its brand guidelines. |
+| `packages/client/src/svgs/FacebookIcon.tsx` | Facebook's mark, fixed by its brand guidelines. |
+| `packages/client/src/svgs/DiscordIcon.tsx` | Discord's mark, fixed by its brand guidelines. |
+| `packages/client/src/svgs/GeminiIcon.tsx` | Gemini's gradient mark, fixed by its brand guidelines. |
+| `packages/client/src/svgs/PaLMIcon.tsx` | PaLM's multicolour mark, fixed by its brand guidelines. |
+| `packages/client/src/svgs/BirthdayIcon.tsx` | A multicolour illustration; the palette is artwork, not a UI role. |
+| `packages/client/src/components/PixelCard.tsx` | The `blue`, `yellow` and `pink` presets are decorative palettes a caller opts into, like the `colors` prop; the app renders only the default, which reads theme roles. |
+| `packages/client/src/icons/provider/registry.ts`, `icons/provider/Avatar.tsx` | Provider brand colours, each already behind a `--provider-*` variable a deployment can override; the literal is only the fallback. |
## Elevation ink in library stylesheets
From 8d52628ec208ee11e05a7393e4c9fb6ce517c4b2 Mon Sep 17 00:00:00 2001
From: Marco Beretta <81851188+berry-13@users.noreply.github.com>
Date: Tue, 29 Sep 2026 08:27:08 +0200
Subject: [PATCH 4/6] test: Cover the SAML Glyph Following the Button Text
Colour
---
e2e/specs/mock/scenarios/saml-glyph.spec.ts | 44 +++++++++++++++++++++
1 file changed, 44 insertions(+)
create mode 100644 e2e/specs/mock/scenarios/saml-glyph.spec.ts
diff --git a/e2e/specs/mock/scenarios/saml-glyph.spec.ts b/e2e/specs/mock/scenarios/saml-glyph.spec.ts
new file mode 100644
index 00000000000..1b9b1aef755
--- /dev/null
+++ b/e2e/specs/mock/scenarios/saml-glyph.spec.ts
@@ -0,0 +1,44 @@
+import { expect, test } from '@playwright/test';
+
+/**
+ * The SAML sign-in button draws a generic key glyph. It used to be filled black, which vanished
+ * on the dark login surface; it now takes the button's own text colour, so it reads in every
+ * mode and theme. SAML is switched on by rewriting `/api/config` the way librechat.yaml would.
+ */
+test.use({ storageState: { cookies: [], origins: [] } });
+
+test('the SAML sign-in glyph is painted with the button text colour @scenario:saml-login-glyph-follows-button-text', async ({
+ page,
+}) => {
+ await page.route(
+ (url) => url.pathname === '/api/config',
+ async (route) => {
+ const response = await route.fetch();
+ const body = await response.json();
+ await route.fulfill({
+ response,
+ json: {
+ ...body,
+ socialLoginEnabled: true,
+ samlLoginEnabled: true,
+ samlImageUrl: '',
+ samlLabel: 'SAML',
+ socialLogins: ['saml'],
+ },
+ });
+ },
+ );
+
+ await page.goto('/login', { timeout: 15_000 });
+ const button = page.getByRole('link', { name: 'SAML' });
+ await expect(button).toBeVisible({ timeout: 15_000 });
+
+ const paint = await button.evaluate((link) => {
+ const glyph = link.querySelector('svg g');
+ return {
+ text: getComputedStyle(link).color,
+ glyph: glyph ? getComputedStyle(glyph).fill : null,
+ };
+ });
+ expect(paint.glyph).toBe(paint.text);
+});
From c64ccdad1d7af379be3abc902119317e04c6ed0e Mon Sep 17 00:00:00 2001
From: Marco Beretta <81851188+berry-13@users.noreply.github.com>
Date: Tue, 29 Sep 2026 08:54:24 +0200
Subject: [PATCH 5/6] fix: Lay the Pixel Card Out Again Only When Its Palette
Changes
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.
---
.../client/src/components/PixelCard.spec.tsx | 13 ++++++++++++
packages/client/src/components/PixelCard.tsx | 21 ++++++++++++++++---
packages/client/src/theme/allowlist.md | 7 +++----
3 files changed, 34 insertions(+), 7 deletions(-)
diff --git a/packages/client/src/components/PixelCard.spec.tsx b/packages/client/src/components/PixelCard.spec.tsx
index 75fce77682e..1ed7c492401 100644
--- a/packages/client/src/components/PixelCard.spec.tsx
+++ b/packages/client/src/components/PixelCard.spec.tsx
@@ -63,4 +63,17 @@ describe('PixelCard palette', () => {
await waitFor(() => expect(fillStyles).toContain('rgb(9 9 9)'));
});
+
+ it('keeps its pixels when an unrelated root variable changes', async () => {
+ render();
+ await waitFor(() => expect(fillStyles).toContain('rgb(1 2 3)'));
+ const measure = HTMLElement.prototype.getBoundingClientRect as jest.Mock;
+ const layouts = measure.mock.calls.length;
+
+ document.documentElement.style.setProperty('--message-scrollbar-gutter', '8px');
+ await new Promise((resolve) => setTimeout(resolve, 50));
+
+ expect(measure.mock.calls.length).toBe(layouts);
+ document.documentElement.style.removeProperty('--message-scrollbar-gutter');
+ });
});
diff --git a/packages/client/src/components/PixelCard.tsx b/packages/client/src/components/PixelCard.tsx
index e82416d75b7..0661b1c2db1 100644
--- a/packages/client/src/components/PixelCard.tsx
+++ b/packages/client/src/components/PixelCard.tsx
@@ -342,8 +342,23 @@ export default function PixelCard({
obs.observe(containerRef.current);
}
/** A mode switch or an applied theme rewrites the root's class or inline variables, which
- * the resolved palette has already been read from, so lay the pixels out again. */
- const themeObs = new MutationObserver(initPixels);
+ * the resolved palette has already been read from. The root's inline style also carries
+ * unrelated variables (scrollbar gutter, font size), so the pixels are laid out again only
+ * when the palette itself resolves differently. */
+ let resolved = containerRef.current
+ ? resolvePalette(palette, containerRef.current).join(',')
+ : '';
+ const themeObs = new MutationObserver(() => {
+ if (!containerRef.current) {
+ return;
+ }
+ const next = resolvePalette(palette, containerRef.current).join(',');
+ if (next === resolved) {
+ return;
+ }
+ resolved = next;
+ initPixels();
+ });
themeObs.observe(document.documentElement, {
attributes: true,
attributeFilter: ['class', 'style'],
@@ -353,7 +368,7 @@ export default function PixelCard({
themeObs.disconnect();
cancelAnimationFrame(animationRef.current!);
};
- }, [initPixels]);
+ }, [initPixels, palette]);
const hoverIn = () => progressRef.current === undefined && startAnim('appear');
const hoverOut = () => progressRef.current === undefined && startAnim('disappear');
diff --git a/packages/client/src/theme/allowlist.md b/packages/client/src/theme/allowlist.md
index fa4329ba06b..5d7c48398ec 100644
--- a/packages/client/src/theme/allowlist.md
+++ b/packages/client/src/theme/allowlist.md
@@ -1,9 +1,8 @@
# Colours outside the theme
-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.
+Every colour in `client/src` and `packages/client/src` that the theme-leakage sweep reached comes from a semantic theme role, except the ones below. Each is a colour that must not follow the theme: a brand mark, artwork, media overlays, 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.
+
+The sweep skipped files an open pull request was editing; those, with their counts, are listed in berry-13/LibreChat#195, and the avatar colours that still need roles in berry-13/LibreChat#194. Until those land, a file absent from this list is not evidence that it holds no literal colour.
A literal the design lint can see stays recorded in `eslint-suppressions.json` (inline disables
of the design rules are rejected by the static checks), so its count there is the exception, not
From 27ced4fcf16f75787247b464c83b4718e8634bc3 Mon Sep 17 00:00:00 2001
From: Marco Beretta <81851188+berry-13@users.noreply.github.com>
Date: Tue, 29 Sep 2026 09:20:09 +0200
Subject: [PATCH 6/6] test: Key the Pixel Card Palette Mock by Variable Name
---
.../client/src/components/PixelCard.spec.tsx | 27 ++++++++++++++++---
1 file changed, 23 insertions(+), 4 deletions(-)
diff --git a/packages/client/src/components/PixelCard.spec.tsx b/packages/client/src/components/PixelCard.spec.tsx
index 1ed7c492401..f47698feea3 100644
--- a/packages/client/src/components/PixelCard.spec.tsx
+++ b/packages/client/src/components/PixelCard.spec.tsx
@@ -5,7 +5,7 @@ import PixelCard from './PixelCard';
* asked the page for: which theme variables, and whether it asks again after a theme change. */
describe('PixelCard palette', () => {
let fillStyles: string[];
- let channels: string;
+ let channels: Record;
beforeEach(() => {
fillStyles = [];
@@ -22,14 +22,18 @@ describe('PixelCard palette', () => {
width: 10,
height: 10,
} as DOMRect);
- channels = '1 2 3';
+ channels = {
+ '--surface-primary-alt': '1 2 3',
+ '--surface-tertiary': '1 2 3',
+ '--border-medium': '1 2 3',
+ };
const computed = window.getComputedStyle.bind(window);
jest.spyOn(window, 'getComputedStyle').mockImplementation((element) => {
const style = computed(element);
return {
...style,
getPropertyValue: (name: string) =>
- name.startsWith('--') ? channels : style.getPropertyValue(name),
+ name.startsWith('--') ? (channels[name] ?? '') : style.getPropertyValue(name),
} as CSSStyleDeclaration;
});
});
@@ -46,6 +50,17 @@ describe('PixelCard palette', () => {
expect(fillStyles.every((style) => style === 'rgb(1 2 3)')).toBe(true);
});
+ it('draws each default slot from its own theme variable, and currentColor when one is unset', async () => {
+ channels = { '--surface-primary-alt': '1 1 1', '--surface-tertiary': '2 2 2' };
+ jest
+ .spyOn(HTMLElement.prototype, 'getBoundingClientRect')
+ .mockReturnValue({ width: 200, height: 200 } as DOMRect);
+ render();
+
+ await waitFor(() => expect(fillStyles.length).toBeGreaterThan(0));
+ expect(new Set(fillStyles)).toEqual(new Set(['rgb(1 1 1)', 'rgb(2 2 2)', 'currentColor']));
+ });
+
it('keeps an explicit colors prop as given', async () => {
render();
@@ -58,7 +73,11 @@ describe('PixelCard palette', () => {
await waitFor(() => expect(fillStyles).toContain('rgb(1 2 3)'));
fillStyles = [];
- channels = '9 9 9';
+ channels = {
+ '--surface-primary-alt': '9 9 9',
+ '--surface-tertiary': '9 9 9',
+ '--border-medium': '9 9 9',
+ };
document.documentElement.classList.add('dark');
await waitFor(() => expect(fillStyles).toContain('rgb(9 9 9)'));