feat: Theme Editor, viewer light/dark mode, and chart theming#786
Open
mlennie wants to merge 31 commits into
Open
feat: Theme Editor, viewer light/dark mode, and chart theming#786mlennie wants to merge 31 commits into
mlennie wants to merge 31 commits into
Conversation
Operators can pick brand colors at /settings/theme without editing publisher.config.json by hand. Saves go to a singleton row in a new DuckDB themes table and surface on /status so every viewer of the instance picks them up on next page load. publisher.config.json's top-level theme block is kept as a boot seed only; the editor wins once it writes. frozenConfig:true deployments render the page read-only and reject writes with 403. Wiring: - ServerProvider now fetches /status via react-query and wraps the app in the SDK's ThemeProvider, so usePublisherTheme() returns the live instanceTheme. RenderedResult applies the theme via vegaConfigOverride (chart series) and CSS variables (tables, dashboard background). - buildTableCssVars sets both --malloy-render--* and --malloy-theme--* on the wrapper. The renderer re-emits --malloy-render--* on a deeper element using var(--malloy-theme--*) fallbacks, so the shadow namespace is required for table chrome to pick up the override. - initializeSchema always runs its CREATE TABLE IF NOT EXISTS pass so existing DuckDB files pick up the new themes table without --init. - ThemeEditorPage debounces auto-save and gates the resync effect on a baseline ref so in-flight typing isn't reverted when the prior save returns. Out of scope for v1 (documented inline): per-environment overrides, per-chart Malloy annotations, light/dark toggle UI, and the renderer's upcoming theme prop. Body text / borders / dashboard tile colors still come from the renderer's hardcoded defaults until that prop lands; the editor's Tables card calls this out. Signed-off-by: Monty Lennie <montylennie@gmail.com>
…heme prop
Schema bumps:
- palette.series, palette.tableBody, palette.tile, palette.tileTitle each
become {light, dark}; palette.background and palette.tableHeader were
already per-mode.
- font.family and font.size become {light, dark}.
Editor:
- Light/Dark toggle picks which variant the section pickers write.
- New TablesSection knobs for tableBody / tile / tileTitle.
SDK:
- New buildMalloyExplicitTheme maps ResolvedTheme onto the upstream
renderer's explicit `theme` prop so table chrome and dashboard tiles
pick up operator colors directly, bypassing the renderer's shadow var
cascade.
- buildTableCssVars emits the --malloy-theme--* shadow namespace in
addition to --malloy-render--* for keys the renderer re-emits.
This commit is a checkpoint before the follow-up simplification that
will collapse per-mode for series + font (kept shared) and drop the
shadow CSS namespace now that the renderer's theme prop handles tables.
Signed-off-by: Monty Lennie <montylennie@gmail.com>
…espace Simplifies the render UI theme system based on three observations surfaced during the in-app editor work: 1. Series palette and font are brand identity, not mode identity. They stay consistent when a viewer toggles light/dark. Reverting them to scalars (palette.series: string[], font.family: string, font.size: number) deletes ~80 lines of per-mode plumbing and removes the editor footgun where Light's 8 picks didn't follow into Dark. Per-mode stays on the five colour keys that genuinely change with mode (background, tableHeader, tableBody, tile, tileTitle). 2. The renderer's explicit `theme` prop handles all table tokens, so the --malloy-theme--* shadow namespace we emitted alongside --malloy-render--* was redundant. buildTableCssVars now writes a single namespace covering only the keys not in MalloyExplicitTheme (label-color, value-color) plus a few that downstream override CSS rules consume directly. 3. Border, pinned border, value colour, foreground text, and axisFaint were each recomputed from `theme.mode` inside three different builders with duplicated hex literals. Hoisted onto ResolvedTheme so resolveTheme computes them once; buildTableCssVars, buildMalloyExplicitTheme, and buildVegaThemeOverride read them instead of branching. Other clean-ups: - Central PER_MODE_COLOR_KEYS list in packages/sdk/src/theme/keys.ts; config.ts mirrors it (deps point server → SDK via the generated API types, not the other way). Adding a new per-mode colour is a one-line change in each list. - mergeThemes now walks PER_MODE_COLOR_KEYS. Previously it only merged background and tableHeader, so an environment override of tableBody / tile / tileTitle silently dropped the instance default for that key. Added a regression test. Manual verification still pending; existing saved themes that use the old per-mode series/font shape will be coerced through sanitizeTheme (per-mode object dropped, scalar accepted, otherwise the default applies). Operators can hit Reset to Defaults if they want a clean slate. Signed-off-by: Monty Lennie <montylennie@gmail.com>
… editor
Operators upgrading past the per-mode series + font revert hit
`TypeError: a.map is not a function` opening /settings/theme. The DB
row still held `palette.series = {light: [...], dark: [...]}` from the
previous schema, the controller returned it verbatim, and
SeriesColorsSection blew up on `.map`.
Two-layer fix:
1. ThemeStore.loadLocked now pipes the parsed JSON through
`sanitizeTheme` before caching. Fields that don't match the
current shape are dropped silently; fields that do (the 5 per-mode
colour keys) carry over. This also collapses the
`initialize` / `initializeLocked` duplication I'd flagged in the
prior review.
2. SeriesColorsSection guards with `Array.isArray(theme.palette?.series)`
and TypographySection guards with `typeof === "string"` / "number".
Belt + suspenders: if a future schema bump produces an unexpected
shape that the sanitiser misses, the picker still mounts.
No new restart-and-Reset dance for operators with old saved themes;
the migration runs on first read.
Signed-off-by: Monty Lennie <montylennie@gmail.com>
Two follow-ups from the PR review: 1. Add `palette.background` to the editor. Previously the operator could only color the dashboard tile wrapper; the chart canvas was always white because Vega's background config came from palette.background and that key wasn't surfaced anywhere in the Theme Editor UI. The new picker lives in the renamed "Charts" section (formerly "Chart series colors") and is per-mode like the rest of the colour knobs. Wiring through to the renderer is unchanged — buildVegaThemeOverride already reads theme.background. 2. ThemeStore.loadLocked now logs which fields the sanitiser dropped when reading the stored payload. Operators upgrading past a schema change can see in the server log exactly which of their old customisations no longer apply (e.g. `palette.series` after the per-mode revert). Added a regression test that injects an old-shape payload directly into the DB and asserts only current-shape fields survive on read. Signed-off-by: Monty Lennie <montylennie@gmail.com>
Ports the Publisher app shell mode toggle from monty/color-choosing
(commit fcae0a6). The renderer-side parts of that WIP were skipped —
the in-app Theme Editor on this branch already handles chart theming
correctly via the upstream renderer's explicit `theme` prop, so the
toggle's only new responsibility is the MUI palette switch.
Mode state lives in one place:
ServerProvider (SDK)
- reads userChoice from localStorage["publisher:themeMode"]
- subscribes to matchMedia("(prefers-color-scheme: dark)")
- resolveMode(instanceTheme.defaultMode, userChoice, prefersDark)
- passes the result into the SDK ThemeProvider that wraps children
Both the new app-level PublisherMuiThemeProvider AND the SDK's renderer
context read mode from that single ThemeProvider via usePublisherTheme().
The MUI palette switches via createPublisherTheme(mode); the renderer's
explicit theme prop already responds (wired earlier on this branch).
Operator-side controls honoured:
- allowUserToggle=false hides the toggle and ignores any stale
localStorage entry; viewers are locked to defaultMode.
- defaultMode in publisher.config.json picks the boot mode.
Cosmetic SDK component changes (AnalyzePackageButton, Environment cards,
Home env cards, Package row hover, NotebookCell markdown, Workbook /
MutableCell backgrounds, styles.ts styled components) cherry-picked from
the same WIP commit so the app shell renders correctly in dark mode —
all replace hardcoded color literals with theme.palette tokens.
Playwright spec adapted: ported the toggle-cycle and persistence tests;
dropped the renderer CSS-var test that referenced --malloy-render--table-
background, a var we deliberately stopped emitting in the renderer-UI
refactor.
Manual sweep recommended after restart: home → environment → package →
chart, verify everything flips together; check allowUserToggle: false
hides the toggle.
Signed-off-by: Monty Lennie <montylennie@gmail.com>
Two small dark-mode polish fixes from a manual sweep of the ecommerce package: 1. The dashboard panel (the area between dashboard tiles) was painting white in dark mode because we deliberately omitted `background` from the renderer's `theme` prop. The original concern was that an operator picking a bold accent for `palette.background` would have it bleed across the surrounding panel. Resolved by adding a new mode-keyed `dashboardRoot` field to `ResolvedTheme` (NOT operator-customisable) and passing it as the renderer's `background`. Light keeps white (no regression); dark paints slate so the panel sits with the page chrome instead of as a bright box. Added spec coverage that confirms an operator's `palette.background` override does not bleed into `dashboardRoot`. 2. The Search and Code icons inside the white-circle action buttons on each NotebookCell were nearly invisible in dark mode. The button background was hardcoded `rgba(255, 255, 255, 0.9)` but the icon colour was `text.secondary`, which resolves to a light slate in dark mode — light glyph on light background. Switched the three icon usages to `grey.700` so they stay dark in both modes (matching the button background that's hardcoded light). Comment captures the invariant. Signed-off-by: Monty Lennie <montylennie@gmail.com>
The previous fix (a3b8eb9 → ad070bc) set `MalloyExplicitTheme.background` to the new mode-keyed `dashboardRoot`, which the renderer writes through to `--malloy-render--background`. That paints the outer `.malloy-render` container. But the inner `.malloy-dashboard` element has dashboard.css's own hardcoded `background: #f7f9fc`, so the dashboard panel kept reading white in dark mode. Same for the sticky `.dashboard-row-header` that the dashboard.css also hardcodes. Added two rules to injectRendererOverrides that paint both surfaces from `--malloy-render--background` so they follow the theme's dashboardRoot. Operator's `palette.background` (the chart canvas via Vega) still flows separately and doesn't touch the panel. Signed-off-by: Monty Lennie <montylennie@gmail.com>
Cells with a dashboard result had their top-right action buttons (the "<>" and search icons) sitting at `top: 8px` of CleanMetricCard, which placed their bottom ~4px inside the dashboard panel area. In light mode the panel was white and the overlap was invisible; once the panel started painting slate in dark mode (ca09b4a), the visual clip became obvious. Moved the Stack to `top: -12px` so the buttons sit fully above the panel's top edge in both modes. Comment captures why. Signed-off-by: Monty Lennie <montylennie@gmail.com>
The Settings entry controls colours and fonts for the Malloy renderer's output (charts, tables, dashboards). With the new app-shell light/dark toggle landed, "Theme" alone was ambiguous: viewers couldn't tell whether it controlled the chart palette or the MUI palette. Renamed to "Visualization theme" so the boundary is obvious from the sidebar. URL stays at /settings/theme so bookmarks keep working. JSDoc on the page component now distinguishes the two theming surfaces explicitly. Signed-off-by: Monty Lennie <montylennie@gmail.com>
Tables on the package page rendered with the renderer's hardcoded white interior in dark mode. Header (#cbd5e1) and body (#e2e8f0) text flipped to light slate per the operator's tableHeader / tableBody choices, but the table BG didn't follow — light text on a white panel read as nearly invisible. Same pattern as the earlier `dashboardRoot` fix: added a mode-keyed `tableBackground` derived field on ResolvedTheme (NOT operator-customizable), and passed it through MalloyExplicitTheme. Light keeps white (no regression). Dark uses slate so table interior matches the surrounding chrome. `palette.background` (the chart canvas, which the operator CAN customize) stays decoupled — a bold accent there still doesn't bleed into table interiors. Regression spec confirms. Signed-off-by: Monty Lennie <montylennie@gmail.com>
The syntax-highlighted Malloy / TypeScript code blocks (package readme, notebook code cells, workbook source, model annotations) rendered with github-light shiki regardless of the viewer's mode toggle. In dark mode the white code island stuck out against the dark page chrome. Loaded github-dark alongside github-light in the shared highlighter factory (both lazy-loaded at first call, no eager cost). Added a `mode: "light" | "dark"` parameter to highlight() that selects which github theme shiki applies. All four call sites (NotebookCell.malloy, NotebookCell.embed, MutableCell.malloy, ModelCell.annotations) now pull mode from usePublisherTheme() and re-highlight when the viewer toggles. Signed-off-by: Monty Lennie <montylennie@gmail.com>
The chip hover used `grey.100` which in our greyScale is the off-white #F4F3F1. In dark mode that's effectively the same colour as the chip text, so hovering made the label vanish. Switched to a mode-aware hover background — translucent white in dark, the off-white in light. Signed-off-by: Monty Lennie <montylennie@gmail.com>
…override
Two changes from a manual sweep of the ecommerce package in light
mode with a custom palette.background (green):
1. Tables were painting white inside green-canvas dashboards because
tableBackground was hardcoded mode-keyed and decoupled from
palette.background. The operator's mental model is "Chart
background covers the chart canvas AND the tables inside tiles",
not "Chart background is one knob and table interior is another."
Coupled tableBackground to palette.background so charts and tables
share a single viz surface colour. dashboardRoot stays decoupled
(mode-keyed neutral) since it's the chrome between tiles, not a
viz surface.
2. The renderer's dashboard.css hardcodes `.malloy-dashboard {
background: #f7f9fc }` AND a matching rule on
`.dashboard-row-header`. Our existing override used
`var(--malloy-render--background)` but with single-selector
specificity. Duplicated the override at higher specificity (three
variants: bare, self-doubled, and tag-qualified) and added an
explicit `background-color` alongside `background` so the
shorthand can't be lost to source order. The comment captures
that this only beats the renderer's own scoped rules; if the
panel still shows the accent colour after restart, that's a
browser cache (hard refresh).
Signed-off-by: Monty Lennie <montylennie@gmail.com>
Operator's mental model is "the padding around the table" vs "the band
at the top of the table". The renderer historically reuses one value
(tablePinnedBackground) for both surfaces, so picking the tile colour
also tinted the preview's header row — and the theme editor preview
showed a band that the actual package page didn't render the same way.
Schema: new `palette.tableHeaderBackground: {light, dark}`. Added to
PER_MODE_COLOR_KEYS (SDK + server mirror), DEFAULT_THEME (#f5fafc
light, #1e293b dark), ResolvedTheme, resolveTheme.
Renderer wiring:
- buildMalloyExplicitTheme.tablePinnedBackground now carries
theme.tableHeaderBackground (not theme.tile).
- New `--malloy-render--tile-background` var in buildTableCssVars
carries theme.tile.
- injectRendererOverrides: .dashboard-item now reads tile-background;
added .malloy-table .th.column-cell rule so non-pinned (i.e. most)
tables actually show a header band reflecting the operator's choice.
Editor:
- TablesSection: new "Header background" picker. Renamed "Header
color" to "Header text color" for clarity. Tile picker re-labelled
"Tile background (around the table)".
- TablePreview takes the new prop and now wraps the inner table in a
tile-coloured padded box so preview = actual page layout.
OpenAPI types regenerated.
Signed-off-by: Monty Lennie <montylennie@gmail.com>
The MUI outlined-TextField floating label sits inside the input's border and is clipped to the input width. With the field width fixed at 110px (so the hex code aligns predictably), labels like "Header background" and "Tile background (around the table)" rendered as "Header back..." / "Tile backgro...". Lifted the label out of the TextField and into its own Typography above the swatch+input row. The label can now be any width; the input keeps the 110px hex layout. Accessibility-wise the label still ties to the input via aria-label on the input element. Signed-off-by: Monty Lennie <montylennie@gmail.com>
The Charts section's sample bar chart was the only viz visible while the operator picked colours, but line charts are at least as common in real dashboards (the by_month chart on the ecommerce page is one). Added a small static SVG LineChartPreview that sits next to the bar preview and paints from the same ResolvedTheme: background for the canvas, series[0] for the line stroke, axisFaint for a baseline. The two previews wrap so the layout stays readable on narrow widths. Signed-off-by: Monty Lennie <montylennie@gmail.com>
The single-column picker stacks left ~70% of the editor card empty on a typical desktop. Switched the Series colours block and the Tables block to a CSS grid (auto-fill / minmax) so pickers pack multiple per row on wide viewports and gracefully fall back to a single column on narrow ones. Minimum cell widths picked to fit the longest labels without truncation (240px for series, 260px for the table block which carries "Tile background (around the table)"). Signed-off-by: Monty Lennie <montylennie@gmail.com>
The "Discard changes" and "Reset to defaults" buttons are one-click destructive: discard throws away unsaved edits, reset wipes the on-disk theme for every viewer. Either one fired by mistake costs real time on a brand. Wrapped both in MUI Confirmation Dialogs. Discard dialog explains that unsaved edits go away but the saved theme is preserved. Reset dialog spells out that reset clears every customized colour/font and applies to every viewer immediately. Both have a Cancel button that defaults to the safe path; the destructive button is autoFocus'd in error colour so an Enter-press after reading still requires explicit confirmation but isn't a hidden trap. Signed-off-by: Monty Lennie <montylennie@gmail.com>
Default MUI Button text color tracks primary.main, which in our dark palette is #334155 (slate-700). On the confirm dialog's slate background that produced near-invisible "Cancel" text. Pointed both Cancel buttons at text.primary so they paint against the dialog in whatever the active mode resolves to (#30302E light, #f1f5f9 dark). Signed-off-by: Monty Lennie <montylennie@gmail.com>
…ads; drop Discard button
Addresses the three highest-severity findings from the PR review.
1. Auto-save / Reset race. The debounce timer could fire and put the
PUT in flight before the operator clicked Reset; the in-flight
save's onSuccess then stomped resetMutation's setQueryData. Added
a `saveGenRef` counter that's bumped on Reset (onMutate) and
captured by each auto-save closure — a save whose generation went
stale silently drops its onSuccess instead of writing through.
Reset's onMutate also clears any pending debounce. Auto-save and
first-sync guards (`hasSyncedOnceRef`) keep the effect from firing
against the pre-query empty state.
2. Defaults snapshotting in series. SeriesColorsSection materialised
the SDK default palette on first edit ("Add color" implicitly
froze 8 defaults + the new color into palette.series). setSeries
now compares against DEFAULT_THEME.palette.series and, if the
operator's array equals SDK defaults exactly, drops the explicit
field so the cascade keeps resolving dynamically. Future Publisher
releases that change brand defaults reach this user as long as
they don't manually customise.
3. Legacy non-object spreads. TablesSection.setColor,
SeriesColorsSection.setBackground, and TypographySection's font
setters all spread `theme.palette?.[key]` (or `theme.font`)
without guarding against legacy non-object shapes. A pre-per-mode
string in those slots would expand into character-indexed garbage
on edit. All three now check `typeof === "object" && !Array`
before spreading and fall back to `{}` otherwise.
Also removed the Discard changes button + confirm dialog. The
auto-save model means the button was permanently disabled (draft
converges to saved within 600ms), making it visual noise. Reset to
defaults handles the "start over" case.
Signed-off-by: Monty Lennie <montylennie@gmail.com>
… var The Business Overview dashboard kept painting the panel between tiles with the operator's palette.background (chart canvas colour) instead of the mode-keyed dashboardRoot neutral. Our override on .malloy-dashboard read var(--malloy-render--background), which we expected to resolve to the theme prop's `background` field (= theme.dashboardRoot, hardcoded white in light). On dashboards that carry a `# theme.background` annotation, that var gets shadowed deeper in the renderer DOM by an inline-style write sourced from the annotation, defeating our intent. Switched the override to a dedicated --publisher-dashboard-root var that's only set on the outer Publisher wrapper. The renderer doesn't know this var exists so it can't be shadowed by any annotation-driven write. Also expanded the override to cover .dashboard-row and .dashboard-row-body alongside .malloy-dashboard and .dashboard-row-header so no internal surface can leak the annotation colour. Signed-off-by: Monty Lennie <montylennie@gmail.com>
…hange)
Adds the operator-facing knob that drives the new
MalloyExplicitTheme.mapColor field in @malloydata/render. Schema
addition is a per-mode hex (`palette.mapColor.{light,dark}`) so the
operator can pick a different brand colour for light vs dark.
End-to-end wiring:
- api-doc.yaml: new `palette.mapColor` field, regen api.ts + client.
- PER_MODE_COLOR_KEYS (SDK keys.ts + server config.ts mirror) extended.
- DEFAULT_THEME: light + dark default to MALLOY_BRAND.teal so unbranded
installs get a brand-coherent ramp instead of the renderer's blue.
- ResolvedTheme + resolveTheme: pick("mapColor") joins the cascade.
- buildMalloyExplicitTheme: passes mapColor through to the renderer.
- SeriesColorsSection: new picker "Map color (gradient saturated end)"
alongside Chart background in a 2-col grid; new MapPreview shows the
5-stop gradient so the operator sees the ramp inline.
Per-chart # theme.palette.mapColor.{light,dark} annotations on a Malloy
model still win over the operator's choice for that chart, same as
every other per-mode colour key.
Signed-off-by: Monty Lennie <montylennie@gmail.com>
Two changes in response to UX feedback: 1. Maps get their own section (was previously a picker in Charts). The operator's mental model is "one knob for maps", not "another thing in the Charts grid"; choropleths consume a sequential scale that doesn't compose with the categorical series palette next to it. MapsSection sits between Charts and Tables in the page. 2. MapPreview is now an actual stylised US-state choropleth instead of a colour bar. 17 hand-traced path tiles arranged loosely as the lower-48 + AK/HI, each tinted by an arbitrary intensity along the same low-to-high ramp the renderer applies internally. The colour bar version misread as "what does my palette look like", not "what does a map look like". Also stripped the now-redundant map picker + preview from SeriesColorsSection and rolled the per-mode color helper back to a single-purpose setBackground (had been generalised earlier when the map picker lived there). If the actual sales_by_state map on a package page still paints with the renderer's hardcoded blue ramp, that's a stale `@malloydata/render` dist — rebuild the renderer (`cd ../malloy/packages/malloy-render && bun run build`) and restart Publisher. Verified: the rebuilt dist contains `MAP_GRADIENT_LOW` and the `explicitTheme?.mapColor` branch. Signed-off-by: Monty Lennie <montylennie@gmail.com>
The previous preview rendered as a rectangular tile grid because the hand-traced "state" paths were axis-aligned rectangles, which read as abstract colour blocks rather than a map. Swapped the tiles for a single simplified lower-48 silhouette path with state-blob circles overlaid at varying intensities along the gradient. Still not geographically accurate (would require ~50 paths + a topology dataset the editor shouldn't pull in), but clearly recognisable as a US map. Signed-off-by: Monty Lennie <montylennie@gmail.com>
Replaced the hand-traced silhouette + circles (which read as "weirdly shaped colour blocks") with the actual us-atlas/states-10m TopoJSON the renderer's shape-map plugin uses, so the editor preview matches the shape an operator will see on a real choropleth page. Each state's intensity comes from a stable FIPS-id hash so re-renders don't reshuffle the gradient assignment. Projection is geoAlbersUsa (same as Vega's default for `sales_by_state`-style charts) fit to a 380×220 viewport. us-atlas, topojson-client, and d3-geo are already in node_modules transitively via @malloydata/render. Added a small local .d.ts stub for the two that ship without bundled types rather than pulling in @types/d3-geo and @types/topojson-client as direct deps for one preview surface. Signed-off-by: Monty Lennie <montylennie@gmail.com>
Signed-off-by: Monty Lennie <montylennie@gmail.com>
bd1a625 to
685dabf
Compare
…rer publishes
The Publisher SDK consumes a `theme` prop on `MalloyRenderer` and a
`MalloyExplicitTheme` type that ship in `monty/explicit-theme-prop`
on `malloydata/malloy` and have not yet been published to npm. Local
dev works via `bun link` against a checkout that already has these
exports. CI installs `@malloydata/render` from npm at v0.0.394+ which
doesn't have them, so `tsc --noEmit` fails on the SDK with:
src/theme/buildMalloyExplicitTheme.ts(1,15): error TS2305:
Module '"@malloydata/render"' has no exported member
'MalloyExplicitTheme'.
src/components/RenderedResult/RenderedResult.tsx(62,7): error TS2353:
Object literal may only specify known properties, and 'theme'
does not exist in type 'MalloyRendererOptions'.
This change adds `packages/sdk/src/types/malloy-render-augmentation.d.ts`
which type-augments `@malloydata/render` to add `MalloyExplicitTheme`
and the `theme` field on `MalloyRendererOptions`. At runtime the
augmentation is type-only; an older published renderer silently ignores
the unknown `theme` option, so chart-canvas / mapColor theming is a
no-op until the renderer publishes, but table chrome continues to
theme via the existing CSS-var path. The Theme Editor UI itself is
fully functional regardless of which renderer version is installed.
REMOVE this file once the renderer PR (monty/explicit-theme-prop)
merges and Malloy CI publishes the next @malloydata/render. Bump the
dependency in `packages/sdk/package.json` to that version in the same
follow-up commit.
Signed-off-by: Monty Lennie <montylennie@gmail.com>
| description: | | ||
| Returns the theme used by the Malloy renderer for charts/tables/dashboards. | ||
| Reflects edits made via the in-app Theme Editor (persisted to the server's | ||
| SQLite database). Empty object when no theme has been configured. |
Collaborator
Author
There was a problem hiding this comment.
@sagarswamirao good catch. I updated to say DuckDB
ThemeStore is backed by the existing StorageManager (DuckDB), not SQLite. The api-doc.yaml description and the theme_store.ts class docstring both said "SQLite" as a leftover from an early plan that predated reusing the shared storage layer. Fixes Sagar's review comment on PR #786. Signed-off-by: Monty Lennie <montylennie@gmail.com>
Collaborator
|
Can you add a couple of screenshots? |
Collaborator
Author
@sagarswamirao thank you for the reminder. Just added screenshots to PR description. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Adds an end-to-end theming system for Publisher with three independent surfaces.
Walkthrough video: https://www.loom.com/share/e31e852ca1664526a1e7a355a07bddd6
Themeschema inpublisher.config.json(instance and per-environment), backed by a new SQLiteThemeStoreand exposed viaGET / PUT / DELETE /api/v0/theme./settings/themewith section-by-section color pickers (background, tables, dashboard tiles, charts, maps, fonts), a Light/Dark variant toggle, and a live preview that renders theresolved cascade.
defaultModeandallowUserTogglefrom the config, persists the viewer's choice tolocalStorage, and followsprefers-color-schemeinauto.Cascade: SDK defaults → instance config → environment config → editor (per environment) → per-chart
# theme.*annotation. Each layer only overrides the keys it sets.Per-mode palette keys:
background,tableHeader,tableHeaderBackground,tableBody,tile,tileTitle,mapColor.seriesandfontare scalars shared across modes (brand identity).Paired renderer change
Depends on
themebeing a real prop onMalloyRenderer. That change lives in malloydata/malloy onmonty/explicit-theme-prop. Without it, chart-canvas andchart-internal colors won't follow the resolved theme. The SDK currently consumes the renderer via
bun linkagainst a local malloy checkout; switch to the npm-published version once the renderer PR merges.Key paths
SDK
packages/sdk/src/theme/*—defaults,keys,types,resolveTheme,buildTableCssVars,buildVegaThemeOverride,buildMalloyExplicitTheme,readChartAnnotations,ThemeContextpackages/sdk/src/components/ServerProvider.tsx— owns mode state,matchMediasubscription,localStoragepersistencepackages/sdk/src/components/RenderedResult/RenderedResult.tsx— passes the resolved theme toMalloyRendererand emits scoped CSS varsServer
api-doc.yaml—Themeschema +/themeendpointspackages/server/src/service/theme_store.ts— SQLite-backed runtime storepackages/server/src/controller/theme.controller.ts— GET / PUT / DELETEpackages/server/src/config.ts— instance + per-environmentthemeparsing with sanitizerpackages/server/src/storage/duckdb/schema.ts—themestable migrationApp
packages/app/src/components/pages/ThemeEditorPage/*— editor page, sections, previewspackages/app/src/components/common/ThemeToggle.tsx— header viewer togglepackages/app/src/theme/PublisherMuiThemeProvider.tsx,packages/app/src/theme/index.tsx— MUI palette keyed off SDK modeREADME
### Themingsubsection under Configuration covering all seven per-mode palette keys plus the scalarseries/fontanddefaultMode/allowUserTogglesemantics. Public-docs walkthrough lives in apaired PR on
malloydata.github.io.Test plan
bun run typecheck && bun run lint && bun run prettier:check && bun run testfrom repo rootcd packages/app && bun run test:playwright(newtheming.spec.tsplus the existing five chromium specs stay green)/settings/theme, change each picker, confirm the live preview updates and edits auto-saveecommerce), confirm chart, table, and dashboard tile colors follow the resolved theme# shape_mapchart (e.g.faa.sales_by_state), confirmpalette.mapColordrives the choropleth gradientlocalStorage["publisher:themeMode"]theme.allowUserToggle: falsein config, restart, confirm the toggle is hidden anddefaultModeis enforcedfrozenConfig: true: confirmPUT /api/v0/themereturns an error and the editor surfaces as read-only# theme.palette.series = [...]to a view, confirm only that view picks up the override