From c1c944672a2896f68be966bbac5a18b8c78ff8dd Mon Sep 17 00:00:00 2001 From: Monty Lennie Date: Mon, 25 May 2026 12:35:59 -0600 Subject: [PATCH 1/4] docs: add Theming Publisher guide Documents the four configuration surfaces for Publisher chart, table, and dashboard tile colors: instance and environment theme blocks in publisher.config.json, the in-app Theme Editor at /settings/theme, and per-chart `# theme.*` annotations. Covers the cascade order, the full palette key set (per-mode background, tableHeader, tableHeaderBackground, tableBody, tile, tileTitle, mapColor; shared series + font), light/dark mode behavior, and current theme-control limits. Adds a cross-link from publishing.malloynb so the main publishing guide points readers at the new doc. Signed-off-by: Monty Lennie --- .../publishing/publishing.malloynb | 2 +- .../user_guides/publishing/theming.malloynb | 115 ++++++++++++++++++ 2 files changed, 116 insertions(+), 1 deletion(-) create mode 100644 src/documentation/user_guides/publishing/theming.malloynb diff --git a/src/documentation/user_guides/publishing/publishing.malloynb b/src/documentation/user_guides/publishing/publishing.malloynb index 7f6b442c..e06d6326 100644 --- a/src/documentation/user_guides/publishing/publishing.malloynb +++ b/src/documentation/user_guides/publishing/publishing.malloynb @@ -104,7 +104,7 @@ That's it for local files. If your models use DuckDB with `.parquet`, `.csv`, or } ``` -See [Publisher Connections](connections.malloynb) for BigQuery, Snowflake, and other databases. +See [Publisher Connections](connections.malloynb) for BigQuery, Snowflake, and other databases. To change the visual style of rendered charts and tables, see [Theming Publisher](theming.malloynb). --- diff --git a/src/documentation/user_guides/publishing/theming.malloynb b/src/documentation/user_guides/publishing/theming.malloynb new file mode 100644 index 00000000..ed92488c --- /dev/null +++ b/src/documentation/user_guides/publishing/theming.malloynb @@ -0,0 +1,115 @@ +>>>markdown +# Theming Publisher + +Publisher renders Malloy charts, tables, and dashboard tiles with a theme that ships in both a light and a dark mode. You can customize it in four places, depending on how broad the change should be: + +| Level | Where it lives | Best for | +|---|---|---| +| Instance | top-level `theme` in `publisher.config.json` | the whole Publisher deployment (your company's brand) | +| Environment | `theme` on a single entry under `environments[]` | a single team or tenant inside a shared Publisher | +| Live (operator) | the in-app Theme Editor at `/settings/theme` | iterating on colors against real charts without restarting the server | +| Per chart | `# theme.*` annotations on a view inside a `.malloy` file | one-off styling for a specific chart | + +Layers cascade lowest to highest. A per-chart annotation only overrides the keys it sets; everything else falls through to the editor (per environment), then the environment config, then the instance config, then Publisher's built-in defaults. + +## Instance theme + +The instance theme applies to every environment unless overridden. Add a `theme` object at the top of `publisher.config.json`: + +```jsonc +{ + "frozenConfig": false, + "theme": { + "defaultMode": "light", + "allowUserToggle": true, + "palette": { + "series": ["#14b3cb", "#e47404", "#1474a4"], + "background": { "light": "#ffffff", "dark": "#1e293b" }, + "tableHeader": { "light": "#5d626b", "dark": "#cbd5e1" }, + "tableHeaderBackground": { "light": "#f5fafc", "dark": "#1e293b" }, + "tableBody": { "light": "#727883", "dark": "#e2e8f0" }, + "tile": { "light": "#f5fafc", "dark": "#0f172a" }, + "tileTitle": { "light": "#5d626b", "dark": "#94a3b8" }, + "mapColor": { "light": "#14b3cb", "dark": "#14b3cb" } + }, + "font": { "family": "Inter, sans-serif", "size": 12 } + }, + "environments": [ /* ... */ ] +} +``` + +Every key is optional. `palette.series` and `font` are shared across modes; the rest take an explicit `{ light, dark }` pair so a viewer in dark mode sees the dark variant. `tile` and `tileTitle` style the dashboard tile container that wraps each chart, independent of the chart background itself. `mapColor` is the saturated end of the choropleth gradient on `# shape_map` and `# segment_map` visualizations. + +`defaultMode` accepts `"light"`, `"dark"`, or `"auto"`. With `"auto"`, Publisher follows the viewer's OS preference (`prefers-color-scheme`) until the viewer overrides it from the header toggle. + +`allowUserToggle` controls whether the sun/moon button appears in the app header. Set it to `false` to lock viewers into your `defaultMode`. + +## Environment theme + +A single environment can override any subset of the instance theme. The override is shallow-deep: top-level keys merge per key, and the `palette.background` / `palette.tableHeader` sub-objects merge per mode. + +```jsonc +{ + "theme": { + "palette": { "series": ["#14b3cb", "#e47404"] } + }, + "environments": [ + { + "name": "marketing", + "theme": { + "palette": { "series": ["#ff0080", "#ff6b00", "#ffd000"] } + }, + "packages": [ /* ... */ ] + }, + { + "name": "finance", + "packages": [ /* uses the instance palette */ ] + } + ] +} +``` + +## In-app Theme Editor + +Editing JSON and restarting the server is fine for an initial setup, but most of the time you want to pick a color, see it on real charts, and pick again. Publisher ships an editor for that. + +Browse to `/settings/theme` while logged in as an operator. The page has a section for each part of the theme (background, tables, dashboard tiles, charts, maps, fonts) and a live preview pane that renders the current cascade with your in-progress edits applied. Changes are auto-saved to the server's runtime ThemeStore (a small SQLite-backed store next to your other persistent state) and take effect on the next render in any tab. + +The editor's writes layer on top of the environment-level theme, so different environments inside one Publisher can have different operator overrides. A Light/Dark toggle inside the editor switches which variant you're editing; the live preview shows that mode. + +When `publisher.config.json` has `frozenConfig: true`, the editor is read-only. This matches Publisher's behavior for every other runtime mutation: a frozen config is the only source of truth. + +## Per-chart annotations + +Inside a `.malloy` file, prefix a view with `# theme.*` tags to override the resolved theme just for that chart. All fields are optional. + +```malloy +# theme.palette.series = ["#ff0080", "#ff6b00", "#ffd000"] +# theme.palette.background.dark = "#080808" +# theme.font.family = "Roboto" +# theme.font.size = 13 +view: revenue_by_quarter is { /* ... */ } +``` + +Annotations with the wrong type are silently dropped. The chart still renders with the next theme layer down, so a typo never breaks a notebook. + +## Light and dark mode + +Publisher persists the viewer's choice in `localStorage` under the key `publisher:themeMode`. Clearing site data resets to the operator's `defaultMode`. + +Dark mode flips: + +- The app shell (sidebar, headers, content background). +- The Malloy renderer's chart background, axis text, and dashboard tile chrome. +- The table chrome (header, body text, borders, pinned background) via the `--malloy-render--*` CSS variables Publisher emits from the resolved theme. +- The saturated end of choropleth and heatmap gradients via `palette.mapColor`. + +`palette.series` and `font` stay the same across modes by design: they're brand identity, and brands generally read the same in either mode. If you genuinely need different series colors per mode, override them in a per-chart annotation that targets just the chart you care about. + +## What's not theme-controlled (yet) + +- The Publisher app logo (use `headerProps.logoHeader` on the SDK's `MalloyPublisherApp` for that). +- Per-package theme overrides. Use the environment level above the package, or a per-chart annotation below. +- Importing a theme from an external file or URL. + +See the [Publisher README](https://github.com/malloydata/publisher/blob/main/README.md#theming) for the canonical config reference. From b47ba71076742277557a0ccbf59e764c20a2fcaf Mon Sep 17 00:00:00 2001 From: Monty Lennie Date: Fri, 3 Jul 2026 09:37:57 -0600 Subject: [PATCH 2/4] docs: correct theming guide to match shipped Publisher behavior The initial draft predated the theming PR review and described behavior that does not match what shipped. Corrected and fact-checked against the merged Publisher/renderer code: - The in-app Theme Editor edits the single instance theme (not a per-environment theme), and its UI is four cards (Charts, Maps, Tables, Typography) with a per-card inline preview, not six sections plus one preview pane. - The runtime theme store is DuckDB, not SQLite. - publisher.config.json's `theme` only seeds the store on first boot; later JSON edits need `--init` or the editor's "Reset to defaults" to re-apply. Added an "applying config changes" note and dropped the implication that a plain restart re-reads the config. - Per-environment themes (environments[].theme) are reserved in the schema but not yet applied by any viewer; documented the three working surfaces and moved per-environment to "what is not theme-controlled yet". - `palette.mapColor` applies to choropleth maps only; rect-mark heatmaps keep their built-in scheme. - The logo note pointed at a non-existent `MalloyPublisherApp` component; corrected to the real app-shell `logoHeader` prop. - Theme changes apply immediately in the editing tab and to other viewers on their next page load (not "the next render in any tab"). - Added a security note: open-source Publisher does not authenticate requests, so the editor and PUT/DELETE /api/v0/theme are reachable by anyone who can reach the server; run behind an auth layer / trusted network and use frozenConfig where the theme must not be mutable. Softened the frozenConfig line since the theme is served from the store, not necessarily the config file. This matches the Publisher README and api-doc. Signed-off-by: Monty Lennie --- .../user_guides/publishing/theming.malloynb | 55 ++++++------------- 1 file changed, 18 insertions(+), 37 deletions(-) diff --git a/src/documentation/user_guides/publishing/theming.malloynb b/src/documentation/user_guides/publishing/theming.malloynb index ed92488c..7665e361 100644 --- a/src/documentation/user_guides/publishing/theming.malloynb +++ b/src/documentation/user_guides/publishing/theming.malloynb @@ -1,20 +1,21 @@ >>>markdown # Theming Publisher -Publisher renders Malloy charts, tables, and dashboard tiles with a theme that ships in both a light and a dark mode. You can customize it in four places, depending on how broad the change should be: +Publisher renders Malloy charts, tables, and dashboard tiles with a theme that ships in both a light and a dark mode. There is one instance theme for the whole Publisher, and you set it in one of three places depending on how broad the change should be: | Level | Where it lives | Best for | |---|---|---| -| Instance | top-level `theme` in `publisher.config.json` | the whole Publisher deployment (your company's brand) | -| Environment | `theme` on a single entry under `environments[]` | a single team or tenant inside a shared Publisher | -| Live (operator) | the in-app Theme Editor at `/settings/theme` | iterating on colors against real charts without restarting the server | +| Instance config | top-level `theme` in `publisher.config.json` | the whole Publisher deployment (your company's brand), checked into config | +| Live (operator) | the in-app Theme Editor at `/settings/theme` | iterating on colors against real charts without editing JSON or restarting | | Per chart | `# theme.*` annotations on a view inside a `.malloy` file | one-off styling for a specific chart | -Layers cascade lowest to highest. A per-chart annotation only overrides the keys it sets; everything else falls through to the editor (per environment), then the environment config, then the instance config, then Publisher's built-in defaults. +The config `theme` and the in-app editor set the same instance theme: the config value is the boot seed, and the editor edits it live (see below). When a chart renders, the layers cascade so a per-chart annotation only overrides the keys it sets, everything else falls through to the instance theme, and anything still unset falls through to Publisher's built-in defaults. + +Note: per-environment themes (a `theme` on an entry under `environments[]`) are reserved in the config schema but not yet applied by any viewer, so leave them off for now. See "What's not theme-controlled yet" at the end. ## Instance theme -The instance theme applies to every environment unless overridden. Add a `theme` object at the top of `publisher.config.json`: +The instance theme applies to every environment and viewer. Add a `theme` object at the top of `publisher.config.json`: ```jsonc { @@ -44,40 +45,19 @@ Every key is optional. `palette.series` and `font` are shared across modes; the `allowUserToggle` controls whether the sun/moon button appears in the app header. Set it to `false` to lock viewers into your `defaultMode`. -## Environment theme - -A single environment can override any subset of the instance theme. The override is shallow-deep: top-level keys merge per key, and the `palette.background` / `palette.tableHeader` sub-objects merge per mode. - -```jsonc -{ - "theme": { - "palette": { "series": ["#14b3cb", "#e47404"] } - }, - "environments": [ - { - "name": "marketing", - "theme": { - "palette": { "series": ["#ff0080", "#ff6b00", "#ffd000"] } - }, - "packages": [ /* ... */ ] - }, - { - "name": "finance", - "packages": [ /* uses the instance palette */ ] - } - ] -} -``` +The `theme` block seeds Publisher's runtime theme store the first time the server starts with an empty store. After that the store is the source of truth, so editing `theme` in `publisher.config.json` and restarting has no effect on later boots. To re-apply a config change, either start the server with `--init` (which drops the runtime store and re-seeds from config, the same way it re-seeds packages and environments) or use "Reset to defaults" in the Theme Editor below (which clears the saved theme and falls back to the config seed). ## In-app Theme Editor -Editing JSON and restarting the server is fine for an initial setup, but most of the time you want to pick a color, see it on real charts, and pick again. Publisher ships an editor for that. +Setting the theme in JSON is fine for the first-boot configuration, but most of the time you want to pick a color, see it on real charts, and pick again. Publisher ships an editor for that. + +Open `/settings/theme` in the Publisher app. The page has cards for Charts (series colors and the chart background), Maps, Tables (which also holds the dashboard-tile background and title), and Typography; each card shows an inline sample preview that reflects your in-progress edits. Changes are auto-saved to the server's runtime theme store (kept in the same DuckDB metadata store Publisher uses for its other runtime state). They apply to the editing tab immediately and to other viewers on their next page load. -Browse to `/settings/theme` while logged in as an operator. The page has a section for each part of the theme (background, tables, dashboard tiles, charts, maps, fonts) and a live preview pane that renders the current cascade with your in-progress edits applied. Changes are auto-saved to the server's runtime ThemeStore (a small SQLite-backed store next to your other persistent state) and take effect on the next render in any tab. +The editor edits the single instance theme, the same one `publisher.config.json` seeds. Saving replaces the stored theme, so your latest edit is what every environment and viewer of this Publisher sees. A Light/Dark toggle inside the editor switches which variant you're editing; the live preview shows that mode. -The editor's writes layer on top of the environment-level theme, so different environments inside one Publisher can have different operator overrides. A Light/Dark toggle inside the editor switches which variant you're editing; the live preview shows that mode. +When `publisher.config.json` has `frozenConfig: true`, the editor is read-only and the theme write endpoints (`PUT`/`DELETE /api/v0/theme`) are rejected, matching Publisher's behavior for every other runtime mutation. The theme is still served from the runtime store, so if you froze after editing, viewers see the last saved theme, not necessarily the config file's value. -When `publisher.config.json` has `frozenConfig: true`, the editor is read-only. This matches Publisher's behavior for every other runtime mutation: a frozen config is the only source of truth. +Note: open-source Publisher does not authenticate requests. The Theme Editor and its `PUT /api/v0/theme` endpoint are reachable by anyone who can reach the server, and a saved theme applies to every viewer. Run Publisher behind your own auth layer or on a trusted network, and set `frozenConfig: true` in any deployment where the theme must not be mutable at runtime. ## Per-chart annotations @@ -102,14 +82,15 @@ Dark mode flips: - The app shell (sidebar, headers, content background). - The Malloy renderer's chart background, axis text, and dashboard tile chrome. - The table chrome (header, body text, borders, pinned background) via the `--malloy-render--*` CSS variables Publisher emits from the resolved theme. -- The saturated end of choropleth and heatmap gradients via `palette.mapColor`. +- The saturated end of choropleth gradients (`# shape_map` / `# segment_map`) via `palette.mapColor`. Rect-mark heatmaps keep their built-in orange/blue scheme regardless of `mapColor`. `palette.series` and `font` stay the same across modes by design: they're brand identity, and brands generally read the same in either mode. If you genuinely need different series colors per mode, override them in a per-chart annotation that targets just the chart you care about. ## What's not theme-controlled (yet) -- The Publisher app logo (use `headerProps.logoHeader` on the SDK's `MalloyPublisherApp` for that). -- Per-package theme overrides. Use the environment level above the package, or a per-chart annotation below. +- Per-environment themes. `environments[].theme` is present in the config schema but not yet applied by any viewer, so today the single instance theme applies everywhere. Per-environment theming is a planned addition. +- The Publisher app logo. The brand mark is a separate app-shell element (the app exposes a `logoHeader` prop for embedders), not part of the theme. +- Per-package theme overrides. Use a per-chart annotation instead. - Importing a theme from an external file or URL. See the [Publisher README](https://github.com/malloydata/publisher/blob/main/README.md#theming) for the canonical config reference. From cf4af05c1765cc822c9c3a06f5ed94eff9840e82 Mon Sep 17 00:00:00 2001 From: Monty Lennie Date: Mon, 20 Jul 2026 13:22:46 -0600 Subject: [PATCH 3/4] docs: list the theming guide in the nav, fix its Publisher link The guide shipped without a table-of-contents entry, so it was only reachable by direct URL. Adds it to the Publishing group after "MCP for AI Agents". The closing reference pointed at README.md#theming. Publisher moved that content to docs/theming.md and the README now only links to it from a table, so the anchor no longer resolves. Points at docs/theming.md instead. Signed-off-by: Monty Lennie --- src/documentation/user_guides/publishing/theming.malloynb | 2 +- src/table_of_contents.json | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/documentation/user_guides/publishing/theming.malloynb b/src/documentation/user_guides/publishing/theming.malloynb index 7665e361..aa8fbedd 100644 --- a/src/documentation/user_guides/publishing/theming.malloynb +++ b/src/documentation/user_guides/publishing/theming.malloynb @@ -93,4 +93,4 @@ Dark mode flips: - Per-package theme overrides. Use a per-chart annotation instead. - Importing a theme from an external file or URL. -See the [Publisher README](https://github.com/malloydata/publisher/blob/main/README.md#theming) for the canonical config reference. +See [Publisher's theming doc](https://github.com/malloydata/publisher/blob/main/docs/theming.md) for the canonical config reference. diff --git a/src/table_of_contents.json b/src/table_of_contents.json index 31420707..28bc0330 100644 --- a/src/table_of_contents.json +++ b/src/table_of_contents.json @@ -97,6 +97,10 @@ { "title": "MCP for AI Agents", "link": "/user_guides/publishing/mcp_agents.malloynb" + }, + { + "title": "Theming", + "link": "/user_guides/publishing/theming.malloynb" } ] }, From cdbc4ae475a4b8f0e41686955f6f910c27d758e3 Mon Sep 17 00:00:00 2001 From: Monty Lennie Date: Mon, 20 Jul 2026 15:38:15 -0600 Subject: [PATCH 4/4] docs: reconcile the two theme vocabularies The site now documents two different `theme.*` grammars. The model-options page teaches flat keys inherited from the bare renderer; the new Publisher guide teaches the namespaced ones Publisher actually reads. Nothing pointed from either to the other, so a reader following the wrong one gets no error, just no effect. Publisher supplies most of those flat properties itself, and a caller-supplied theme is resolved ahead of both `# theme.*` and `## theme.*`, so only the four weight and spacing keys fall through. Says so, and points at the guide. Also drops a colour claim about heatmap gradients that only held for some mark types, and replaces the "use a per-chart annotation instead" line: a model-level `## theme.*` themes a whole file, which is the real answer for that case. Signed-off-by: Monty Lennie --- src/documentation/user_guides/publishing/theming.malloynb | 5 +++-- src/documentation/visualizations/model_options.malloynb | 6 ++++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/documentation/user_guides/publishing/theming.malloynb b/src/documentation/user_guides/publishing/theming.malloynb index aa8fbedd..1ae8bae5 100644 --- a/src/documentation/user_guides/publishing/theming.malloynb +++ b/src/documentation/user_guides/publishing/theming.malloynb @@ -82,7 +82,7 @@ Dark mode flips: - The app shell (sidebar, headers, content background). - The Malloy renderer's chart background, axis text, and dashboard tile chrome. - The table chrome (header, body text, borders, pinned background) via the `--malloy-render--*` CSS variables Publisher emits from the resolved theme. -- The saturated end of choropleth gradients (`# shape_map` / `# segment_map`) via `palette.mapColor`. Rect-mark heatmaps keep their built-in orange/blue scheme regardless of `mapColor`. +- The saturated end of choropleth gradients (`# shape_map` / `# segment_map`) via `palette.mapColor`. Rect-mark heatmaps keep their built-in scheme regardless of `mapColor`. `palette.series` and `font` stay the same across modes by design: they're brand identity, and brands generally read the same in either mode. If you genuinely need different series colors per mode, override them in a per-chart annotation that targets just the chart you care about. @@ -90,7 +90,8 @@ Dark mode flips: - Per-environment themes. `environments[].theme` is present in the config schema but not yet applied by any viewer, so today the single instance theme applies everywhere. Per-environment theming is a planned addition. - The Publisher app logo. The brand mark is a separate app-shell element (the app exposes a `logoHeader` prop for embedders), not part of the theme. -- Per-package theme overrides. Use a per-chart annotation instead. +- Per-package theme overrides. Annotate a whole model instead: a `## theme.*` annotation at the top of a + `.malloy` file themes every view in it, and a view's own `# theme.*` still wins over it. - Importing a theme from an external file or URL. See [Publisher's theming doc](https://github.com/malloydata/publisher/blob/main/docs/theming.md) for the canonical config reference. diff --git a/src/documentation/visualizations/model_options.malloynb b/src/documentation/visualizations/model_options.malloynb index 084ec605..dc19b579 100644 --- a/src/documentation/visualizations/model_options.malloynb +++ b/src/documentation/visualizations/model_options.malloynb @@ -107,4 +107,10 @@ run: airports -> { | `tablePinnedBorder` | Border for pinned column edges | `2px solid #ddd` | | `fontFamily` | Font family for all rendered output | `monospace`, `'Helvetica Neue'` | | `background` | Overall background color | `white`, `transparent` | + +**Viewing these models in Malloy Publisher?** It depends on which surface you are looking at, and the two behave in opposite ways. + +On a model view, a notebook cell, or a saved result, Publisher supplies its own instance theme, and a caller-supplied theme takes precedence over both `# theme.*` and `## theme.*`. So most of the properties above have no effect there: `tableBodyColor`, `tableHeaderColor`, `tableFontSize`, `tableBackground`, `tableBorder`, `tablePinnedBackground`, `tablePinnedBorder`, `fontFamily` and `background` are all supplied by Publisher and win. Only `tableBodyWeight`, `tableHeaderWeight`, `tableRowHeight` and `tableGutterSize` fall through. For those surfaces Publisher has its own vocabulary, namespaced under `theme.palette` and `theme.font`, where a per-chart annotation overrides the instance theme rather than losing to it. See [Theming Publisher](../user_guides/publishing/theming.malloynb). + +In the Explore panel on a model page, results render with no instance theme at all, so the properties above work exactly as documented here, and the namespaced `theme.palette` vocabulary does nothing. Both panels sit on the same page, so a view can look different in the explorer than it does once saved. >>>markdown