feat: widget css tokens - #13
Conversation
| * One hidden probe per `(el, cssProperty)` is created once and reused, | ||
| * instead of inserted and removed on every call. | ||
| */ | ||
| function getProbe(el: HTMLElement, cssProperty: string): HTMLElement { |
There was a problem hiding this comment.
@batpad this is the slightly annoying/ugly part, eg Chart.js needs a real hex code and not a CSS var name. Problem is our tokens fall back through a whole chain (--mw-color-text to --myst-* to --jp-* to hex) which is good, but then if you just do getComputedStyle(el).getPropertyValue("--mw-color-text") the browser will give you back the unresolved var() expression, not the actual color. So the workaround is to add a hidden / throwaway <span>, assign the var() to a real CSS property on it (like color) and then read THAT back for the browser to resolve it.
| return palette; | ||
| } | ||
|
|
||
| type ThemeFieldResolver<T> = (el: HTMLElement) => T; |
There was a problem hiding this comment.
This is the only framework-ish bit, it just turns a { field: resolver } object into one (el) => theme function. So if you're adding theming to a new canvas/webgl widget, chart/src/theme.ts below is the file to copy and swap the fields for whatever your lib needs.
We need to document this better. But I think it'd be cool to extend the agent skill, so that if a new dev wants to generate a new widget, it will ask them a bunch of clarifying questions (eg. name of the widget, is it canvas/webgl based, etc) and based on that copy a template that can be based on chart/theme.ts. cc @batpad @wrynearson
| fontWeightStrong: string; | ||
| } | ||
|
|
||
| export const readChartTheme = defineThemeReader<ChartTheme>({ |
There was a problem hiding this comment.
This is the whole adapter file and here's the copy for Chart.js specifically, that just says "these are the fields Chart.js options need and here's how to get each one from our --mw-* tokens." The core doesn't know Chart.js exists and all the lib-specific knowledge lives right here.
|
|
||
| function build(): void { | ||
| chart?.destroy(); | ||
| const theme = readChartTheme(el); |
There was a problem hiding this comment.
And this is where we call readChartTheme on first render and again whenever theme_vars changes (eg light/dark toggle) the chart repaints with the new colors.
|
@dzole0311 at a quick glance, this looks really good to me - can spend a bit more time with it tomorrow. |
Contributes to: #7
Changes:
Two parts, but happy to split part two into its own PR if that's easier to review.
Part 1: widgets use --mw-* tokens
Replaces hardcoded colors, fonts and sizes in each widget's CSS with the shared --mw-* tokens, so widgets pick up a theme instead of using fixed values. Touches button, toggle, slider, range slider, dropdown, number input, stat, number display, legend, text and chart.
Part 2: Theming for canvas-based widgets (eg. Chart.js)
Widgets like Chart.js draw on canvas so CSS tokens don't reach it on their own. This adds a small pattern for that.
Why a plain CSS read isn't enough? A canvas lib needs a plain value in JS (like "#0366d6"), not a CSS variable name. Normally you'd read the CSS variable with getComputedStyle, but that doesn't work here since our tokens often fall to other tokens (our color falls back to a JupyterLab color, which falls back to a plain hex code) and the browser won't resolve that chain when you read a custom prop directly.
The core doesn't know anything about Chart.js or any other lib. It only has the small helper functions (get a color, get a font size, get a palette). Each widget decides what fields it needs and writes its own file. Nothing to extend, no base class, just copy chart's file and change the names.
Screen.Recording.2026-08-03.at.17.21.27.mov
Proposed next steps: