From 8ba533df76753a2087e2e99e9f5c9a1afc8c7908 Mon Sep 17 00:00:00 2001 From: Gjore Milevski Date: Wed, 29 Jul 2026 08:20:56 +0200 Subject: [PATCH 01/11] feat(slider): consume --mw-* theme tokens and apply theme vars --- src/manywidgets/slider/src/index.ts | 8 ++++++-- src/manywidgets/slider/style.css | 26 +++++++++++++------------- 2 files changed, 19 insertions(+), 15 deletions(-) diff --git a/src/manywidgets/slider/src/index.ts b/src/manywidgets/slider/src/index.ts index c471246..2f86bfe 100644 --- a/src/manywidgets/slider/src/index.ts +++ b/src/manywidgets/slider/src/index.ts @@ -1,5 +1,5 @@ import type { RenderProps } from "@anywidget/types"; -import { onChanges, safeSaveChanges } from "@manywidgets/core"; +import { applyThemeVars, onChanges, safeSaveChanges } from "@manywidgets/core"; interface SliderModel { value: number; @@ -9,7 +9,9 @@ interface SliderModel { label: string; } -function render({ model, el }: RenderProps): void { +function render({ model, el }: RenderProps): () => void { + const disposeTheme = applyThemeVars(el, model); + const container = document.createElement("div"); container.className = "manywidgets-slider"; @@ -61,6 +63,8 @@ function render({ model, el }: RenderProps): void { model.on("change:label", () => { label.textContent = model.get("label"); }); + + return disposeTheme; } export default { render }; diff --git a/src/manywidgets/slider/style.css b/src/manywidgets/slider/style.css index b15e210..84630ef 100644 --- a/src/manywidgets/slider/style.css +++ b/src/manywidgets/slider/style.css @@ -1,12 +1,11 @@ .manywidgets-slider { - padding: 10px 14px; - border: 1px solid #e1e4e8; - border-radius: 8px; - background: #ffffff; - max-width: 320px; + padding: var(--mw-control-padding-y) var(--mw-control-padding-x); + border: 1px solid var(--mw-color-border); + border-radius: var(--mw-control-radius); + background: var(--mw-color-surface); + max-width: var(--mw-control-max-width); margin: 10px 0; - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, - Ubuntu, sans-serif; + font-family: var(--mw-font-family); box-sizing: border-box; } @@ -18,18 +17,19 @@ } .manywidgets-slider__label { - color: #24292e; - font-size: 14px; - font-weight: 600; + color: var(--mw-color-text); + font-size: var(--mw-font-size-md); + font-weight: var(--mw-font-weight-strong); } .manywidgets-slider__value { - color: #0366d6; - font-size: 14px; + color: var(--mw-color-accent); + font-size: var(--mw-font-size-md); font-variant-numeric: tabular-nums; } .manywidgets-slider__input { width: 100%; - accent-color: #0366d6; + accent-color: var(--mw-color-accent); + cursor: var(--mw-cursor-slider-thumb); } From cdb8642817758c0dfa1087b232f8ceb105bfce84 Mon Sep 17 00:00:00 2001 From: Gjore Milevski Date: Wed, 29 Jul 2026 08:20:56 +0200 Subject: [PATCH 02/11] feat(toggle): consume --mw-* theme tokens and apply theme vars --- src/manywidgets/toggle/src/index.ts | 8 ++++++-- src/manywidgets/toggle/style.css | 31 ++++++++++++++--------------- 2 files changed, 21 insertions(+), 18 deletions(-) diff --git a/src/manywidgets/toggle/src/index.ts b/src/manywidgets/toggle/src/index.ts index 5145107..be0a75f 100644 --- a/src/manywidgets/toggle/src/index.ts +++ b/src/manywidgets/toggle/src/index.ts @@ -1,12 +1,14 @@ import type { RenderProps } from "@anywidget/types"; -import { safeSaveChanges } from "@manywidgets/core"; +import { applyThemeVars, safeSaveChanges } from "@manywidgets/core"; interface ToggleModel { value: boolean; label: string; } -function render({ model, el }: RenderProps): void { +function render({ model, el }: RenderProps): () => void { + const disposeTheme = applyThemeVars(el, model); + const container = document.createElement("label"); container.className = "manywidgets-toggle"; @@ -38,6 +40,8 @@ function render({ model, el }: RenderProps): void { model.on("change:label", () => { label.textContent = model.get("label"); }); + + return disposeTheme; } export default { render }; diff --git a/src/manywidgets/toggle/style.css b/src/manywidgets/toggle/style.css index 06170a1..c992d2b 100644 --- a/src/manywidgets/toggle/style.css +++ b/src/manywidgets/toggle/style.css @@ -3,9 +3,8 @@ align-items: center; gap: 10px; margin: 10px 0; - cursor: pointer; - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, - Ubuntu, sans-serif; + cursor: var(--mw-cursor-button); + font-family: var(--mw-font-family); user-select: none; } @@ -18,10 +17,10 @@ .manywidgets-toggle__track { position: relative; - width: 40px; - height: 22px; - border-radius: 11px; - background: #d0d7de; + width: var(--mw-toggle-track-width); + height: var(--mw-toggle-track-height); + border-radius: var(--mw-radius-thumb); + background: var(--mw-color-border-strong); transition: background 0.2s ease; flex: none; } @@ -31,16 +30,16 @@ position: absolute; top: 2px; left: 2px; - width: 18px; - height: 18px; - border-radius: 50%; - background: #fff; + width: var(--mw-toggle-thumb-size); + height: var(--mw-toggle-thumb-size); + border-radius: var(--mw-radius-thumb); + background: var(--mw-color-surface); transition: transform 0.2s ease; - box-shadow: 0 1px 2px rgba(0, 0, 0, 0.25); + box-shadow: var(--mw-shadow-1); } .manywidgets-toggle__input:checked + .manywidgets-toggle__track { - background: #0366d6; + background: var(--mw-color-accent); } .manywidgets-toggle__input:checked + .manywidgets-toggle__track::after { @@ -48,7 +47,7 @@ } .manywidgets-toggle__label { - color: #24292e; - font-size: 14px; - font-weight: 600; + color: var(--mw-color-text); + font-size: var(--mw-font-size-md); + font-weight: var(--mw-font-weight-strong); } From b0c7a38ad614cd80e30a8014ac5f37a87331e6c7 Mon Sep 17 00:00:00 2001 From: Gjore Milevski Date: Wed, 29 Jul 2026 08:20:56 +0200 Subject: [PATCH 03/11] feat(range-slider): consume --mw-* theme tokens and apply theme vars --- src/manywidgets/range_slider/src/index.ts | 8 +++++-- src/manywidgets/range_slider/style.css | 26 +++++++++++------------ 2 files changed, 19 insertions(+), 15 deletions(-) diff --git a/src/manywidgets/range_slider/src/index.ts b/src/manywidgets/range_slider/src/index.ts index 8f98c3d..24a1727 100644 --- a/src/manywidgets/range_slider/src/index.ts +++ b/src/manywidgets/range_slider/src/index.ts @@ -1,5 +1,5 @@ import type { RenderProps } from "@anywidget/types"; -import { onChanges, safeSaveChanges } from "@manywidgets/core"; +import { applyThemeVars, onChanges, safeSaveChanges } from "@manywidgets/core"; interface RangeSliderModel { low: number; @@ -10,7 +10,9 @@ interface RangeSliderModel { label: string; } -function render({ model, el }: RenderProps): void { +function render({ model, el }: RenderProps): () => void { + const disposeTheme = applyThemeVars(el, model); + const container = document.createElement("div"); container.className = "manywidgets-range"; @@ -84,6 +86,8 @@ function render({ model, el }: RenderProps): void { model.on("change:label", () => { label.textContent = model.get("label"); }); + + return disposeTheme; } export default { render }; diff --git a/src/manywidgets/range_slider/style.css b/src/manywidgets/range_slider/style.css index de3f1e4..368142b 100644 --- a/src/manywidgets/range_slider/style.css +++ b/src/manywidgets/range_slider/style.css @@ -1,12 +1,11 @@ .manywidgets-range { - padding: 10px 14px; - border: 1px solid #e1e4e8; - border-radius: 8px; - background: #ffffff; - max-width: 320px; + padding: var(--mw-control-padding-y) var(--mw-control-padding-x); + border: 1px solid var(--mw-color-border); + border-radius: var(--mw-control-radius); + background: var(--mw-color-surface); + max-width: var(--mw-control-max-width); margin: 10px 0; - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, - Ubuntu, sans-serif; + font-family: var(--mw-font-family); box-sizing: border-box; } @@ -18,14 +17,14 @@ } .manywidgets-range__label { - color: #24292e; - font-size: 14px; - font-weight: 600; + color: var(--mw-color-text); + font-size: var(--mw-font-size-md); + font-weight: var(--mw-font-weight-strong); } .manywidgets-range__value { - color: #0366d6; - font-size: 14px; + color: var(--mw-color-accent); + font-size: var(--mw-font-size-md); font-variant-numeric: tabular-nums; } @@ -36,5 +35,6 @@ .manywidgets-range__input { width: 100%; - accent-color: #0366d6; + accent-color: var(--mw-color-accent); + cursor: var(--mw-cursor-slider-thumb); } From cf59747dbc3191b7bdcdcde84b6f25b6e25bbb29 Mon Sep 17 00:00:00 2001 From: Gjore Milevski Date: Wed, 29 Jul 2026 08:20:56 +0200 Subject: [PATCH 04/11] feat(button,dropdown,number-input): consume --mw-* theme tokens and apply theme vars --- src/manywidgets/button/src/index.ts | 8 ++++-- src/manywidgets/button/style.css | 17 ++++++------ src/manywidgets/dropdown/src/index.ts | 8 ++++-- src/manywidgets/dropdown/style.css | 32 +++++++++++------------ src/manywidgets/number_input/src/index.ts | 8 ++++-- src/manywidgets/number_input/style.css | 31 +++++++++++----------- 6 files changed, 58 insertions(+), 46 deletions(-) diff --git a/src/manywidgets/button/src/index.ts b/src/manywidgets/button/src/index.ts index 30d3e2e..e563014 100644 --- a/src/manywidgets/button/src/index.ts +++ b/src/manywidgets/button/src/index.ts @@ -1,12 +1,14 @@ import type { RenderProps } from "@anywidget/types"; -import { asNumber, safeSaveChanges } from "@manywidgets/core"; +import { applyThemeVars, asNumber, safeSaveChanges } from "@manywidgets/core"; interface ButtonModel { clicks: number; label: string; } -function render({ model, el }: RenderProps): void { +function render({ model, el }: RenderProps): () => void { + const disposeTheme = applyThemeVars(el, model); + const button = document.createElement("button"); button.className = "manywidgets-button"; button.type = "button"; @@ -22,6 +24,8 @@ function render({ model, el }: RenderProps): void { model.on("change:label", () => { button.textContent = model.get("label"); }); + + return disposeTheme; } export default { render }; diff --git a/src/manywidgets/button/style.css b/src/manywidgets/button/style.css index f5427b7..30aefdb 100644 --- a/src/manywidgets/button/style.css +++ b/src/manywidgets/button/style.css @@ -1,20 +1,19 @@ .manywidgets-button { padding: 8px 18px; - font-size: 14px; - font-weight: 600; + font-size: var(--mw-font-size-md); + font-weight: var(--mw-font-weight-strong); border: none; - border-radius: 6px; - cursor: pointer; - background: #0366d6; - color: #fff; + border-radius: var(--mw-input-radius); + cursor: var(--mw-cursor-button); + background: var(--mw-color-accent); + color: var(--mw-color-on-accent); margin: 10px 0; transition: background 0.15s ease, transform 0.1s ease; - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, - Ubuntu, sans-serif; + font-family: var(--mw-font-family); } .manywidgets-button:hover { - background: #0256c7; + background: var(--mw-color-accent-hover); } .manywidgets-button:active { diff --git a/src/manywidgets/dropdown/src/index.ts b/src/manywidgets/dropdown/src/index.ts index 838865e..f362f27 100644 --- a/src/manywidgets/dropdown/src/index.ts +++ b/src/manywidgets/dropdown/src/index.ts @@ -1,5 +1,5 @@ import type { RenderProps } from "@anywidget/types"; -import { safeSaveChanges } from "@manywidgets/core"; +import { applyThemeVars, safeSaveChanges } from "@manywidgets/core"; type Option = string | number | [string, unknown]; @@ -16,7 +16,9 @@ function optionValue(opt: Option): unknown { return Array.isArray(opt) ? opt[1] : opt; } -function render({ model, el }: RenderProps): void { +function render({ model, el }: RenderProps): () => void { + const disposeTheme = applyThemeVars(el, model); + const container = document.createElement("div"); container.className = "manywidgets-dropdown"; @@ -67,6 +69,8 @@ function render({ model, el }: RenderProps): void { model.on("change:label", () => { label.textContent = model.get("label"); }); + + return disposeTheme; } export default { render }; diff --git a/src/manywidgets/dropdown/style.css b/src/manywidgets/dropdown/style.css index 15de1af..9cc2f61 100644 --- a/src/manywidgets/dropdown/style.css +++ b/src/manywidgets/dropdown/style.css @@ -1,28 +1,28 @@ .manywidgets-dropdown { - padding: 10px 14px; - border: 1px solid #e1e4e8; - border-radius: 8px; - background: #ffffff; - max-width: 320px; + padding: var(--mw-control-padding-y) var(--mw-control-padding-x); + border: 1px solid var(--mw-color-border); + border-radius: var(--mw-control-radius); + background: var(--mw-color-surface); + max-width: var(--mw-control-max-width); margin: 10px 0; - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, - Ubuntu, sans-serif; + font-family: var(--mw-font-family); box-sizing: border-box; display: grid; - gap: 6px; + gap: var(--mw-control-gap); } .manywidgets-dropdown__label { - color: #24292e; - font-size: 14px; - font-weight: 600; + color: var(--mw-color-text); + font-size: var(--mw-font-size-md); + font-weight: var(--mw-font-weight-strong); } .manywidgets-dropdown__select { width: 100%; - padding: 6px 8px; - font-size: 14px; - border: 1px solid #d0d7de; - border-radius: 6px; - background: #fff; + padding: var(--mw-input-padding-y) var(--mw-input-padding-x); + font-size: var(--mw-font-size-md); + border: 1px solid var(--mw-color-border-strong); + border-radius: var(--mw-input-radius); + background: var(--mw-color-surface); + color: var(--mw-color-text); } diff --git a/src/manywidgets/number_input/src/index.ts b/src/manywidgets/number_input/src/index.ts index 2f5ccae..a83e0c7 100644 --- a/src/manywidgets/number_input/src/index.ts +++ b/src/manywidgets/number_input/src/index.ts @@ -1,5 +1,5 @@ import type { RenderProps } from "@anywidget/types"; -import { asNumber, onChanges, safeSaveChanges } from "@manywidgets/core"; +import { applyThemeVars, asNumber, onChanges, safeSaveChanges } from "@manywidgets/core"; interface NumberInputModel { value: number; @@ -9,7 +9,9 @@ interface NumberInputModel { label: string; } -function render({ model, el }: RenderProps): void { +function render({ model, el }: RenderProps): () => void { + const disposeTheme = applyThemeVars(el, model); + const container = document.createElement("div"); container.className = "manywidgets-number"; @@ -50,6 +52,8 @@ function render({ model, el }: RenderProps): void { model.on("change:label", () => { label.textContent = model.get("label"); }); + + return disposeTheme; } export default { render }; diff --git a/src/manywidgets/number_input/style.css b/src/manywidgets/number_input/style.css index 0ee4e3c..3d84fc8 100644 --- a/src/manywidgets/number_input/style.css +++ b/src/manywidgets/number_input/style.css @@ -1,28 +1,29 @@ .manywidgets-number { - padding: 10px 14px; - border: 1px solid #e1e4e8; - border-radius: 8px; - background: #ffffff; - max-width: 320px; + padding: var(--mw-control-padding-y) var(--mw-control-padding-x); + border: 1px solid var(--mw-color-border); + border-radius: var(--mw-control-radius); + background: var(--mw-color-surface); + max-width: var(--mw-control-max-width); margin: 10px 0; - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, - Ubuntu, sans-serif; + font-family: var(--mw-font-family); box-sizing: border-box; display: grid; - gap: 6px; + gap: var(--mw-control-gap); } .manywidgets-number__label { - color: #24292e; - font-size: 14px; - font-weight: 600; + color: var(--mw-color-text); + font-size: var(--mw-font-size-md); + font-weight: var(--mw-font-weight-strong); } .manywidgets-number__input { width: 100%; - padding: 6px 8px; - font-size: 14px; - border: 1px solid #d0d7de; - border-radius: 6px; + padding: var(--mw-input-padding-y) var(--mw-input-padding-x); + font-size: var(--mw-font-size-md); + border: 1px solid var(--mw-color-border-strong); + border-radius: var(--mw-input-radius); box-sizing: border-box; + background: var(--mw-color-surface); + color: var(--mw-color-text); } From 338c46f36dc715880d87dc52dfb7c6d26471b5b2 Mon Sep 17 00:00:00 2001 From: Gjore Milevski Date: Wed, 29 Jul 2026 08:20:56 +0200 Subject: [PATCH 05/11] feat(stat,number-display): consume --mw-* theme tokens and apply theme vars --- src/manywidgets/number_display/src/index.ts | 8 +++-- src/manywidgets/number_display/style.css | 13 ++++---- src/manywidgets/stat/src/index.ts | 8 +++-- src/manywidgets/stat/style.css | 35 ++++++++++----------- 4 files changed, 35 insertions(+), 29 deletions(-) diff --git a/src/manywidgets/number_display/src/index.ts b/src/manywidgets/number_display/src/index.ts index 6fdccba..36867b2 100644 --- a/src/manywidgets/number_display/src/index.ts +++ b/src/manywidgets/number_display/src/index.ts @@ -1,5 +1,5 @@ import type { RenderProps } from "@anywidget/types"; -import { asNumber } from "@manywidgets/core"; +import { applyThemeVars, asNumber } from "@manywidgets/core"; interface NumberDisplayModel { value: number; @@ -42,7 +42,9 @@ export function formatNumber(n: number, spec: string): string { const easeOut = (p: number) => 1 - Math.pow(1 - p, 3); -function render({ model, el }: RenderProps): void { +function render({ model, el }: RenderProps): () => void { + const disposeTheme = applyThemeVars(el, model); + const container = document.createElement("div"); container.className = "manywidgets-numberdisplay"; @@ -95,6 +97,8 @@ function render({ model, el }: RenderProps): void { model.on("change:label", () => { labelEl.textContent = model.get("label"); }); + + return disposeTheme; } export default { render }; diff --git a/src/manywidgets/number_display/style.css b/src/manywidgets/number_display/style.css index 793294a..9d820f2 100644 --- a/src/manywidgets/number_display/style.css +++ b/src/manywidgets/number_display/style.css @@ -2,22 +2,21 @@ display: inline-block; padding: 12px 18px; margin: 10px 0; - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, - Ubuntu, sans-serif; + font-family: var(--mw-font-family); } .manywidgets-numberdisplay__label { - color: #586069; - font-size: 12px; - font-weight: 600; + color: var(--mw-color-text-muted); + font-size: var(--mw-font-size-sm); + font-weight: var(--mw-font-weight-strong); text-transform: uppercase; letter-spacing: 0.04em; } .manywidgets-numberdisplay__value { - font-size: 40px; + font-size: var(--mw-number-display-value-size); font-weight: 700; - color: #0366d6; + color: var(--mw-color-accent); font-variant-numeric: tabular-nums; line-height: 1.1; } diff --git a/src/manywidgets/stat/src/index.ts b/src/manywidgets/stat/src/index.ts index af0665a..0f1ae77 100644 --- a/src/manywidgets/stat/src/index.ts +++ b/src/manywidgets/stat/src/index.ts @@ -1,5 +1,5 @@ import type { RenderProps } from "@anywidget/types"; -import { onChanges } from "@manywidgets/core"; +import { applyThemeVars, onChanges } from "@manywidgets/core"; interface StatModel { label: string; @@ -8,7 +8,9 @@ interface StatModel { delta: unknown; } -function render({ model, el }: RenderProps): void { +function render({ model, el }: RenderProps): () => void { + const disposeTheme = applyThemeVars(el, model); + const card = document.createElement("div"); card.className = "manywidgets-stat"; @@ -52,6 +54,8 @@ function render({ model, el }: RenderProps): void { update(); onChanges(model, ["label", "value", "unit", "delta"], update); + + return disposeTheme; } export default { render }; diff --git a/src/manywidgets/stat/style.css b/src/manywidgets/stat/style.css index b756691..c11de33 100644 --- a/src/manywidgets/stat/style.css +++ b/src/manywidgets/stat/style.css @@ -1,19 +1,18 @@ .manywidgets-stat { display: inline-block; - min-width: 140px; - padding: 14px 18px; - border: 1px solid #e1e4e8; - border-radius: 10px; - background: #ffffff; + min-width: var(--mw-stat-min-width); + padding: var(--mw-panel-padding-y) var(--mw-panel-padding-x); + border: 1px solid var(--mw-color-border); + border-radius: var(--mw-panel-radius); + background: var(--mw-color-surface); margin: 10px 0; - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, - Ubuntu, sans-serif; + font-family: var(--mw-font-family); } .manywidgets-stat__label { - color: #586069; - font-size: 12px; - font-weight: 600; + color: var(--mw-color-text-muted); + font-size: var(--mw-font-size-sm); + font-weight: var(--mw-font-weight-strong); text-transform: uppercase; letter-spacing: 0.04em; } @@ -26,30 +25,30 @@ } .manywidgets-stat__value { - font-size: 30px; + font-size: var(--mw-stat-value-size); font-weight: 700; - color: #24292e; + color: var(--mw-color-text); font-variant-numeric: tabular-nums; } .manywidgets-stat__unit { - font-size: 14px; - color: #586069; + font-size: var(--mw-font-size-md); + color: var(--mw-color-text-muted); } .manywidgets-stat__delta { margin-top: 4px; font-size: 13px; - font-weight: 600; + font-weight: var(--mw-font-weight-strong); font-variant-numeric: tabular-nums; } .manywidgets-stat__delta--up { - color: #1a7f37; + color: var(--mw-color-positive); } .manywidgets-stat__delta--down { - color: #cf222e; + color: var(--mw-color-negative); } .manywidgets-stat__delta--flat { - color: #586069; + color: var(--mw-color-text-muted); } From 3282697e9da8427ee2551f77deb1420fc998679b Mon Sep 17 00:00:00 2001 From: Gjore Milevski Date: Wed, 29 Jul 2026 08:20:56 +0200 Subject: [PATCH 06/11] feat(legend,text): consume --mw-* theme tokens and apply theme vars --- src/manywidgets/legend/src/index.ts | 8 ++++++-- src/manywidgets/legend/style.css | 21 ++++++++++----------- src/manywidgets/text/src/index.ts | 8 ++++++-- src/manywidgets/text/style.css | 12 ++++++------ 4 files changed, 28 insertions(+), 21 deletions(-) diff --git a/src/manywidgets/legend/src/index.ts b/src/manywidgets/legend/src/index.ts index 278c4cb..b711f3a 100644 --- a/src/manywidgets/legend/src/index.ts +++ b/src/manywidgets/legend/src/index.ts @@ -1,5 +1,5 @@ import type { RenderProps } from "@anywidget/types"; -import { onChanges } from "@manywidgets/core"; +import { applyThemeVars, onChanges } from "@manywidgets/core"; type Color = string | number[]; type Entry = [Color, string]; @@ -19,7 +19,9 @@ function toCss(color: Color): string { return "transparent"; } -function render({ model, el }: RenderProps): void { +function render({ model, el }: RenderProps): () => void { + const disposeTheme = applyThemeVars(el, model); + const container = document.createElement("div"); container.className = "manywidgets-legend"; @@ -57,6 +59,8 @@ function render({ model, el }: RenderProps): void { update(); onChanges(model, ["entries", "title"], update); + + return disposeTheme; } export default { render }; diff --git a/src/manywidgets/legend/style.css b/src/manywidgets/legend/style.css index 50503d5..1115577 100644 --- a/src/manywidgets/legend/style.css +++ b/src/manywidgets/legend/style.css @@ -1,18 +1,17 @@ .manywidgets-legend { display: inline-block; - padding: 10px 14px; - border: 1px solid #e1e4e8; - border-radius: 8px; - background: #ffffff; + padding: var(--mw-control-padding-y) var(--mw-control-padding-x); + border: 1px solid var(--mw-color-border); + border-radius: var(--mw-control-radius); + background: var(--mw-color-surface); margin: 10px 0; - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, - Ubuntu, sans-serif; + font-family: var(--mw-font-family); } .manywidgets-legend__title { - color: #586069; - font-size: 12px; - font-weight: 600; + color: var(--mw-color-text-muted); + font-size: var(--mw-font-size-sm); + font-weight: var(--mw-font-weight-strong); text-transform: uppercase; letter-spacing: 0.04em; margin-bottom: 6px; @@ -41,6 +40,6 @@ } .manywidgets-legend__label { - font-size: 14px; - color: #24292e; + font-size: var(--mw-font-size-md); + color: var(--mw-color-text); } diff --git a/src/manywidgets/text/src/index.ts b/src/manywidgets/text/src/index.ts index 3f1042d..29b3da5 100644 --- a/src/manywidgets/text/src/index.ts +++ b/src/manywidgets/text/src/index.ts @@ -1,5 +1,5 @@ import type { RenderProps } from "@anywidget/types"; -import { onChanges } from "@manywidgets/core"; +import { applyThemeVars, onChanges } from "@manywidgets/core"; import { marked } from "marked"; interface TextModel { @@ -7,7 +7,9 @@ interface TextModel { markdown: boolean; } -function render({ model, el }: RenderProps): void { +function render({ model, el }: RenderProps): () => void { + const disposeTheme = applyThemeVars(el, model); + const container = document.createElement("div"); container.className = "manywidgets-text"; el.appendChild(container); @@ -26,6 +28,8 @@ function render({ model, el }: RenderProps): void { update(); onChanges(model, ["value", "markdown"], update); + + return disposeTheme; } export default { render }; diff --git a/src/manywidgets/text/style.css b/src/manywidgets/text/style.css index ea2e150..4671345 100644 --- a/src/manywidgets/text/style.css +++ b/src/manywidgets/text/style.css @@ -1,9 +1,8 @@ .manywidgets-text { margin: 10px 0; - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, - Ubuntu, sans-serif; - font-size: 14px; - color: #24292e; + font-family: var(--mw-font-family); + font-size: var(--mw-font-size-md); + color: var(--mw-color-text); line-height: 1.5; } @@ -16,8 +15,9 @@ } .manywidgets-text--markdown code { - background: #f6f8fa; + background: var(--mw-color-code-bg); padding: 0.1em 0.3em; - border-radius: 4px; + border-radius: var(--mw-radius-2); font-size: 0.9em; + font-family: var(--mw-font-family-mono); } From fdf1ec68198337f8070e8b33458c835a00b0c6f0 Mon Sep 17 00:00:00 2001 From: Gjore Milevski Date: Wed, 29 Jul 2026 08:20:56 +0200 Subject: [PATCH 07/11] feat(chart): consume --mw-* theme tokens and apply theme vars --- src/manywidgets/chart/src/index.ts | 9 +++++++-- src/manywidgets/chart/style.css | 3 +-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/manywidgets/chart/src/index.ts b/src/manywidgets/chart/src/index.ts index d0cd3c2..e89cc7b 100644 --- a/src/manywidgets/chart/src/index.ts +++ b/src/manywidgets/chart/src/index.ts @@ -1,5 +1,5 @@ import type { RenderProps } from "@anywidget/types"; -import { onChanges, safeSaveChanges } from "@manywidgets/core"; +import { applyThemeVars, onChanges, safeSaveChanges } from "@manywidgets/core"; import Chart from "chart.js/auto"; interface Series { @@ -83,6 +83,8 @@ function formatSeriesData(seriesData: Series[], defaultType: string) { } function render({ model, el }: RenderProps): () => void { + const disposeTheme = applyThemeVars(el, model); + const container = document.createElement("div"); container.className = "manywidgets-chart"; container.style.width = `${model.get("width")}px`; @@ -195,7 +197,10 @@ function render({ model, el }: RenderProps): () => void { chart?.resize(); }); - return () => chart?.destroy(); + return () => { + chart?.destroy(); + disposeTheme(); + }; } export default { render }; diff --git a/src/manywidgets/chart/style.css b/src/manywidgets/chart/style.css index fad228a..584c181 100644 --- a/src/manywidgets/chart/style.css +++ b/src/manywidgets/chart/style.css @@ -1,7 +1,6 @@ .manywidgets-chart { margin: 10px 0; - font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, - Ubuntu, sans-serif; + font-family: var(--mw-font-family); max-width: 100%; } From 1c16f971bae5f345949280bd4b1742ab656f2f4a Mon Sep 17 00:00:00 2001 From: Gjore Milevski Date: Mon, 3 Aug 2026 14:03:24 +0200 Subject: [PATCH 08/11] feat: add core primitives for resolving --mw-* tokens for non-DOM renderers --- packages/core/src/index.ts | 103 +++++++++++++++++++++++++++++ packages/core/tests/core.test.ts | 108 +++++++++++++++++++++++++++++++ 2 files changed, 211 insertions(+) diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index c47d2b7..cef6942 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -608,3 +608,106 @@ export function ensureShadowCss(el: HTMLElement, cssText: string, key: string): style.textContent = cssText; container.appendChild(style); } + +// Non-DOM renderer theme resolution. A canvas/WebGL widget never reads CSS, +// so it resolves the --mw-* values it needs into plain JS. Core only provides +// the primitives below; see chart/src/theme.ts for how one widget uses them. + +/** + * Resolve a `--mw-*` custom property to its used value. + * + * A direct `getComputedStyle(el).getPropertyValue(varName)` leaves nested + * `var()` references unsubstituted, so tokens like `--mw-color-text` that + * fall back through `--myst-*`/`--jp-*` chains come back unresolved. + * Assigning the same expression to a real CSS property forces the browser to + * resolve it. See + * https://css-tricks.com/making-sense-of-custom-properties-runtime-values/. + * + * 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 { + const attr = `data-mw-probe-${cssProperty}`; + const existing = el.querySelector(`:scope > [${attr}]`); + if (existing) return existing; + const probe = document.createElement("span"); + probe.style.cssText = "display:none;"; + probe.setAttribute(attr, ""); + el.appendChild(probe); + return probe; +} + +function resolveCssVar(el: HTMLElement, cssProperty: string, varExpression: string, fallback: string): string { + const probe = getProbe(el, cssProperty); + probe.style.setProperty(cssProperty, `var(${varExpression}, ${fallback})`); + const value = getComputedStyle(probe).getPropertyValue(cssProperty).trim(); + // Environments with no CSS custom-property support in getComputedStyle + // (notably jsdom) hand the var() expression back verbatim. + return value && !value.includes("var(") ? value : fallback; +} + +/** Resolve a `--mw-*` token as a used color (e.g. `--mw-color-text`). */ +export function resolveThemeColor(el: HTMLElement, varName: string, fallback: string): string { + return resolveCssVar(el, "color", varName, fallback); +} + +/** Resolve a `--mw-*` token as a used font family. */ +export function resolveThemeFontFamily(el: HTMLElement, varName: string, fallback: string): string { + return resolveCssVar(el, "font-family", varName, fallback); +} + +/** Resolve a `--mw-*` token as a used font weight (e.g. `600`, `"bold"`). */ +export function resolveThemeFontWeight(el: HTMLElement, varName: string, fallback: string): string { + return resolveCssVar(el, "font-weight", varName, fallback); +} + +/** Resolve a `--mw-*` font-size token to its used pixel value. */ +export function resolveThemeFontSize(el: HTMLElement, varName: string, fallback: number): number { + const n = Number.parseFloat(resolveCssVar(el, "font-size", varName, `${fallback}px`)); + return Number.isFinite(n) ? n : fallback; +} + +export interface ResolveThemePaletteOptions { + /** Used for every entry the cascade doesn't override. Required, core has no default palette. */ + fallback: string[]; + /** Custom property naming the palette length. Defaults to `--mw-palette-size`. */ + sizeVar?: string; + /** Maps a 1-based index to its custom property name. Defaults to `--mw-palette-{i}`. */ + colorVar?: (index: number) => string; +} + +/** + * Resolve an indexed categorical palette (by default `--mw-palette-1`, + * `--mw-palette-2`, and so on, sized by `--mw-palette-size`) into a plain + * array. Reads the custom properties directly, unlike {@link + * resolveThemeColor}, since `Theme.to_vars()` only ever writes literal + * colors here. + */ +export function resolveThemePalette(el: HTMLElement, options: ResolveThemePaletteOptions): string[] { + const { fallback, sizeVar = "--mw-palette-size", colorVar = (i: number) => `--mw-palette-${i}` } = options; + const computed = getComputedStyle(el); + const size = Number.parseInt(computed.getPropertyValue(sizeVar).trim(), 10); + const count = Number.isFinite(size) && size > 0 ? size : fallback.length; + const palette: string[] = []; + for (let i = 1; i <= count; i++) { + const literal = computed.getPropertyValue(colorVar(i)).trim(); + palette.push(literal || fallback[(i - 1) % fallback.length]); + } + return palette; +} + +type ThemeFieldResolver = (el: HTMLElement) => T; + +/** Turn a `{ field: (el) => value }` map into a single `(el) => theme` function. */ +export function defineThemeReader( + resolvers: { [K in keyof T]: ThemeFieldResolver }, +): (el: HTMLElement) => T { + const entries = Object.entries(resolvers) as [keyof T, ThemeFieldResolver][]; + return (el: HTMLElement): T => { + const theme = {} as T; + for (const [field, resolve] of entries) { + theme[field] = resolve(el); + } + return theme; + }; +} diff --git a/packages/core/tests/core.test.ts b/packages/core/tests/core.test.ts index a99444f..57c0234 100644 --- a/packages/core/tests/core.test.ts +++ b/packages/core/tests/core.test.ts @@ -2,11 +2,15 @@ import { afterEach, describe, expect, it, vi } from "vitest"; import { applyThemeVars, asNumber, + defineThemeReader, deliverCustomMessage, detectHostColorMode, onChanges, renderChild, resolveModel, + resolveThemeColor, + resolveThemeFontSize, + resolveThemePalette, safeSaveChanges, setByPath, } from "@manywidgets/core"; @@ -295,3 +299,107 @@ describe("applyThemeVars", () => { expect(el.dataset.mwColorMode).toBe("light"); }); }); + +describe("resolveThemePalette", () => { + it("reads an explicit palette and size from --mw-palette-* custom properties", () => { + const el = mountEl(); + const m = fakeModel({ + theme_vars: { + "--mw-palette-size": "3", + "--mw-palette-1": "#111111", + "--mw-palette-2": "#222222", + "--mw-palette-3": "#333333", + "--mw-palette-4": "#444444", + }, + }); + applyThemeVars(el, m as never); + + expect(resolveThemePalette(el, { fallback: ["#000000"] })).toEqual([ + "#111111", "#222222", "#333333", + ]); + }); + + it("falls back to the caller-supplied palette when no tokens are set", () => { + const el = mountEl(); + const m = fakeModel({ theme_vars: {} }); + applyThemeVars(el, m as never); + + expect(resolveThemePalette(el, { fallback: ["#aaaaaa", "#bbbbbb"] })).toEqual([ + "#aaaaaa", "#bbbbbb", + ]); + }); + + it("supports a custom var namespace", () => { + const el = mountEl(); + const m = fakeModel({ + theme_vars: { "--mw-map-size": "2", "--mw-map-1": "#123123", "--mw-map-2": "#456456" }, + }); + applyThemeVars(el, m as never); + + const palette = resolveThemePalette(el, { + fallback: ["#000000"], + sizeVar: "--mw-map-size", + colorVar: (i) => `--mw-map-${i}`, + }); + expect(palette).toEqual(["#123123", "#456456"]); + }); +}); + +describe("resolveThemeColor / resolveThemeFontSize", () => { + it("return the caller-supplied fallback", () => { + const el = mountEl(); + const m = fakeModel({ theme_vars: {} }); + applyThemeVars(el, m as never); + + expect(resolveThemeColor(el, "--mw-color-text", "#123456")).toBe("#123456"); + expect(resolveThemeFontSize(el, "--mw-font-size-md", 14)).toBe(14); + }); + + it("reuses one hidden probe per property instead of inserting/removing on every call", () => { + const el = mountEl(); + const m = fakeModel({ theme_vars: {} }); + applyThemeVars(el, m as never); + + resolveThemeColor(el, "--mw-color-text", "#111111"); + resolveThemeColor(el, "--mw-color-text-muted", "#222222"); + resolveThemeColor(el, "--mw-color-border", "#333333"); + resolveThemeFontSize(el, "--mw-font-size-md", 14); + resolveThemeFontSize(el, "--mw-font-size-sm", 12); + + expect(el.querySelectorAll("[data-mw-probe-color]").length).toBe(1); + expect(el.querySelectorAll("[data-mw-probe-font-size]").length).toBe(1); + }); +}); + +describe("defineThemeReader", () => { + it("runs each field's resolver and assembles the result", () => { + const el = mountEl(); + const m = fakeModel({ theme_vars: {} }); + applyThemeVars(el, m as never); + + interface FakeTheme { + textColor: string; + count: number; + } + const readFakeTheme = defineThemeReader({ + textColor: (target) => resolveThemeColor(target, "--mw-color-text", "#abcdef"), + count: () => 42, + }); + + expect(readFakeTheme(el)).toEqual({ textColor: "#abcdef", count: 42 }); + }); + + it("re-runs resolvers on every call, not just once", () => { + const el = mountEl(); + let calls = 0; + const readFakeTheme = defineThemeReader<{ value: number }>({ + value: () => { + calls += 1; + return calls; + }, + }); + + expect(readFakeTheme(el)).toEqual({ value: 1 }); + expect(readFakeTheme(el)).toEqual({ value: 2 }); + }); +}); From dff9f2cd48774ae112b7bad3079a0e269f8fdf4c Mon Sep 17 00:00:00 2001 From: Gjore Milevski Date: Mon, 3 Aug 2026 14:03:31 +0200 Subject: [PATCH 09/11] feat: add chart.js theme adapter --- src/manywidgets/chart/src/theme.ts | 35 +++++++++++++++++++++++ src/manywidgets/chart/tests/theme.test.ts | 23 +++++++++++++++ 2 files changed, 58 insertions(+) create mode 100644 src/manywidgets/chart/src/theme.ts create mode 100644 src/manywidgets/chart/tests/theme.test.ts diff --git a/src/manywidgets/chart/src/theme.ts b/src/manywidgets/chart/src/theme.ts new file mode 100644 index 0000000..fe9db26 --- /dev/null +++ b/src/manywidgets/chart/src/theme.ts @@ -0,0 +1,35 @@ +import { + defineThemeReader, + resolveThemeColor, + resolveThemeFontFamily, + resolveThemeFontSize, + resolveThemeFontWeight, + resolveThemePalette, +} from "@manywidgets/core"; + +const DEFAULT_PALETTE = [ + "#3e63dd", "#e5484d", "#30a46c", "#f76b15", + "#8e4ec6", "#0090ff", "#ffc53d", "#d6409f", +]; + +export interface ChartTheme { + palette: string[]; + textColor: string; + mutedColor: string; + gridColor: string; + fontFamily: string; + fontSize: number; + fontSizeSmall: number; + fontWeightStrong: string; +} + +export const readChartTheme = defineThemeReader({ + palette: (el) => resolveThemePalette(el, { fallback: DEFAULT_PALETTE }), + textColor: (el) => resolveThemeColor(el, "--mw-color-text", "#24292e"), + mutedColor: (el) => resolveThemeColor(el, "--mw-color-text-muted", "#586069"), + gridColor: (el) => resolveThemeColor(el, "--mw-color-border", "#e1e4e8"), + fontFamily: (el) => resolveThemeFontFamily(el, "--mw-font-family", "sans-serif"), + fontSize: (el) => resolveThemeFontSize(el, "--mw-font-size-md", 14), + fontSizeSmall: (el) => resolveThemeFontSize(el, "--mw-font-size-sm", 12), + fontWeightStrong: (el) => resolveThemeFontWeight(el, "--mw-font-weight-strong", "600"), +}); diff --git a/src/manywidgets/chart/tests/theme.test.ts b/src/manywidgets/chart/tests/theme.test.ts new file mode 100644 index 0000000..d4aef10 --- /dev/null +++ b/src/manywidgets/chart/tests/theme.test.ts @@ -0,0 +1,23 @@ +import { describe, expect, it } from "vitest"; +import { applyThemeVars } from "@manywidgets/core"; +import { fakeModel, mountEl } from "@manywidgets/test-utils"; +import { readChartTheme } from "../src/theme"; + +describe("readChartTheme", () => { + it("composes core's resolution primitives into the chart theme shape", () => { + const el = mountEl(); + const m = fakeModel({ + theme_vars: { + "--mw-palette-size": "2", + "--mw-palette-1": "#123456", + "--mw-palette-2": "#abcdef", + }, + }); + applyThemeVars(el, m as never); + + const theme = readChartTheme(el); + expect(theme.palette).toEqual(["#123456", "#abcdef"]); + expect(typeof theme.textColor).toBe("string"); + expect(typeof theme.fontFamily).toBe("string"); + }); +}); From eaf9d80cb3a76a8cd723416923f1de977e06ad67 Mon Sep 17 00:00:00 2001 From: Gjore Milevski Date: Mon, 3 Aug 2026 14:03:37 +0200 Subject: [PATCH 10/11] feat: theme chart.js canvas colors, fonts and palette --- src/manywidgets/chart/src/index.ts | 72 +++++++++++++++++------ src/manywidgets/chart/tests/chart.test.ts | 49 +++++++++++++++ 2 files changed, 102 insertions(+), 19 deletions(-) diff --git a/src/manywidgets/chart/src/index.ts b/src/manywidgets/chart/src/index.ts index e89cc7b..11332ac 100644 --- a/src/manywidgets/chart/src/index.ts +++ b/src/manywidgets/chart/src/index.ts @@ -1,6 +1,7 @@ import type { RenderProps } from "@anywidget/types"; -import { applyThemeVars, onChanges, safeSaveChanges } from "@manywidgets/core"; +import { applyThemeVars, observeHostColorMode, onChange, onChanges, safeSaveChanges } from "@manywidgets/core"; import Chart from "chart.js/auto"; +import { readChartTheme, type ChartTheme } from "./theme"; interface Series { type?: string; @@ -37,14 +38,9 @@ interface ChartModel { hover_point: PointEvent | Record; } -const PALETTE = [ - "#1f77b4", "#ff7f0e", "#2ca02c", "#d62728", "#9467bd", - "#8c564b", "#e377c2", "#7f7f7f", "#bcbd22", "#17becf", -]; - -function formatSeriesData(seriesData: Series[], defaultType: string) { +function formatSeriesData(seriesData: Series[], defaultType: string, palette: string[]) { return (seriesData || []).map((series, index) => { - const color = series.color || PALETTE[index % PALETTE.length]; + const color = series.color || palette[index % palette.length]; const type = series.type || defaultType || "line"; let data: unknown; @@ -113,15 +109,26 @@ function render({ model, el }: RenderProps): () => void { safeSaveChanges(model); }; - function buildOptions(): Record { + function buildOptions(theme: ChartTheme): Record { const options: Record = { responsive: true, maintainAspectRatio: false, animation: { duration: model.get("animation_enabled") ? 750 : 0 }, interaction: { mode: "nearest", intersect: false, axis: "x" }, + color: theme.textColor, + font: { family: theme.fontFamily, size: theme.fontSize }, plugins: { - title: { display: !!model.get("title"), text: model.get("title"), font: { size: 16 } }, - legend: { display: model.get("legend_enabled") !== false, position: "top" }, + title: { + display: !!model.get("title"), + text: model.get("title"), + color: theme.textColor, + font: { family: theme.fontFamily, size: theme.fontSize + 2, weight: theme.fontWeightStrong }, + }, + legend: { + display: model.get("legend_enabled") !== false, + position: "top", + labels: { color: theme.textColor, font: { family: theme.fontFamily, size: theme.fontSizeSmall } }, + }, tooltip: { enabled: model.get("tooltips_enabled") !== false, mode: "index", @@ -132,9 +139,25 @@ function render({ model, el }: RenderProps): () => void { x: { type: "linear", position: "bottom", - title: { display: !!model.get("x_label"), text: model.get("x_label") }, + title: { + display: !!model.get("x_label"), + text: model.get("x_label"), + color: theme.mutedColor, + font: { family: theme.fontFamily }, + }, + ticks: { color: theme.mutedColor, font: { family: theme.fontFamily, size: theme.fontSizeSmall } }, + grid: { color: theme.gridColor }, + }, + y: { + title: { + display: !!model.get("y_label"), + text: model.get("y_label"), + color: theme.mutedColor, + font: { family: theme.fontFamily }, + }, + ticks: { color: theme.mutedColor, font: { family: theme.fontFamily, size: theme.fontSizeSmall } }, + grid: { color: theme.gridColor }, }, - y: { title: { display: !!model.get("y_label"), text: model.get("y_label") } }, }, onClick: emitPoint("clicked_point"), onHover: emitPoint("hover_point"), @@ -151,21 +174,27 @@ function render({ model, el }: RenderProps): () => void { function build(): void { chart?.destroy(); + const theme = readChartTheme(el); chart = new Chart(canvas, { type: (model.get("chart_type") || "line") as never, - data: { datasets: formatSeriesData(model.get("series_data"), model.get("chart_type")) as never }, - options: buildOptions() as never, + data: { + datasets: formatSeriesData(model.get("series_data"), model.get("chart_type"), theme.palette) as never, + }, + options: buildOptions(theme) as never, }); } function createOrUpdate(): void { if (!chart) { build(); - } else { - chart.data = { datasets: formatSeriesData(model.get("series_data"), model.get("chart_type")) as never }; - chart.options = buildOptions() as never; - chart.update(); + return; } + const theme = readChartTheme(el); + chart.data = { + datasets: formatSeriesData(model.get("series_data"), model.get("chart_type"), theme.palette) as never, + }; + chart.options = buildOptions(theme) as never; + chart.update(); } if ((model.get("series_data") || []).length > 0) build(); @@ -197,8 +226,13 @@ function render({ model, el }: RenderProps): () => void { chart?.resize(); }); + const offThemeChange = onChange(model, "theme_vars", createOrUpdate); + const offHostColorMode = observeHostColorMode(el, createOrUpdate); + return () => { chart?.destroy(); + offThemeChange(); + offHostColorMode(); disposeTheme(); }; } diff --git a/src/manywidgets/chart/tests/chart.test.ts b/src/manywidgets/chart/tests/chart.test.ts index 0f3eb6f..2320b56 100644 --- a/src/manywidgets/chart/tests/chart.test.ts +++ b/src/manywidgets/chart/tests/chart.test.ts @@ -77,4 +77,53 @@ describe("Chart", () => { expect(instances.length).toBe(before); expect(model.get("title")).toBe("Hello"); }); + + it("colors datasets from the theme palette, not a hardcoded array", () => { + const el = mountEl(); + const model = fakeModel( + baseState({ + theme_vars: { + "--mw-palette-size": "2", + "--mw-palette-1": "#123456", + "--mw-palette-2": "#abcdef", + }, + series_data: [ + { name: "a", data: [[0, 1]] }, + { name: "b", data: [[0, 2]] }, + ], + }), + ); + widget.render({ model, el } as never); + + const chart = instances[instances.length - 1] as { data: { datasets: Array<{ borderColor: string }> } }; + expect(chart.data.datasets[0].borderColor).toBe("#123456"); + expect(chart.data.datasets[1].borderColor).toBe("#abcdef"); + }); + + it("an explicit per-series color still overrides the theme palette", () => { + const el = mountEl(); + const model = fakeModel( + baseState({ + theme_vars: { "--mw-palette-size": "1", "--mw-palette-1": "#123456" }, + series_data: [{ name: "a", color: "#ff00ff", data: [[0, 1]] }], + }), + ); + widget.render({ model, el } as never); + + const chart = instances[instances.length - 1] as { data: { datasets: Array<{ borderColor: string }> } }; + expect(chart.data.datasets[0].borderColor).toBe("#ff00ff"); + }); + + it("rebuilds chart options (not just redraw) when theme_vars changes", () => { + const el = mountEl(); + const model = fakeModel( + baseState({ theme_vars: { "--mw-color-text": "#111111" } }), + ); + widget.render({ model, el } as never); + + model.set("theme_vars", { "--mw-color-text": "#222222" }); + + const chart = instances[instances.length - 1] as { options: { color: string } }; + expect(chart.options).toHaveProperty("color"); + }); }); From 6ec862fd0da6939304285709a8578cf8c0a637fa Mon Sep 17 00:00:00 2001 From: Gjore Milevski Date: Mon, 3 Aug 2026 14:03:42 +0200 Subject: [PATCH 11/11] feat: expose theme palette as --mw-palette-* tokens --- src/manywidgets/themes/_theme.py | 22 ++++++++++++++++------ src/manywidgets/themes/radix.py | 4 ++-- tests/test_theming.py | 23 +++++++++++++++++++++++ 3 files changed, 41 insertions(+), 8 deletions(-) diff --git a/src/manywidgets/themes/_theme.py b/src/manywidgets/themes/_theme.py index 9cb7672..602eb9d 100644 --- a/src/manywidgets/themes/_theme.py +++ b/src/manywidgets/themes/_theme.py @@ -11,7 +11,7 @@ from dataclasses import dataclass, field, fields, replace from typing import Mapping -_NON_VAR_FIELDS = {"chart_palette", "tokens"} +_NON_VAR_FIELDS = {"palette", "tokens"} _MW_PREFIX = "--mw-" @@ -163,8 +163,7 @@ class Theme: panel_radius: str | None = None stat_value_size: str | None = None - # Non-DOM renderers need explicit values instead of CSS variables. - chart_palette: list[str] | None = None + palette: list[str] | None = None # Escape hatch for any other manywidgets-owned CSS custom property. tokens: Mapping[str, object] = field(default_factory=dict) @@ -173,7 +172,14 @@ def __post_init__(self) -> None: object.__setattr__(self, "tokens", _normalize_vars(self.tokens)) def to_vars(self) -> dict[str, str]: - """Serialize set fields to a flat ``{"--mw-*": value}`` dict.""" + """Serialize set fields to a flat ``{"--mw-*": value}`` dict. + + ``palette`` is a list, not a single CSS value, so it expands into + indexed ``--mw-palette-N`` tokens (plus a ``--mw-palette-size`` count). + A non-DOM renderer resolves those back via its own small adapter over + ``@manywidgets/core``'s ``resolveThemePalette``. See + ``src/manywidgets/chart/src/theme.ts`` for the Chart.js one. + """ out: dict[str, str] = dict(self.tokens) for f in fields(self): if f.name in _NON_VAR_FIELDS: @@ -181,6 +187,10 @@ def to_vars(self) -> dict[str, str]: value = getattr(self, f.name) if value is not None: out[_MW_PREFIX + f.name.replace("_", "-")] = str(value) + if self.palette: + out[_MW_PREFIX + "palette-size"] = str(len(self.palette)) + for i, color in enumerate(self.palette, start=1): + out[f"{_MW_PREFIX}palette-{i}"] = str(color) return out def extend(self, **overrides: object) -> "Theme": @@ -215,7 +225,7 @@ def merge(*themes: "Theme | None") -> "Theme": if f.name not in _NON_VAR_FIELDS and getattr(theme, f.name) is not None } overrides["tokens"] = {**result.tokens, **theme.tokens} - if theme.chart_palette is not None: - overrides["chart_palette"] = list(theme.chart_palette) + if theme.palette is not None: + overrides["palette"] = list(theme.palette) result = replace(result, **overrides) return result diff --git a/src/manywidgets/themes/radix.py b/src/manywidgets/themes/radix.py index 2fa98c0..a5a2e95 100644 --- a/src/manywidgets/themes/radix.py +++ b/src/manywidgets/themes/radix.py @@ -425,7 +425,7 @@ def radix_theme( tokens.update(RADIX_RADIUS_PRESETS[radius]) tokens["--mw-scaling"] = RADIX_SCALING_PRESETS[scaling] - return Theme(tokens=tokens, chart_palette=_chart_palette(appearance)) + return Theme(tokens=tokens, palette=_palette(appearance)) def light_theme( @@ -494,7 +494,7 @@ def _focus_tokens() -> dict[str, str]: return tokens -def _chart_palette(appearance: str) -> list[str]: +def _palette(appearance: str) -> list[str]: return list(_LIGHT_CHART_PALETTE if appearance == "light" else _DARK_CHART_PALETTE) diff --git a/tests/test_theming.py b/tests/test_theming.py index 95b8224..174214c 100644 --- a/tests/test_theming.py +++ b/tests/test_theming.py @@ -22,6 +22,29 @@ def test_theme_rejects_non_mw_tokens(): Theme.from_vars({"--radix-shadow-3": "0 3px 12px #0002"}) +def test_palette_serializes_to_indexed_vars(): + vars_ = Theme(palette=["#111111", "#222222"]).to_vars() + + assert vars_["--mw-palette-size"] == "2" + assert vars_["--mw-palette-1"] == "#111111" + assert vars_["--mw-palette-2"] == "#222222" + assert "--mw-palette-3" not in vars_ + + +def test_plain_theme_has_no_palette_vars(): + assert Theme().to_vars() == {} + + +def test_radix_theme_includes_appearance_matched_palette(): + light_vars = light.to_vars() + dark_vars = dark.to_vars() + + assert light_vars["--mw-palette-size"] == "10" + assert light_vars["--mw-palette-1"] == "#3e63dd" + assert dark_vars["--mw-palette-1"] == "#9eb1ff" + assert light_vars["--mw-palette-1"] != dark_vars["--mw-palette-1"] + + def test_extend_overrides_and_merges_tokens(): base = Theme(color_accent="#000", tokens={"--mw-radius-4": "8px"}) out = base.extend(color_accent="#fff", tokens={"--mw-shadow-2": "none"})