From d07527ff9390d70bf1bc755136eb59872a35769c Mon Sep 17 00:00:00 2001 From: Daniel Xavier <117164123+danielclickh@users.noreply.github.com> Date: Fri, 4 Sep 2026 17:17:48 +0100 Subject: [PATCH] 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) * fix(theme): register muted text role * fix(theme): register chart widget colors --------- Co-authored-by: Claude Opus 5 (1M context) Co-authored-by: Ravi Kumar L --- .../src/components/Insights/InsightsView.tsx | 4 + client/src/style.css | 8 ++ packages/client/src/theme/registry.spec.ts | 77 +++++++++++++++++++ packages/client/src/theme/registry.ts | 18 +++++ packages/client/src/theme/themes/dark.ts | 3 + packages/client/src/theme/themes/default.ts | 3 + packages/client/src/theme/types/index.ts | 9 +++ .../client/src/theme/utils/applyTheme.spec.ts | 45 +++++++++++ packages/client/src/theme/utils/applyTheme.ts | 15 ++++ .../src/theme/utils/createTailwindColors.js | 2 + 10 files changed, 184 insertions(+) diff --git a/client/src/components/Insights/InsightsView.tsx b/client/src/components/Insights/InsightsView.tsx index d86d03b10e..bc0d18a4b9 100644 --- a/client/src/components/Insights/InsightsView.tsx +++ b/client/src/components/Insights/InsightsView.tsx @@ -106,6 +106,10 @@ function Panel({ children, className }: { children: React.ReactNode; className?:
diff --git a/client/src/style.css b/client/src/style.css index 1c3d26dfbb..a63845e6f0 100644 --- a/client/src/style.css +++ b/client/src/style.css @@ -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; diff --git a/packages/client/src/theme/registry.spec.ts b/packages/client/src/theme/registry.spec.ts index 8a7badfdd3..27a52fa755 100644 --- a/packages/client/src/theme/registry.spec.ts +++ b/packages/client/src/theme/registry.spec.ts @@ -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'); diff --git a/packages/client/src/theme/registry.ts b/packages/client/src/theme/registry.ts index c9e5aec448..f778d89c91 100644 --- a/packages/client/src/theme/registry.ts +++ b/packages/client/src/theme/registry.ts @@ -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, appearance: { ...defaultAppearance, ...definition?.appearance }, brands: { ...defaultBrands, ...theme.brands }, diff --git a/packages/client/src/theme/themes/dark.ts b/packages/client/src/theme/themes/dark.ts index b2e377f76c..5179b1f3d5 100644 --- a/packages/client/src/theme/themes/dark.ts +++ b/packages/client/src/theme/themes/dark.ts @@ -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) diff --git a/packages/client/src/theme/themes/default.ts b/packages/client/src/theme/themes/default.ts index 3d3949cc8b..dcc72b7220 100644 --- a/packages/client/src/theme/themes/default.ts +++ b/packages/client/src/theme/themes/default.ts @@ -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) diff --git a/packages/client/src/theme/types/index.ts b/packages/client/src/theme/types/index.ts index ac079885df..074c6e29ff 100644 --- a/packages/client/src/theme/types/index.ts +++ b/packages/client/src/theme/types/index.ts @@ -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; diff --git a/packages/client/src/theme/utils/applyTheme.spec.ts b/packages/client/src/theme/utils/applyTheme.spec.ts index b1e7864c76..8aaeebdb59 100644 --- a/packages/client/src/theme/utils/applyTheme.spec.ts +++ b/packages/client/src/theme/utils/applyTheme.spec.ts @@ -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'); diff --git a/packages/client/src/theme/utils/applyTheme.ts b/packages/client/src/theme/utils/applyTheme.ts index e969d25a35..d5a209949c 100644 --- a/packages/client/src/theme/utils/applyTheme.ts +++ b/packages/client/src/theme/utils/applyTheme.ts @@ -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; } diff --git a/packages/client/src/theme/utils/createTailwindColors.js b/packages/client/src/theme/utils/createTailwindColors.js index 040226e04a..d065a73457 100644 --- a/packages/client/src/theme/utils/createTailwindColors.js +++ b/packages/client/src/theme/utils/createTailwindColors.js @@ -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'),