style(insights): give panels their own surface in dark mode (#15552)

* style(insights): give panels the Click UI chartWidget surface in dark mode

In dark mode the panels drew the shared `surface-primary` (#0d0d0d) against a
page of the same colour, so the cards read as one flat sheet with only a faint
border separating them. Click UI treats a dashboard widget as its own surface,
a step lighter than the page behind it.

Add the `dashboards.chartWidget` surface and stroke as theme tokens and apply
them to the Insights panels under `dark:` only, so light mode keeps the shared
surface/border tokens untouched:

  dark   #282828 surface / #323232 stroke
  light  #ffffff surface / #e6e7e9 stroke  (defined for completeness; unused)

`Panel` is local to InsightsView, so this cannot leak into other pages -- all
six panels (the four KPI tiles, both user tables, the conversation list, and
the loading/error cards) pick it up from the one definition.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(theme): register muted text role

* fix(theme): register chart widget colors

---------

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Co-authored-by: Ravi Kumar L <ravi.lazar@clickhouse.com>
This commit is contained in:
Daniel Xavier 2026-09-04 17:17:48 +01:00 • committed by GitHub
parent d21bffa4c6
commit d07527ff93
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
10 changed files with 184 additions and 0 deletions

View file

@ -106,6 +106,10 @@ function Panel({ children, className }: { children: React.ReactNode; className?:
<section
className={cn(
'min-w-0 rounded-lg border border-border-light bg-surface-primary p-5',
/** Dark mode only: Click UI gives dashboard widgets their own surface and
* stroke, a step lighter than the page behind them. Light mode keeps the
* shared surface/border tokens. */
'dark:border-chart-widget-stroke dark:bg-chart-widget-surface',
className,
)}
>

View file

@ -173,6 +173,8 @@ html {
--surface-hover-alt: var(--gray-300);
--surface-composer-hover: var(--gray-200);
--surface-primary: var(--white);
--chart-widget-surface: 255 255 255; /* Click UI dashboards.chartWidget.color.background.default #ffffff */
--chart-widget-stroke: 230 231 233; /* Click UI dashboards.chartWidget.color.stroke.default #e6e7e9 */
--surface-primary-alt: var(--gray-50);
--surface-primary-contrast: var(--gray-100);
--surface-secondary: var(--gray-50);
@ -283,6 +285,8 @@ html {
--surface-hover-alt: var(--gray-600);
--surface-composer-hover: var(--gray-600);
--surface-primary: var(--gray-900);
--chart-widget-surface: 40 40 40; /* Click UI dashboards.chartWidget.color.background.default #282828 */
--chart-widget-stroke: 50 50 50; /* Click UI dashboards.chartWidget.color.stroke.default #323232 */
--surface-primary-alt: var(--gray-850);
--surface-primary-contrast: var(--gray-850);
--surface-secondary: var(--gray-800);
@ -355,6 +359,8 @@ html {
--text-tertiary: var(--gizmo-gray-500);
--text-muted: 105 110 121; /* Click UI global.color.text.muted #696e79 */
--surface-primary: var(--white);
--chart-widget-surface: 255 255 255; /* Click UI dashboards.chartWidget.color.background.default #ffffff */
--chart-widget-stroke: 230 231 233; /* Click UI dashboards.chartWidget.color.stroke.default #e6e7e9 */
--surface-secondary: var(--gray-50);
--surface-tertiary: var(--gray-100);
--border-light: 0 0 0;
@ -372,6 +378,8 @@ html {
--text-tertiary: var(--gizmo-gray-500);
--text-muted: 179 182 189; /* Click UI global.color.text.muted #b3b6bd */
--surface-primary: var(--gray-900);
--chart-widget-surface: 40 40 40; /* Click UI dashboards.chartWidget.color.background.default #282828 */
--chart-widget-stroke: 50 50 50; /* Click UI dashboards.chartWidget.color.stroke.default #323232 */
--surface-secondary: var(--gray-800);
--surface-tertiary: var(--gray-700);
--border-light: 217 217 227;

View file

@ -88,6 +88,83 @@ describe('theme registry', () => {
);
});
it('derives omitted muted text from a theme that restates tertiary text', () => {
const resolved = resolveTheme(
{
version: 1,
name: 'legacy-muted-reference',
modes: { dark: { colors: { 'rgb-text-tertiary': '120 121 122' } } },
},
'dark',
);
expect(resolved.colors['rgb-text-muted']).toBe('120 121 122');
});
it('preserves an explicit muted text color', () => {
const resolved = resolveTheme(
{
version: 1,
name: 'explicit-muted-reference',
modes: {
light: {
colors: {
'rgb-text-tertiary': '120 121 122',
'rgb-text-muted': '90 91 92',
},
},
},
},
'light',
);
expect(resolved.colors['rgb-text-muted']).toBe('90 91 92');
});
it('derives omitted chart widget colors from the previous panel roles', () => {
const resolved = resolveTheme(
{
version: 1,
name: 'legacy-chart-widget-reference',
modes: {
dark: {
colors: {
'rgb-surface-primary': '20 21 22',
'rgb-border-light': '30 31 32',
},
},
},
},
'dark',
);
expect(resolved.colors['rgb-chart-widget-surface']).toBe('20 21 22');
expect(resolved.colors['rgb-chart-widget-stroke']).toBe('30 31 32');
});
it('preserves explicit chart widget colors', () => {
const resolved = resolveTheme(
{
version: 1,
name: 'explicit-chart-widget-reference',
modes: {
dark: {
colors: {
'rgb-surface-primary': '20 21 22',
'rgb-border-light': '30 31 32',
'rgb-chart-widget-surface': '40 41 42',
'rgb-chart-widget-stroke': '50 51 52',
},
},
},
},
'dark',
);
expect(resolved.colors['rgb-chart-widget-surface']).toBe('40 41 42');
expect(resolved.colors['rgb-chart-widget-stroke']).toBe('50 51 52');
});
it('resolves provider brand tokens and lets a theme override them', () => {
const defaults = resolveTheme(libreChatTheme, 'light');
expect(defaults.brands['provider-anthropic']).toBe('#d09a74');

View file

@ -287,6 +287,21 @@ export function resolveTheme(theme: ThemeDefinition, mode: ThemeMode): ResolvedT
customColors?.['rgb-text-primary'] !== undefined
? { 'rgb-shimmer-base': customColors['rgb-text-primary'] }
: {};
const textMutedFallback =
customColors?.['rgb-text-muted'] === undefined &&
customColors?.['rgb-text-tertiary'] !== undefined
? { 'rgb-text-muted': customColors['rgb-text-tertiary'] }
: {};
const chartWidgetSurfaceFallback =
customColors?.['rgb-chart-widget-surface'] === undefined &&
customColors?.['rgb-surface-primary'] !== undefined
? { 'rgb-chart-widget-surface': customColors['rgb-surface-primary'] }
: {};
const chartWidgetStrokeFallback =
customColors?.['rgb-chart-widget-stroke'] === undefined &&
customColors?.['rgb-border-light'] !== undefined
? { 'rgb-chart-widget-stroke': customColors['rgb-border-light'] }
: {};
return {
version: THEME_VERSION,
@ -297,6 +312,9 @@ export function resolveTheme(theme: ThemeDefinition, mode: ThemeMode): ResolvedT
...customColors,
...composerHoverFallback,
...shimmerBaseFallback,
...textMutedFallback,
...chartWidgetSurfaceFallback,
...chartWidgetStrokeFallback,
} as Required<IThemeRGB>,
appearance: { ...defaultAppearance, ...definition?.appearance },
brands: { ...defaultBrands, ...theme.brands },

View file

@ -10,6 +10,7 @@ export const darkTheme: IThemeRGB = {
'rgb-text-secondary': '205 205 205', // #cdcdcd (gray-300)
'rgb-text-secondary-alt': '153 150 150', // #999696 (gray-400)
'rgb-text-tertiary': '153 150 150', // #999696 (gray-400)
'rgb-text-muted': '179 182 189', // #b3b6bd (Click UI text.muted)
'rgb-text-warning': '245 158 11', // #f59e0b (amber-500)
'rgb-text-destructive': '248 113 113', // #f87171 (red-400)
'rgb-shimmer-base': '255 255 255', // #ffffff, carried at 0.8 alpha
@ -37,6 +38,8 @@ export const darkTheme: IThemeRGB = {
'rgb-surface-hover-alt': '66 66 66', // #424242 (gray-600)
'rgb-surface-composer-hover': '66 66 66', // #424242 (gray-600)
'rgb-surface-primary': '13 13 13', // #0d0d0d (gray-900)
'rgb-chart-widget-surface': '40 40 40', // #282828 (Click UI chart widget)
'rgb-chart-widget-stroke': '50 50 50', // #323232 (Click UI chart widget)
'rgb-surface-primary-alt': '23 23 23', // #171717 (gray-850)
'rgb-surface-primary-contrast': '23 23 23', // #171717 (gray-850)
'rgb-surface-secondary': '33 33 33', // #212121 (gray-800)

View file

@ -10,6 +10,7 @@ export const defaultTheme: IThemeRGB = {
'rgb-text-secondary': '66 66 66', // #424242 (gray-600)
'rgb-text-secondary-alt': '89 89 89', // #595959 (gray-500)
'rgb-text-tertiary': '89 89 89', // #595959 (gray-500)
'rgb-text-muted': '105 110 121', // #696e79 (Click UI text.muted)
'rgb-text-warning': '180 83 9', // #b45309 (amber-700)
'rgb-text-destructive': '220 38 38', // #dc2626 (red-600)
'rgb-shimmer-base': '33 33 33', // #212121 (gray-800), matching text-primary
@ -37,6 +38,8 @@ export const defaultTheme: IThemeRGB = {
'rgb-surface-hover-alt': '205 205 205', // #cdcdcd (gray-300)
'rgb-surface-composer-hover': '227 227 227', // #e3e3e3 (gray-200)
'rgb-surface-primary': '255 255 255', // #fff (white)
'rgb-chart-widget-surface': '255 255 255', // #fff (Click UI chart widget)
'rgb-chart-widget-stroke': '230 231 233', // #e6e7e9 (Click UI chart widget)
'rgb-surface-primary-alt': '247 247 248', // #f7f7f8 (gray-50)
'rgb-surface-primary-contrast': '236 236 236', // #ececec (gray-100)
'rgb-surface-secondary': '247 247 248', // #f7f7f8 (gray-50)

View file

@ -8,6 +8,7 @@ export interface IThemeRGB {
'rgb-text-secondary'?: string;
'rgb-text-secondary-alt'?: string;
'rgb-text-tertiary'?: string;
'rgb-text-muted'?: string;
'rgb-text-warning'?: string;
'rgb-text-destructive'?: string;
/** Bright and dipped stops of the in-flight label sweep (`.shimmer`). Their
@ -38,6 +39,8 @@ export interface IThemeRGB {
'rgb-surface-hover-alt'?: string;
'rgb-surface-composer-hover'?: string;
'rgb-surface-primary'?: string;
'rgb-chart-widget-surface'?: string;
'rgb-chart-widget-stroke'?: string;
'rgb-surface-primary-alt'?: string;
'rgb-surface-primary-contrast'?: string;
'rgb-surface-secondary'?: string;
@ -116,6 +119,7 @@ export interface IThemeVariables {
'--text-secondary': string;
'--text-secondary-alt': string;
'--text-tertiary': string;
'--text-muted': string;
'--text-warning': string;
'--text-destructive': string;
'--shimmer-base': string;
@ -135,6 +139,8 @@ export interface IThemeVariables {
'--surface-hover-alt': string;
'--surface-composer-hover': string;
'--surface-primary': string;
'--chart-widget-surface': string;
'--chart-widget-stroke': string;
'--surface-primary-alt': string;
'--surface-primary-contrast': string;
'--surface-secondary': string;
@ -205,6 +211,7 @@ export interface IThemeColors {
'text-secondary'?: string;
'text-secondary-alt'?: string;
'text-tertiary'?: string;
'text-muted'?: string;
'text-warning'?: string;
'text-destructive'?: string;
link?: string;
@ -222,6 +229,8 @@ export interface IThemeColors {
'surface-hover-alt'?: string;
'surface-composer-hover'?: string;
'surface-primary'?: string;
'chart-widget-surface'?: string;
'chart-widget-stroke'?: string;
'surface-primary-alt'?: string;
'surface-primary-contrast'?: string;
'surface-secondary'?: string;

View file

@ -14,6 +14,9 @@ const semanticProperties = [
'--accent-primary',
'--accent-primary-hover',
'--text-destructive',
'--text-muted',
'--chart-widget-surface',
'--chart-widget-stroke',
'--border-destructive',
'--status-success',
'--status-success-subtle',
@ -185,6 +188,48 @@ describe('applyTheme', () => {
expect(root.style.getPropertyValue('--shimmer-base')).toBe('90 80 70');
});
it('carries a legacy theme without muted text onto its tertiary text color', () => {
const root = document.documentElement;
applyTheme({ 'rgb-text-tertiary': '80 81 82' }, root);
expect(root.style.getPropertyValue('--text-muted')).toBe('80 81 82');
});
it('leaves a legacy theme that names muted text alone', () => {
const root = document.documentElement;
applyTheme({ 'rgb-text-tertiary': '80 81 82', 'rgb-text-muted': '100 101 102' }, root);
expect(root.style.getPropertyValue('--text-muted')).toBe('100 101 102');
});
it('carries legacy panel colors onto chart widgets', () => {
const root = document.documentElement;
applyTheme({ 'rgb-surface-primary': '20 21 22', 'rgb-border-light': '30 31 32' }, root);
expect(root.style.getPropertyValue('--chart-widget-surface')).toBe('20 21 22');
expect(root.style.getPropertyValue('--chart-widget-stroke')).toBe('30 31 32');
});
it('leaves explicit chart widget colors alone', () => {
const root = document.documentElement;
applyTheme(
{
'rgb-surface-primary': '20 21 22',
'rgb-border-light': '30 31 32',
'rgb-chart-widget-surface': '40 41 42',
'rgb-chart-widget-stroke': '50 51 52',
},
root,
);
expect(root.style.getPropertyValue('--chart-widget-surface')).toBe('40 41 42');
expect(root.style.getPropertyValue('--chart-widget-stroke')).toBe('50 51 52');
});
it('clears only properties owned by the theme module', () => {
const root = document.documentElement;
root.style.setProperty('--text-primary', '1 2 3');

View file

@ -47,6 +47,21 @@ function mapColors(colors: IThemeRGB): Array<[string, string]> {
variables.push(['--shimmer-base', colors['rgb-text-primary']]);
}
if (colors['rgb-text-muted'] === undefined && colors['rgb-text-tertiary'] !== undefined) {
variables.push(['--text-muted', colors['rgb-text-tertiary']]);
}
if (
colors['rgb-chart-widget-surface'] === undefined &&
colors['rgb-surface-primary'] !== undefined
) {
variables.push(['--chart-widget-surface', colors['rgb-surface-primary']]);
}
if (colors['rgb-chart-widget-stroke'] === undefined && colors['rgb-border-light'] !== undefined) {
variables.push(['--chart-widget-stroke', colors['rgb-border-light']]);
}
return variables;
}

View file

@ -70,6 +70,8 @@ function createTailwindColors() {
'text-secondary-alt': cssVar('--text-secondary-alt'),
'text-tertiary': cssVar('--text-tertiary'),
'text-muted': cssVar('--text-muted'),
'chart-widget-surface': cssVar('--chart-widget-surface'),
'chart-widget-stroke': cssVar('--chart-widget-stroke'),
'text-warning': cssVar('--text-warning'),
'text-destructive': cssVar('--text-destructive'),