From d0f02302e377335bb554fde92012ef7c5d5c929d Mon Sep 17 00:00:00 2001 From: Paddy Mullen Date: Wed, 25 Feb 2026 06:35:51 -0500 Subject: [PATCH] feat: add custom theme support via component_config MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add ThemeConfig (colorScheme, accentColor, accentHoverColor, backgroundColor, foregroundColor, oddRowBackgroundColor, borderColor) nested in ComponentConfig. Flows through existing pipeline — Python widget, server /load endpoint, and JS rendering all support theme overrides with no new traitlets or sync wiring. Resolves #582 Co-Authored-By: Claude Opus 4.6 --- buckaroo/dataflow/styling_core.py | 13 +- buckaroo/server/handlers.py | 23 +++- .../pw-tests/theme-custom-server.spec.ts | 84 ++++++++++++ .../pw-tests/theme-custom.spec.ts | 92 +++++++++++++ .../src/components/BuckarooWidgetInfinite.tsx | 1 + .../src/components/DCFCell.tsx | 1 + .../DFViewerParts/DFViewerInfinite.tsx | 46 +++++-- .../src/components/DFViewerParts/DFWhole.ts | 11 ++ .../src/components/DFViewerParts/gridUtils.ts | 35 ++++- .../src/components/StatusBar.tsx | 16 ++- .../stories/ThemeCustomization.stories.tsx | 128 ++++++++++++++++++ .../buckaroo-js-core/src/style/dcf-npm.css | 10 +- tests/unit/test_theme_config.py | 42 ++++++ 13 files changed, 469 insertions(+), 33 deletions(-) create mode 100644 packages/buckaroo-js-core/pw-tests/theme-custom-server.spec.ts create mode 100644 packages/buckaroo-js-core/pw-tests/theme-custom.spec.ts create mode 100644 packages/buckaroo-js-core/src/stories/ThemeCustomization.stories.tsx create mode 100644 tests/unit/test_theme_config.py diff --git a/buckaroo/dataflow/styling_core.py b/buckaroo/dataflow/styling_core.py index 42cb40e08..a2db22999 100644 --- a/buckaroo/dataflow/styling_core.py +++ b/buckaroo/dataflow/styling_core.py @@ -155,13 +155,24 @@ 'default_renderer_columns': NotRequired[List[str]] # used to render index column values with string not the specified displayer }) +ThemeConfig = TypedDict('ThemeConfig', { + 'colorScheme': NotRequired[Literal["light", "dark", "auto"]], + 'accentColor': NotRequired[str], + 'accentHoverColor': NotRequired[str], + 'backgroundColor': NotRequired[str], + 'foregroundColor': NotRequired[str], + 'oddRowBackgroundColor': NotRequired[str], + 'borderColor': NotRequired[str], +}) + ComponentConfig = TypedDict('ComponentConfig', { 'height_fraction': NotRequired[float], 'dfvHeight': NotRequired[int], # temporary debugging prop 'layoutType': NotRequired[Literal["autoHeight", "normal"]], 'shortMode': NotRequired[bool], 'selectionBackground': NotRequired[str], - 'className': NotRequired[str] + 'className': NotRequired[str], + 'theme': NotRequired[ThemeConfig], }) DFViewerConfig = TypedDict('DFViewerConfig', { diff --git a/buckaroo/server/handlers.py b/buckaroo/server/handlers.py index 755c8ed84..d4ad02ff1 100644 --- a/buckaroo/server/handlers.py +++ b/buckaroo/server/handlers.py @@ -149,9 +149,9 @@ def _parse_request_body(self) -> dict: return body def _validate_request(self, body: dict) -> tuple: - """Validate and extract session_id, path, mode, prompt, and no_browser from request. + """Validate and extract session_id, path, mode, prompt, no_browser, and component_config from request. - Returns (session_id, path, mode, prompt, no_browser) or a tuple of Nones on error. + Returns (session_id, path, mode, prompt, no_browser, component_config) or a tuple of Nones on error. """ session_id = body.get("session") path = body.get("path") @@ -159,12 +159,13 @@ def _validate_request(self, body: dict) -> tuple: if not session_id or not path: self.set_status(400) self.write({"error": "Missing 'session' or 'path'"}) - return None, None, None, None, None + return None, None, None, None, None, None mode = body.get("mode", "viewer") prompt = body.get("prompt", "") no_browser = bool(body.get("no_browser", False)) - return session_id, path, mode, prompt, no_browser + component_config = body.get("component_config") + return session_id, path, mode, prompt, no_browser, component_config def _load_lazy_polars(self, session, path: str, ldf, metadata: dict): """Set up lazy polars session state.""" @@ -235,7 +236,7 @@ async def post(self): if body is None: return - session_id, path, mode, prompt, no_browser = self._validate_request(body) + session_id, path, mode, prompt, no_browser, component_config = self._validate_request(body) if session_id is None: return @@ -243,6 +244,8 @@ async def post(self): session = sessions.get_or_create(session_id, path) session.mode = mode session.prompt = prompt + if component_config: + session.component_config = component_config # Load data in appropriate mode file_obj, metadata = self._load_file_with_error_handling(path, is_lazy=(mode == "lazy")) @@ -272,6 +275,16 @@ async def post(self): session.df_data_dict = display_state["df_data_dict"] session.df_meta = display_state["df_meta"] + # Merge component_config into df_display_args if provided + if component_config and session.df_display_args: + for key in session.df_display_args: + dvc = session.df_display_args[key].get("df_viewer_config") + if dvc is not None: + dvc["component_config"] = { + **dvc.get("component_config", {}), + **component_config, + } + # Notify connected clients and open browser self._push_state_to_clients(session, metadata) browser_action = "skipped" if no_browser else self._handle_browser_window(session_id) diff --git a/packages/buckaroo-js-core/pw-tests/theme-custom-server.spec.ts b/packages/buckaroo-js-core/pw-tests/theme-custom-server.spec.ts new file mode 100644 index 000000000..eea9a0c3f --- /dev/null +++ b/packages/buckaroo-js-core/pw-tests/theme-custom-server.spec.ts @@ -0,0 +1,84 @@ +import { test, expect } from '@playwright/test'; +import { waitForGrid } from './server-helpers'; +import * as fs from 'fs'; +import * as path from 'path'; +import * as os from 'os'; +import { fileURLToPath } from 'url'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const screenshotsDir = path.join(__dirname, '..', 'screenshots'); + +const PORT = 8701; +const BASE = `http://localhost:${PORT}`; + +function writeTempCsv(): string { + const header = 'name,age,score'; + const rows = [ + 'Alice,30,88.5', + 'Bob,25,92.3', + 'Charlie,35,76.1', + 'Diana,28,95.0', + 'Eve,32,81.7', + ]; + const content = [header, ...rows].join('\n') + '\n'; + const tmpPath = path.join(os.tmpdir(), `buckaroo_theme_${Date.now()}.csv`); + fs.writeFileSync(tmpPath, content); + return tmpPath; +} + +test.beforeAll(() => { + fs.mkdirSync(screenshotsDir, { recursive: true }); +}); + +test('server: theme config applied via /load', async ({ page, request }) => { + const csvPath = writeTempCsv(); + const session = `theme-${Date.now()}`; + const resp = await request.post(`${BASE}/load`, { + data: { + session, + path: csvPath, + component_config: { + theme: { + accentColor: '#ff6600', + backgroundColor: '#1a1a2e', + colorScheme: 'dark', + }, + }, + }, + }); + expect(resp.ok()).toBeTruthy(); + + await page.goto(`${BASE}/s/${session}`); + await waitForGrid(page); + + // Assert background color on the grid + const gridBody = page.locator('.ag-body-viewport').first(); + const bg = await gridBody.evaluate(el => getComputedStyle(el).backgroundColor); + expect(bg).toBe('rgb(26, 26, 46)'); // #1a1a2e + + await page.screenshot({ + path: path.join(screenshotsDir, 'server-theme-custom.png'), + fullPage: true, + }); + + fs.unlinkSync(csvPath); +}); + +test('server: no theme = default rendering', async ({ page, request }) => { + const csvPath = writeTempCsv(); + const session = `no-theme-${Date.now()}`; + const resp = await request.post(`${BASE}/load`, { + data: { session, path: csvPath }, + }); + expect(resp.ok()).toBeTruthy(); + + await page.emulateMedia({ colorScheme: 'light' }); + await page.goto(`${BASE}/s/${session}`); + await waitForGrid(page); + + const gridBody = page.locator('.ag-body-viewport').first(); + const bg = await gridBody.evaluate(el => getComputedStyle(el).backgroundColor); + expect(bg).toBe('rgb(255, 255, 255)'); + + fs.unlinkSync(csvPath); +}); diff --git a/packages/buckaroo-js-core/pw-tests/theme-custom.spec.ts b/packages/buckaroo-js-core/pw-tests/theme-custom.spec.ts new file mode 100644 index 000000000..c96f6baaa --- /dev/null +++ b/packages/buckaroo-js-core/pw-tests/theme-custom.spec.ts @@ -0,0 +1,92 @@ +import { test, expect } from '@playwright/test'; +import { waitForCells } from './ag-pw-utils'; +import * as path from 'path'; +import * as fs from 'fs'; +import { fileURLToPath } from 'url'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); +const screenshotsDir = path.join(__dirname, '..', 'screenshots'); + +const STORYBOOK_BASE = 'http://localhost:6006/iframe.html?viewMode=story&id='; + +// Ensure screenshots directory exists +test.beforeAll(() => { + fs.mkdirSync(screenshotsDir, { recursive: true }); +}); + +test('default story renders without theme overrides', async ({ page }) => { + await page.emulateMedia({ colorScheme: 'light' }); + await page.goto(`${STORYBOOK_BASE}buckaroo-theme-themecustomization--default-no-theme`); + await waitForCells(page); + + const gridBody = page.locator('.ag-body-viewport').first(); + const bg = await gridBody.evaluate(el => getComputedStyle(el).backgroundColor); + // Light mode default: white + expect(bg).toBe('rgb(255, 255, 255)'); +}); + +test('custom accent color applied to selected column', async ({ page }) => { + await page.goto(`${STORYBOOK_BASE}buckaroo-theme-themecustomization--custom-accent`); + await waitForCells(page); + + // Click column header to select it + await page.locator('.ag-header-cell').nth(1).click(); + + // Assert the accent color is applied to cells + const cell = page.locator('.ag-cell[col-id="a"]').first(); + await expect(cell).toHaveCSS('background-color', 'rgb(255, 102, 0)'); // #ff6600 +}); + +test('forced dark scheme ignores OS light preference', async ({ page }) => { + await page.emulateMedia({ colorScheme: 'light' }); // OS says light + await page.goto(`${STORYBOOK_BASE}buckaroo-theme-themecustomization--forced-dark`); + await waitForCells(page); + + // Grid should use dark background despite OS light mode + const gridBody = page.locator('.ag-body-viewport').first(); + const bg = await gridBody.evaluate(el => getComputedStyle(el).backgroundColor); + expect(bg).toBe('rgb(26, 26, 46)'); // #1a1a2e +}); + +test('forced light scheme ignores OS dark preference', async ({ page }) => { + await page.emulateMedia({ colorScheme: 'dark' }); // OS says dark + await page.goto(`${STORYBOOK_BASE}buckaroo-theme-themecustomization--forced-light`); + await waitForCells(page); + + const gridBody = page.locator('.ag-body-viewport').first(); + const bg = await gridBody.evaluate(el => getComputedStyle(el).backgroundColor); + expect(bg).toBe('rgb(250, 250, 250)'); // #fafafa +}); + +test('full custom theme applies all properties', async ({ page }) => { + await page.goto(`${STORYBOOK_BASE}buckaroo-theme-themecustomization--full-custom`); + await waitForCells(page); + + // Background + const gridBody = page.locator('.ag-body-viewport').first(); + const bg = await gridBody.evaluate(el => getComputedStyle(el).backgroundColor); + expect(bg).toBe('rgb(26, 26, 46)'); // #1a1a2e + + // Screenshot for visual debugging + await page.screenshot({ + path: path.join(screenshotsDir, 'theme-full-custom.png'), + fullPage: true, + }); +}); + +test('screenshot: default vs custom comparison', async ({ page }) => { + await page.emulateMedia({ colorScheme: 'light' }); + await page.goto(`${STORYBOOK_BASE}buckaroo-theme-themecustomization--default-no-theme`); + await waitForCells(page); + await page.screenshot({ + path: path.join(screenshotsDir, 'theme-default-light.png'), + fullPage: true, + }); + + await page.goto(`${STORYBOOK_BASE}buckaroo-theme-themecustomization--forced-dark`); + await waitForCells(page); + await page.screenshot({ + path: path.join(screenshotsDir, 'theme-forced-dark.png'), + fullPage: true, + }); +}); diff --git a/packages/buckaroo-js-core/src/components/BuckarooWidgetInfinite.tsx b/packages/buckaroo-js-core/src/components/BuckarooWidgetInfinite.tsx index b71858b56..ece09f468 100644 --- a/packages/buckaroo-js-core/src/components/BuckarooWidgetInfinite.tsx +++ b/packages/buckaroo-js-core/src/components/BuckarooWidgetInfinite.tsx @@ -184,6 +184,7 @@ export function BuckarooInfiniteWidget({ buckarooState={buckaroo_state} setBuckarooState={on_buckaroo_state} buckarooOptions={buckaroo_options} + themeConfig={cDisp.df_viewer_config?.component_config?.theme} /> {error_info ?
{error_info}
: null} -
) @@ -185,7 +199,9 @@ export function DFViewerInfiniteInner({ setActiveCol, outside_df_params, renderStartTime: _renderStartTime, - hs + hs, + themeConfig, + effectiveScheme }: { data_wrapper: DatasourceOrRaw; df_viewer_config: DFViewerConfig; @@ -197,7 +213,9 @@ export function DFViewerInfiniteInner({ // them as keys to get updated data outside_df_params?: any; renderStartTime:any; - hs:HeightStyleI + hs:HeightStyleI; + themeConfig?: ThemeConfig; + effectiveScheme?: 'light' | 'dark'; }) { @@ -255,7 +273,7 @@ export function DFViewerInfiniteInner({ } if (activeCol === field) { //return {background:selectBackground} - return { background: AccentColor } + return { background: themeConfig?.accentColor || AccentColor } } return { background: "inherit" } @@ -290,15 +308,15 @@ export function DFViewerInfiniteInner({ [outside_df_params], ); - const colorScheme = useColorScheme(); - const myTheme = useMemo(() => getThemeForScheme(colorScheme).withParams({ + const resolvedScheme = effectiveScheme || 'dark'; + const myTheme = useMemo(() => getThemeForScheme(resolvedScheme, themeConfig).withParams({ headerRowBorder: true, headerColumnBorder: true, headerColumnResizeHandleWidth: 0, - ...(colorScheme === 'dark' - ? { backgroundColor: "#121212", oddRowBackgroundColor: '#3f3f3f' } - : { backgroundColor: "#ffffff", oddRowBackgroundColor: '#f0f0f0' }), - }), [colorScheme]); + ...(resolvedScheme === 'dark' + ? { backgroundColor: themeConfig?.backgroundColor || "#121212", oddRowBackgroundColor: themeConfig?.oddRowBackgroundColor || '#3f3f3f' } + : { backgroundColor: themeConfig?.backgroundColor || "#ffffff", oddRowBackgroundColor: themeConfig?.oddRowBackgroundColor || '#f0f0f0' }), + }), [resolvedScheme, themeConfig]); const gridOptions: GridOptions = useMemo( () => { return { ...outerGridOptions(setActiveCol, df_viewer_config.extra_grid_config), diff --git a/packages/buckaroo-js-core/src/components/DFViewerParts/DFWhole.ts b/packages/buckaroo-js-core/src/components/DFViewerParts/DFWhole.ts index b454b2dd7..58ccc4123 100644 --- a/packages/buckaroo-js-core/src/components/DFViewerParts/DFWhole.ts +++ b/packages/buckaroo-js-core/src/components/DFViewerParts/DFWhole.ts @@ -169,6 +169,16 @@ export type PinnedRowConfig = { default_renderer_columns?: string[]; }; +export type ThemeConfig = { + colorScheme?: 'light' | 'dark' | 'auto'; + accentColor?: string; + accentHoverColor?: string; + backgroundColor?: string; + foregroundColor?: string; + oddRowBackgroundColor?: string; + borderColor?: string; +}; + export type ComponentConfig = { height_fraction?: number; // temporary debugging props @@ -177,6 +187,7 @@ export type ComponentConfig = { shortMode?: boolean; selectionBackground?: string; className?: string; + theme?: ThemeConfig; }; export interface DFViewerConfig { diff --git a/packages/buckaroo-js-core/src/components/DFViewerParts/gridUtils.ts b/packages/buckaroo-js-core/src/components/DFViewerParts/gridUtils.ts index 89982bc6c..9ef35b22f 100644 --- a/packages/buckaroo-js-core/src/components/DFViewerParts/gridUtils.ts +++ b/packages/buckaroo-js-core/src/components/DFViewerParts/gridUtils.ts @@ -578,6 +578,37 @@ export const myThemeLight: Theme = themeAlpine.withPart(colorSchemeLight).withPa /** @deprecated Use getThemeForScheme() instead */ export const myTheme: Theme = myThemeDark; -export function getThemeForScheme(scheme: 'light' | 'dark'): Theme { - return scheme === 'light' ? myThemeLight : myThemeDark; +export type ThemeConfig = { + colorScheme?: 'light' | 'dark' | 'auto'; + accentColor?: string; + accentHoverColor?: string; + backgroundColor?: string; + foregroundColor?: string; + oddRowBackgroundColor?: string; + borderColor?: string; +}; + +export function resolveColorScheme( + osScheme: 'light' | 'dark', + themeConfig?: ThemeConfig +): 'light' | 'dark' { + const override = themeConfig?.colorScheme; + if (override && override !== 'auto') { + return override; + } + return osScheme; +} + +export function getThemeForScheme(scheme: 'light' | 'dark', themeConfig?: ThemeConfig): Theme { + const base = scheme === 'light' ? myThemeLight : myThemeDark; + if (!themeConfig) return base; + + const overrides: Record = {}; + if (themeConfig.backgroundColor) overrides.backgroundColor = themeConfig.backgroundColor; + if (themeConfig.foregroundColor) overrides.foregroundColor = themeConfig.foregroundColor; + if (themeConfig.oddRowBackgroundColor) overrides.oddRowBackgroundColor = themeConfig.oddRowBackgroundColor; + if (themeConfig.borderColor) overrides.borderColor = themeConfig.borderColor; + + if (Object.keys(overrides).length === 0) return base; + return base.withParams(overrides); } diff --git a/packages/buckaroo-js-core/src/components/StatusBar.tsx b/packages/buckaroo-js-core/src/components/StatusBar.tsx index ae50884b9..ea0050c74 100644 --- a/packages/buckaroo-js-core/src/components/StatusBar.tsx +++ b/packages/buckaroo-js-core/src/components/StatusBar.tsx @@ -9,7 +9,8 @@ import { BuckarooOptions } from "./WidgetTypes"; import { BuckarooState, BKeys } from "./WidgetTypes"; import { ClientSideRowModelModule } from "@ag-grid-community/client-side-row-model"; import { CustomCellEditorProps } from '@ag-grid-community/react'; -import { getThemeForScheme } from "./DFViewerParts/gridUtils"; +import { getThemeForScheme, resolveColorScheme } from "./DFViewerParts/gridUtils"; +import type { ThemeConfig } from "./DFViewerParts/gridUtils"; import { Theme } from "@ag-grid-community/theming"; import { useColorScheme } from "./useColorScheme"; @@ -202,13 +203,15 @@ export function StatusBar({ buckarooState, setBuckarooState, buckarooOptions, - heightOverride + heightOverride, + themeConfig }: { dfMeta: DFMeta; buckarooState: BuckarooState; setBuckarooState: React.Dispatch>; buckarooOptions: BuckarooOptions; heightOverride?: number; + themeConfig?: ThemeConfig; }) { if (false) { console.log("heightOverride", heightOverride); @@ -364,12 +367,13 @@ export function StatusBar({ cellStyle: { textAlign: "left" }, }; - const colorScheme = useColorScheme(); - const statusTheme: Theme = useMemo(()=> getThemeForScheme(colorScheme).withParams({ + const osColorScheme = useColorScheme(); + const effectiveScheme = resolveColorScheme(osColorScheme, themeConfig); + const statusTheme: Theme = useMemo(()=> getThemeForScheme(effectiveScheme, themeConfig).withParams({ headerFontSize: 14, rowVerticalPaddingScale: 0.8, - }), [colorScheme]); - const themeClass = colorScheme === 'light' ? 'ag-theme-alpine' : 'ag-theme-alpine-dark'; + }), [effectiveScheme, themeConfig]); + const themeClass = effectiveScheme === 'light' ? 'ag-theme-alpine' : 'ag-theme-alpine-dark'; return (
{ + const defaultSetColumnFunc = (newCol:[string, string]):void => { + console.log("defaultSetColumnFunc", newCol) + } + const sac:SetColumnFunc = defaultSetColumnFunc; + + return ( +
+ +
); +} + +const meta = { + title: "Buckaroo/Theme/ThemeCustomization", + component: DFViewerInfiniteWrap, + parameters: { + layout: "centered", + }, + tags: ["autodocs"], + argTypes: {}, +} satisfies Meta; + +export default meta; +type Story = StoryObj; + +const INDEX_COL_CONFIG: NormalColumnConfig = { + col_name: 'index', + header_name: 'index', + displayer_args: { displayer: 'string' }, +}; + +const left_col_configs = [INDEX_COL_CONFIG]; + +const sampleData = [ + { index: "0", a: 10, b: "foo", c: 100 }, + { index: "1", a: 20, b: "bar", c: 200 }, + { index: "2", a: 30, b: "baz", c: 300 }, + { index: "3", a: 40, b: "qux", c: 400 }, + { index: "4", a: 50, b: "quux", c: 500 }, +]; + +const rawData: DatasourceOrRaw = { + data_type: "Raw", + data: sampleData, + length: sampleData.length, +}; + +const baseColumnConfig: NormalColumnConfig[] = [ + { col_name: 'a', header_name: 'a', displayer_args: { displayer: 'integer', min_digits: 1, max_digits: 5 } }, + { col_name: 'b', header_name: 'b', displayer_args: { displayer: 'obj' } }, + { col_name: 'c', header_name: 'c', displayer_args: { displayer: 'integer', min_digits: 1, max_digits: 5 } }, +]; + +function makeConfig(theme?: ThemeConfig): DFViewerConfig { + return { + column_config: baseColumnConfig, + pinned_rows: [], + left_col_configs, + component_config: theme ? { theme } : undefined, + }; +} + +export const DefaultNoTheme: Story = { + args: { + data_wrapper: rawData, + df_viewer_config: makeConfig(), + }, +}; + +export const CustomAccent: Story = { + args: { + data_wrapper: rawData, + df_viewer_config: makeConfig({ + accentColor: '#ff6600', + }), + }, +}; + +export const ForcedDark: Story = { + args: { + data_wrapper: rawData, + df_viewer_config: makeConfig({ + colorScheme: 'dark', + backgroundColor: '#1a1a2e', + }), + }, +}; + +export const ForcedLight: Story = { + args: { + data_wrapper: rawData, + df_viewer_config: makeConfig({ + colorScheme: 'light', + backgroundColor: '#fafafa', + }), + }, +}; + +export const FullCustom: Story = { + args: { + data_wrapper: rawData, + df_viewer_config: makeConfig({ + colorScheme: 'dark', + accentColor: '#e91e63', + accentHoverColor: '#c2185b', + backgroundColor: '#1a1a2e', + foregroundColor: '#e0e0e0', + oddRowBackgroundColor: '#16213e', + borderColor: '#0f3460', + }), + }, +}; diff --git a/packages/buckaroo-js-core/src/style/dcf-npm.css b/packages/buckaroo-js-core/src/style/dcf-npm.css index 33aa824d7..7c5b89444 100644 --- a/packages/buckaroo-js-core/src/style/dcf-npm.css +++ b/packages/buckaroo-js-core/src/style/dcf-npm.css @@ -145,7 +145,7 @@ .operation-adder button { padding: 1px 2px; - background-color: #007bff; + background-color: var(--bk-accent-color, #007bff); color: white; border: none; border-radius: 0; @@ -155,7 +155,7 @@ } .operation-adder button:hover { - background-color: #0056b3; + background-color: var(--bk-accent-hover-color, #0056b3); } .operations-list { @@ -371,7 +371,7 @@ } div.dependent-tabs ul.tabs li.active { - background:rgba(33, 150, 243, 0.49); + background: var(--bk-accent-color, rgba(33, 150, 243, 0.49)); /* border:1px solid red; */ } @@ -431,11 +431,11 @@ div.dependent-tabs ul.tabs li.active { /* Ensure empty grid area below rows has proper background */ .theme-hanger { - background-color: #181D1F; + background-color: var(--bk-bg-color, #181D1F); } @media (prefers-color-scheme: light) { .theme-hanger { - background-color: #ffffff; + background-color: var(--bk-bg-color, #ffffff); } } diff --git a/tests/unit/test_theme_config.py b/tests/unit/test_theme_config.py new file mode 100644 index 000000000..8af46d7f6 --- /dev/null +++ b/tests/unit/test_theme_config.py @@ -0,0 +1,42 @@ +import pandas as pd +from buckaroo.buckaroo_widget import BuckarooWidget + + +simple_df = pd.DataFrame({'int_col': [1, 2, 3], 'str_col': ['a', 'b', 'c']}) + + +def test_theme_flows_through_widget(): + theme = {'colorScheme': 'dark', 'accentColor': '#ff6600'} + w = BuckarooWidget(simple_df, component_config={'theme': theme}) + cc = w.df_display_args['main']['df_viewer_config'].get('component_config', {}) + assert cc['theme'] == theme + + +def test_theme_absent_by_default(): + w = BuckarooWidget(simple_df) + cc = w.df_display_args['main']['df_viewer_config'].get('component_config', {}) + assert 'theme' not in cc or cc.get('theme') is None + + +def test_full_theme_config_roundtrips(): + theme = { + 'colorScheme': 'dark', + 'accentColor': '#e91e63', + 'accentHoverColor': '#c2185b', + 'backgroundColor': '#1a1a2e', + 'foregroundColor': '#e0e0e0', + 'oddRowBackgroundColor': '#16213e', + 'borderColor': '#0f3460', + } + w = BuckarooWidget(simple_df, component_config={'theme': theme}) + cc = w.df_display_args['main']['df_viewer_config'].get('component_config', {}) + assert cc['theme'] == theme + + +def test_theme_with_other_component_config(): + """Theme coexists with other component_config properties.""" + theme = {'accentColor': '#ff6600'} + w = BuckarooWidget(simple_df, component_config={'theme': theme, 'className': 'my-class'}) + cc = w.df_display_args['main']['df_viewer_config'].get('component_config', {}) + assert cc['theme'] == theme + assert cc['className'] == 'my-class'