-
Notifications
You must be signed in to change notification settings - Fork 15
feat: add custom theme support via component_config #583
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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, | ||
| }); | ||
| }); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -30,8 +30,9 @@ import { | |
| HeightStyleI, | ||
| SetColumnFunc | ||
| } from "./gridUtils"; | ||
| import { getThemeForScheme } from './gridUtils'; | ||
| import { getThemeForScheme, resolveColorScheme } from './gridUtils'; | ||
| import { useColorScheme } from '../useColorScheme'; | ||
| import type { ThemeConfig } from './gridUtils'; | ||
|
|
||
| ModuleRegistry.registerModules([ClientSideRowModelModule]); | ||
| ModuleRegistry.registerModules([InfiniteRowModelModule]); | ||
|
|
@@ -156,13 +157,24 @@ export function DFViewerInfinite({ | |
| )}, [hsCacheKey] | ||
| ); | ||
| const defaultActiveCol:[string, string] = ["", ""]; | ||
| const colorScheme = useColorScheme(); | ||
| const defaultThemeClass = colorScheme === 'light' ? 'ag-theme-alpine' : 'ag-theme-alpine-dark'; | ||
| const divClass = df_viewer_config?.component_config?.className || defaultThemeClass; | ||
| const osColorScheme = useColorScheme(); | ||
| const themeConfig = compConfig?.theme; | ||
| const effectiveScheme = resolveColorScheme(osColorScheme, themeConfig); | ||
| const defaultThemeClass = effectiveScheme === 'light' ? 'ag-theme-alpine' : 'ag-theme-alpine-dark'; | ||
| const divClass = `${defaultThemeClass} ${compConfig?.className || ''}`.trim(); | ||
|
|
||
| const themeStyle: React.CSSProperties = { | ||
| ...hs.applicableStyle, | ||
| ...(themeConfig?.accentColor ? { '--bk-accent-color': themeConfig.accentColor } as any : {}), | ||
| ...(themeConfig?.accentHoverColor ? { '--bk-accent-hover-color': themeConfig.accentHoverColor } as any : {}), | ||
| ...(themeConfig?.backgroundColor ? { '--bk-bg-color': themeConfig.backgroundColor } as any : {}), | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
This style injection only sets Useful? React with 👍 / 👎. |
||
| ...(themeConfig?.foregroundColor ? { '--bk-fg-color': themeConfig.foregroundColor } as any : {}), | ||
| }; | ||
|
|
||
| return ( | ||
| <div className={`df-viewer ${hs.classMode} ${hs.inIframe}`}> | ||
| {error_info ? <pre>{error_info}</pre> : null} | ||
| <div style={hs.applicableStyle} | ||
| <div style={themeStyle} | ||
| className={`theme-hanger ${divClass}`}> | ||
| <DFViewerInfiniteInner | ||
| data_wrapper={data_wrapper} | ||
|
|
@@ -173,6 +185,8 @@ export function DFViewerInfinite({ | |
| outside_df_params={outside_df_params} | ||
| renderStartTime={renderStartTime} | ||
| hs={hs} | ||
| themeConfig={themeConfig} | ||
| effectiveScheme={effectiveScheme} | ||
| /> | ||
| </div> | ||
| </div>) | ||
|
|
@@ -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), | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LoadHandler.postonly overlayscomponent_configontosession.df_display_argshere, but buckaroo interactions later rebuilddf_display_argsfromsession.dataflowinDataStreamHandler._handle_buckaroo_state_change(websocket_handler.py), which never receives this config. In servermode="buckaroo", theme/custom component settings from/loadare therefore lost after the first state-changing action (for example changing cleaning/post-processing), so the new feature does not persist through normal usage.Useful? React with 👍 / 👎.