From f9e5b66c9edd51f9a6e5b3a8ec278fcfc83e168d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gy=C3=B6rgy=20Bolyki?= Date: Wed, 7 Oct 2026 18:06:35 +0100 Subject: [PATCH 1/5] TASK-557 - Add six web colour themes Extend the existing picker, shared URLs, startup palettes and covers. Focused and Chrome checks pass; full scanner suite needs Java. --- .../task-557 - Add-six-web-colour-themes.md | 62 ++++++++ docs/viewers/web/index.md | 11 +- src/viewers/web/atoms/theme.ts | 149 +++++++++++++++++- src/viewers/web/page.ts | 13 +- src/viewers/web/startup/page.ts | 8 +- src/viewers/web/url.ts | 2 +- test-bun/web-sharing.test.ts | 5 +- test-bun/web-theme.test.ts | 5 +- 8 files changed, 234 insertions(+), 21 deletions(-) create mode 100644 backlog/tasks/task-557 - Add-six-web-colour-themes.md diff --git a/backlog/tasks/task-557 - Add-six-web-colour-themes.md b/backlog/tasks/task-557 - Add-six-web-colour-themes.md new file mode 100644 index 000000000..a3cb7a19d --- /dev/null +++ b/backlog/tasks/task-557 - Add-six-web-colour-themes.md @@ -0,0 +1,62 @@ +--- +id: TASK-557 +title: Add six web colour themes +status: Done +assignee: + - '@codex' +created_date: '2026-10-07 17:00' +updated_date: '2026-10-07 17:06' +labels: [] +dependencies: [] +references: + - src/viewers/web/atoms/theme.ts + - src/viewers/web/page.ts +modified_files: + - src/viewers/web/atoms/theme.ts + - src/viewers/web/page.ts + - src/viewers/web/startup/page.ts + - test-bun/web-theme.test.ts + - test-bun/web-sharing.test.ts + - docs/viewers/web/index.md + - src/viewers/web/url.ts +ordinal: 640000 +--- + +## Description + + +Architecture maps need palettes that suit different projects, including a dark electric-blue and cyan palette inspired by the Teslatlas app icon. + + +## Acceptance Criteria + +- [x] #1 Six additional themes are selectable and remembered in the web viewer. +- [x] #2 New themes survive shared query URLs and publication paths, including setup and exported covers. +- [x] #3 Existing Auto, Light, Dark and Blueprint choices keep working. + + +## Definition of Done + +- [x] #1 Acceptance criteria have objective verification evidence. +- [x] #2 Relevant checks pass and changes remain task-scoped. +- [x] #3 Public contracts or documentation are updated when behavior changes. +- [x] #4 Implementation Plan reflects the final approach; correction history and verification are recorded in Implementation Notes. + + +## Implementation Plan + + +Extend the existing palette registry and derive page/setup CSS from it. Keep the current theme picker, URL and cover mechanisms. Extend existing URL and sharing tests because their hardcoded old choices miss unsupported new themes; verify actual switching in a browser and run the repository check. + + +## Implementation Notes + + +Focused viewer checks: 14 pass, 0 fail with the repository runner options. Chrome browser checks passed for all 9 palettes: picker, saved choice, reload and setup screen. Full check: lint, typecheck and 32 Node tests pass; Bun suite 756 pass, 51 skip, 17 fail, all Java/Scala/COBOL scanner failures due to missing Java runtime. No scanner changes. Browser screenshots retained outside source. + + +## Final Summary + + +Added Teslatlas, Solar, Ocean, Forest, Plum and Sand to the existing theme menu, shared URLs, startup CSS and exported covers. Registry-derived CSS keeps the page and setup consistent. Focused and browser checks pass; full suite requires a Java runtime for unrelated scanners. + diff --git a/docs/viewers/web/index.md b/docs/viewers/web/index.md index a65a96239..fd18e4222 100644 --- a/docs/viewers/web/index.md +++ b/docs/viewers/web/index.md @@ -114,7 +114,7 @@ infer its merge base, or install a CI workflow. ### Social previews -Export also writes `cover-light.png`, `cover-dark.png`, and `cover-blueprint.png` +Export also writes a `cover-{theme}.png` for each theme (excluding Auto) at 1200×630 pixels. Each cover uses the actual map, one continuous graph-paper grid, and a glass footer with the project title and groma.md attribution. The map's geometry, drawing, themes, and bundled fonts remain the source of truth. @@ -275,11 +275,14 @@ so all Help content fits without scrolling at 1280×720. The information icon op a brief product description and repository link, followed by every directly declared third-party runtime library and development tool, including its version, license, and project link. The dropdown lists Auto, -Light, Dark and Blueprint. Auto is the default for a browser profile with no +Light, Dark, Blueprint, Teslatlas, Solar, Ocean, Forest, Plum and Sand. Auto is the default for a browser profile with no saved choice and follows that browser's light or dark colour preference. A choice is saved for later visits. Blueprint uses a deep navy field, quieter blue drafting lines, and groma.md green for selection and active flows, with restrained calibration marks -and the same architecture and compass. The full footer is absent, and Live work keeps the bottom +and the same architecture and compass. Teslatlas pairs near-black paper with electric blue lines +and cyan highlights, inspired by the Teslatlas app icon. Solar uses warm amber, Ocean teal, +Forest moss green, Plum violet, and Sand warm light paper with terracotta highlights. +All themes use the same map, saved-choice, URL and cover rules. The full footer is absent, and Live work keeps the bottom centre. The camera fits architecture into the clear area between the floating hierarchy and any open details pane, so the grid continues beneath the chrome without hiding the fitted world. @@ -930,7 +933,7 @@ Repeated `flow=` parameters name checked scenarios in selection order; selection, the last flow owns details. An architecture selection alongside it opens endpoint inspection while retaining the checked flows and focused step. Opening a shared selection fits its content. -`theme=auto|light|dark|blueprint` names an explicit theme, and `hud=off` hides +`theme=auto|light|dark|blueprint|teslatlas|solar|ocean|forest|plum|sand` names an explicit theme, and `hud=off` hides the page chrome. A publication path ending in `/architecture/{theme}/`, with any repository prefix, also selects one of those themes. An explicit query theme takes priority over the path, then the saved browser preference applies. diff --git a/src/viewers/web/atoms/theme.ts b/src/viewers/web/atoms/theme.ts index 933d07ff5..79de4a531 100644 --- a/src/viewers/web/atoms/theme.ts +++ b/src/viewers/web/atoms/theme.ts @@ -2,6 +2,7 @@ import { GROMA_ACCENT, GROMA_ACCENT_ON_LIGHT } from '../../../brand.ts' /** Every colour the page and the map use, as CSS variable values; the map's level tints mix paper and ink in its stylesheet. */ export interface Palette { + colourScheme: 'light' | 'dark' paper: string ink: string muted: string @@ -29,7 +30,7 @@ export interface Palette { diffRemoved: string } -export type WebTheme = 'light' | 'dark' | 'blueprint' +export type WebTheme = 'light' | 'dark' | 'blueprint' | 'teslatlas' | 'solar' | 'ocean' | 'forest' | 'plum' | 'sand' export type WebThemeMode = 'auto' | WebTheme /** Shared typography for the interactive map and its published cover. */ @@ -43,10 +44,11 @@ export function mixColour(paper: string, ink: string, share: number): string { return `#${channels}` } -export const themeModes: readonly WebThemeMode[] = ['auto', 'light', 'dark', 'blueprint'] +export const themeModes: readonly WebThemeMode[] = ['auto', 'light', 'dark', 'blueprint', 'teslatlas', 'solar', 'ocean', 'forest', 'plum', 'sand'] export const palettes: Record = { light: { + colourScheme: 'light', paper: '#FFFFFF', ink: '#22262E', muted: '#585B62', @@ -68,6 +70,7 @@ export const palettes: Record = { diffRemoved: '#B42318', }, dark: { + colourScheme: 'dark', paper: '#111315', ink: '#E6E8EB', muted: '#9AA0A8', @@ -89,6 +92,7 @@ export const palettes: Record = { diffRemoved: '#FF7B72', }, blueprint: { + colourScheme: 'dark', paper: '#07152B', ink: '#D8F3FF', muted: '#79A9BD', @@ -109,6 +113,139 @@ export const palettes: Record = { diffModified: '#FFE066', diffRemoved: '#FF8FAB', }, + // Electric blue, cyan and near-black from the Teslatlas app icon. + teslatlas: { + colourScheme: 'dark', + paper: '#030813', + ink: '#E6F4FF', + muted: '#91ABC9', + accentText: GROMA_ACCENT, + highlight: '#21D4DC', + highlightText: '#21D4DC', + hairline: '#193151', + hover: 'rgba(33, 212, 220, 0.10)', + line: '#386ED6', + hatch: '#21417D', + syntaxComment: '#8096B5', + syntaxFunction: '#21D4DC', + syntaxKeyword: '#9DADFF', + syntaxNumber: '#FFC08A', + syntaxString: '#83E6CA', + syntaxType: '#68B5FF', + diffAdded: '#68B5FF', + diffModified: '#FFD477', + diffRemoved: '#FF929C', + }, + solar: { + colourScheme: 'dark', + paper: '#1D1410', + ink: '#FFF1DE', + muted: '#C2A38C', + accentText: GROMA_ACCENT, + highlight: '#FFB35C', + highlightText: '#FFB35C', + hairline: '#483127', + hover: 'rgba(255, 179, 92, 0.10)', + line: '#AB7854', + hatch: '#69462F', + syntaxComment: '#B59A86', + syntaxFunction: '#FFD48C', + syntaxKeyword: '#F9A3B7', + syntaxNumber: '#FFBC80', + syntaxString: '#B4D99C', + syntaxType: '#9DCFE6', + diffAdded: '#9DCFE6', + diffModified: '#FFD48C', + diffRemoved: '#FF9292', + }, + ocean: { + colourScheme: 'dark', + paper: '#071E26', + ink: '#E1F6F7', + muted: '#90B8BF', + accentText: GROMA_ACCENT, + highlight: '#5BD6CE', + highlightText: '#5BD6CE', + hairline: '#204650', + hover: 'rgba(91, 214, 206, 0.10)', + line: '#558E9F', + hatch: '#2D5968', + syntaxComment: '#82A7B1', + syntaxFunction: '#89E2DC', + syntaxKeyword: '#C9B4FF', + syntaxNumber: '#FFCA8A', + syntaxString: '#B4E3A2', + syntaxType: '#8FCFFF', + diffAdded: '#8FCFFF', + diffModified: '#FFCA8A', + diffRemoved: '#FF9A9A', + }, + forest: { + colourScheme: 'dark', + paper: '#101C15', + ink: '#EAF3E6', + muted: '#A0B59A', + accentText: GROMA_ACCENT, + highlight: '#B6DA75', + highlightText: '#B6DA75', + hairline: '#2E4332', + hover: 'rgba(182, 218, 117, 0.10)', + line: '#6E9567', + hatch: '#425E3F', + syntaxComment: '#92AB8A', + syntaxFunction: '#E2D68B', + syntaxKeyword: '#D9B1E6', + syntaxNumber: '#F4BA89', + syntaxString: '#B6DA75', + syntaxType: '#99CDDC', + diffAdded: '#99CDDC', + diffModified: '#E2D68B', + diffRemoved: '#F69B98', + }, + plum: { + colourScheme: 'dark', + paper: '#201426', + ink: '#F6E9FC', + muted: '#BDA2C8', + accentText: GROMA_ACCENT, + highlight: '#E3A4FA', + highlightText: '#E3A4FA', + hairline: '#493052', + hover: 'rgba(227, 164, 250, 0.10)', + line: '#9F78B0', + hatch: '#64476F', + syntaxComment: '#AC91B7', + syntaxFunction: '#F2C58D', + syntaxKeyword: '#E3A4FA', + syntaxNumber: '#F8B29A', + syntaxString: '#A8DEBF', + syntaxType: '#9ACFF5', + diffAdded: '#9ACFF5', + diffModified: '#F2C58D', + diffRemoved: '#FF99AD', + }, + sand: { + colourScheme: 'light', + paper: '#F7F0E3', + ink: '#352D25', + muted: '#706253', + accentText: GROMA_ACCENT_ON_LIGHT, + highlight: '#A34A24', + highlightText: '#963D1A', + hairline: '#DED2BD', + hover: 'rgba(53, 45, 37, 0.06)', + line: '#A3947D', + hatch: '#C5B89F', + syntaxComment: '#786D5E', + syntaxFunction: '#226C76', + syntaxKeyword: '#7C3E91', + syntaxNumber: '#A34A24', + syntaxString: '#3B6D38', + syntaxType: '#315CA0', + diffAdded: '#315CA0', + diffModified: '#89600B', + diffRemoved: '#AC302D', + }, } /** Whether a stored or shared value names a supported user choice. */ @@ -133,6 +270,7 @@ export const onColour = '#020B12' export function cssBlock(palette: Palette): string { return ` + color-scheme: ${palette.colourScheme}; --paper: ${palette.paper}; --ink: ${palette.ink}; --muted: ${palette.muted}; @@ -158,3 +296,10 @@ export function cssBlock(palette: Palette): string { --map-grid-major: ${mixColour(palette.paper, palette.line, 0.2)}; ` } + +/** The viewer and startup screen install the same palettes. */ +export function themeCss(): string { + return Object.entries(palettes) + .map(([theme, palette]) => `[data-theme="${theme}"] { ${cssBlock(palette)} }`) + .join('\n') +} diff --git a/src/viewers/web/page.ts b/src/viewers/web/page.ts index bd6a18f8e..227f07415 100644 --- a/src/viewers/web/page.ts +++ b/src/viewers/web/page.ts @@ -15,7 +15,7 @@ import { chromeCss } from './atoms/chrome.ts' import { escaped } from './atoms/escape.ts' import { anchoredPopoverCss } from './atoms/popover.ts' import { kindGlyph, kindLabel } from '../atoms/kind.ts' -import { cssBlock, palettes, themeLabel, themeModes, webFontFamily, type WebThemeMode } from './atoms/theme.ts' +import { cssBlock, palettes, themeCss, themeLabel, themeModes, webFontFamily } from './atoms/theme.ts' import { addDialogCss } from './chrome/add.ts' import { creditsControl, creditsCss } from './chrome/credits.ts' import { editorCss } from './editing/gestures.ts' @@ -89,9 +89,8 @@ const style = ` --hierarchy-inset: var(--hierarchy-column); --details-inset: var(--details-column); } - [data-theme="dark"] { ${cssBlock(palettes.dark)} } + ${themeCss()} [data-theme="blueprint"] { - ${cssBlock(palettes.blueprint)} --chrome-surface: color-mix(in srgb, var(--paper) 78%, transparent); } html { margin: 0; height: 100%; overflow-x: auto; overflow-y: hidden; background: var(--paper); } @@ -396,7 +395,7 @@ function legend(): string { }).join('') } -const themeIcons: Record = { +const themeIcons = { auto: autoIcon, light: sunIcon, dark: moonIcon, @@ -405,7 +404,11 @@ const themeIcons: Record = { function themeControl(): string { const options = themeModes - .map(mode => ``) + .map(mode => { + const mark = mode in themeIcons ? themeIcons[mode as keyof typeof themeIcons] + : icon(``) + return `` + }) .join('') return `
ThemeAuto
${options}
` } diff --git a/src/viewers/web/startup/page.ts b/src/viewers/web/startup/page.ts index 5e814b97a..9a3e74fa0 100644 --- a/src/viewers/web/startup/page.ts +++ b/src/viewers/web/startup/page.ts @@ -6,7 +6,7 @@ import { scannerSelection, scannerSelectionScript } from './scanners.ts' import type { GromaInitResult } from '../../../initialize.ts' import { escaped } from '../atoms/escape.ts' -import { cssBlock, palettes } from '../atoms/theme.ts' +import { cssBlock, palettes, themeCss, themeModes } from '../atoms/theme.ts' import { chromeCss } from '../atoms/chrome.ts' import { startupUpdate, type StartupProgress } from './progress.ts' @@ -25,9 +25,7 @@ interface SetupPage { const style = ` :root { ${cssBlock(palettes.light)} color-scheme: light; } @media (prefers-color-scheme: dark) { :root { ${cssBlock(palettes.dark)} color-scheme: dark; } } - [data-theme="light"] { ${cssBlock(palettes.light)} color-scheme: light; } - [data-theme="dark"] { ${cssBlock(palettes.dark)} color-scheme: dark; } - [data-theme="blueprint"] { ${cssBlock(palettes.blueprint)} color-scheme: dark; } + ${themeCss()} * { box-sizing: border-box; } body { margin: 0; min-height: 100dvh; padding: 32px 20px; @@ -156,7 +154,7 @@ const style = ` const script = ` ${scannerSelectionScript} const theme = localStorage.getItem('groma.theme'); - if (['light', 'dark', 'blueprint'].includes(theme)) document.documentElement.dataset.theme = theme; + if (${JSON.stringify(themeModes.filter(mode => mode !== 'auto'))}.includes(theme)) document.documentElement.dataset.theme = theme; const main = document.querySelector('main'); const content = document.querySelector('#startup-content'); const progress = document.querySelector('#startup-progress'); diff --git a/src/viewers/web/url.ts b/src/viewers/web/url.ts index 841846608..46f3df66b 100644 --- a/src/viewers/web/url.ts +++ b/src/viewers/web/url.ts @@ -128,7 +128,7 @@ function appendTab(pairs: [string, string][], state: ViewState, change: Componen * Reads the ordered architecture selection (repeated `=` and * `relationship=/` entries) or `task=`, * checked flows (repeated `flow=`) and the last flow's one-based `step=`, - * `tab=how|tasks`, `theme=auto|light|dark|blueprint` and `hud=off`. A query theme overrides + * `tab=how|tasks`, `theme=` and `hud=off`. A query theme overrides * the publication path theme, then the saved default. Ids are the authored ids; anything the world * or the work does not know is ignored, a kind naming an element of another kind included. */ diff --git a/test-bun/web-sharing.test.ts b/test-bun/web-sharing.test.ts index b9b62fd31..6e23b9aa5 100644 --- a/test-bun/web-sharing.test.ts +++ b/test-bun/web-sharing.test.ts @@ -8,6 +8,7 @@ import { loadProjectProfile, saveProjectProfile } from '../src/project-profile.t import { exportWebViewer } from '../src/viewers/web/export.ts' import { startWebViewer } from '../src/viewers/web/server.ts' import { sharingMetadata } from '../src/viewers/web/sharing/metadata.ts' +import { coverThemes } from '../src/viewers/web/sharing/images.ts' const fixture = path.resolve(import.meta.dir, '../test/fixtures/empty-project') const mappedFixture = path.resolve(import.meta.dir, '../test/fixtures/source-view') @@ -50,7 +51,7 @@ test.concurrent('export writes the themed image referenced by its initial sharin assert.equal(fields.get('og:image:height'), '630') assert.ok(fields.get('og:image:alt')) assert.equal(fields.has('og:description'), false) - for (const theme of ['light', 'dark', 'blueprint']) { + for (const theme of coverThemes) { assertCover(await readFile(path.join(output, `cover-${theme}.png`))) } } finally { @@ -66,7 +67,7 @@ test.concurrent('live initial metadata uses the project profile and links to a t const project = await loadProjectProfile(root) await saveProjectProfile(root, { ...project!, title: 'Shared project', description: 'Tracks submitted requests.' }) viewer = await startWebViewer(root, { port: 0, workSource: emptyWork() }) - for (const theme of ['light', 'dark', 'blueprint']) { + for (const theme of coverThemes) { const url: string = `${viewer.url}/?theme=${theme}` const fields = await headMetadata(await (await fetch(url)).text()) assert.equal(fields.get('og:title'), 'Shared project') diff --git a/test-bun/web-theme.test.ts b/test-bun/web-theme.test.ts index c1987fcb6..967abf9a4 100644 --- a/test-bun/web-theme.test.ts +++ b/test-bun/web-theme.test.ts @@ -5,12 +5,13 @@ import { test } from 'bun:test' import { loadAnnotatedArchitecture } from '../src/core.ts' import { readView, writeView } from '../src/viewers/web/url.ts' +import { themeModes } from '../src/viewers/web/atoms/theme.ts' const fixture = path.resolve(import.meta.dir, '../test/fixtures/empty-project') test.concurrent('publication paths select a theme before the saved browser preference', async () => { const world = await loadAnnotatedArchitecture(fixture) - for (const theme of ['auto', 'light', 'dark', 'blueprint']) { + for (const theme of themeModes) { const url = new URL(`https://example.test/repository/architecture/${theme}/`) assert.equal(readView(url, world, [], [], 'light').theme, theme) } @@ -37,7 +38,7 @@ test.concurrent('theme choices survive a shared URL including Auto on a Blueprin const world = await loadAnnotatedArchitecture(fixture) const url = new URL('https://example.test/repository/architecture/blueprint/') const opened = readView(url, world, [], [], 'dark') - for (const theme of ['auto', 'light', 'dark', 'blueprint'] as const) { + for (const theme of themeModes) { url.search = writeView({ ...opened, theme }, world, [], url.pathname) assert.equal(readView(url, world, [], [], 'dark').theme, theme) assert.equal(url.searchParams.get('theme'), theme === 'blueprint' ? null : theme) From d706839c0c861a4c27543d42c561267370bd7ad5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gy=C3=B6rgy=20Bolyki?= Date: Wed, 7 Oct 2026 18:26:07 +0100 Subject: [PATCH 2/5] TASK-557 - Add six web colour themes Name the electric-blue and cyan palette Electric and remove product branding. Focused URL, export and browser checks pass. --- backlog/tasks/task-557 - Add-six-web-colour-themes.md | 10 +++++++--- docs/viewers/web/index.md | 8 ++++---- src/viewers/web/atoms/theme.ts | 8 ++++---- 3 files changed, 15 insertions(+), 11 deletions(-) diff --git a/backlog/tasks/task-557 - Add-six-web-colour-themes.md b/backlog/tasks/task-557 - Add-six-web-colour-themes.md index a3cb7a19d..a7bdff42d 100644 --- a/backlog/tasks/task-557 - Add-six-web-colour-themes.md +++ b/backlog/tasks/task-557 - Add-six-web-colour-themes.md @@ -5,7 +5,7 @@ status: Done assignee: - '@codex' created_date: '2026-10-07 17:00' -updated_date: '2026-10-07 17:06' +updated_date: '2026-10-07 17:26' labels: [] dependencies: [] references: @@ -25,7 +25,7 @@ ordinal: 640000 ## Description -Architecture maps need palettes that suit different projects, including a dark electric-blue and cyan palette inspired by the Teslatlas app icon. +Architecture maps need palettes that suit different projects, including a dark electric-blue and cyan palette. ## Acceptance Criteria @@ -47,16 +47,20 @@ Architecture maps need palettes that suit different projects, including a dark e Extend the existing palette registry and derive page/setup CSS from it. Keep the current theme picker, URL and cover mechanisms. Extend existing URL and sharing tests because their hardcoded old choices miss unsupported new themes; verify actual switching in a browser and run the repository check. + +Rename the electric-blue/cyan palette to Electric throughout the public theme registry, documentation and PR. Rebuild the website and recheck theme links and covers. ## Implementation Notes Focused viewer checks: 14 pass, 0 fail with the repository runner options. Chrome browser checks passed for all 9 palettes: picker, saved choice, reload and setup screen. Full check: lint, typecheck and 32 Node tests pass; Bun suite 756 pass, 51 skip, 17 fail, all Java/Scala/COBOL scanner failures due to missing Java runtime. No scanner changes. Browser screenshots retained outside source. + +Renamed the electric-blue/cyan palette to Electric at the owner request; removed product references from public code, documentation, task description and PR. All 14 focused tests and browser checks for all 9 palettes pass after renaming. Website export rebuilt; 57 site tests pass. ## Final Summary -Added Teslatlas, Solar, Ocean, Forest, Plum and Sand to the existing theme menu, shared URLs, startup CSS and exported covers. Registry-derived CSS keeps the page and setup consistent. Focused and browser checks pass; full suite requires a Java runtime for unrelated scanners. +Added Electric, Solar, Ocean, Forest, Plum and Sand to the existing theme menu, shared URLs, startup CSS and exported covers. Focused and browser checks pass; full suite requires Java for unrelated scanners. diff --git a/docs/viewers/web/index.md b/docs/viewers/web/index.md index fd18e4222..0c3268ded 100644 --- a/docs/viewers/web/index.md +++ b/docs/viewers/web/index.md @@ -275,12 +275,12 @@ so all Help content fits without scrolling at 1280×720. The information icon op a brief product description and repository link, followed by every directly declared third-party runtime library and development tool, including its version, license, and project link. The dropdown lists Auto, -Light, Dark, Blueprint, Teslatlas, Solar, Ocean, Forest, Plum and Sand. Auto is the default for a browser profile with no +Light, Dark, Blueprint, Electric, Solar, Ocean, Forest, Plum and Sand. Auto is the default for a browser profile with no saved choice and follows that browser's light or dark colour preference. A choice is saved for later visits. Blueprint uses a deep navy field, quieter blue drafting lines, and groma.md green for selection and active flows, with restrained calibration marks -and the same architecture and compass. Teslatlas pairs near-black paper with electric blue lines -and cyan highlights, inspired by the Teslatlas app icon. Solar uses warm amber, Ocean teal, +and the same architecture and compass. Electric pairs near-black paper with electric blue lines +and cyan highlights. Solar uses warm amber, Ocean teal, Forest moss green, Plum violet, and Sand warm light paper with terracotta highlights. All themes use the same map, saved-choice, URL and cover rules. The full footer is absent, and Live work keeps the bottom centre. The camera fits architecture into the clear area between @@ -933,7 +933,7 @@ Repeated `flow=` parameters name checked scenarios in selection order; selection, the last flow owns details. An architecture selection alongside it opens endpoint inspection while retaining the checked flows and focused step. Opening a shared selection fits its content. -`theme=auto|light|dark|blueprint|teslatlas|solar|ocean|forest|plum|sand` names an explicit theme, and `hud=off` hides +`theme=auto|light|dark|blueprint|electric|solar|ocean|forest|plum|sand` names an explicit theme, and `hud=off` hides the page chrome. A publication path ending in `/architecture/{theme}/`, with any repository prefix, also selects one of those themes. An explicit query theme takes priority over the path, then the saved browser preference applies. diff --git a/src/viewers/web/atoms/theme.ts b/src/viewers/web/atoms/theme.ts index 79de4a531..6c4ee61f7 100644 --- a/src/viewers/web/atoms/theme.ts +++ b/src/viewers/web/atoms/theme.ts @@ -30,7 +30,7 @@ export interface Palette { diffRemoved: string } -export type WebTheme = 'light' | 'dark' | 'blueprint' | 'teslatlas' | 'solar' | 'ocean' | 'forest' | 'plum' | 'sand' +export type WebTheme = 'light' | 'dark' | 'blueprint' | 'electric' | 'solar' | 'ocean' | 'forest' | 'plum' | 'sand' export type WebThemeMode = 'auto' | WebTheme /** Shared typography for the interactive map and its published cover. */ @@ -44,7 +44,7 @@ export function mixColour(paper: string, ink: string, share: number): string { return `#${channels}` } -export const themeModes: readonly WebThemeMode[] = ['auto', 'light', 'dark', 'blueprint', 'teslatlas', 'solar', 'ocean', 'forest', 'plum', 'sand'] +export const themeModes: readonly WebThemeMode[] = ['auto', 'light', 'dark', 'blueprint', 'electric', 'solar', 'ocean', 'forest', 'plum', 'sand'] export const palettes: Record = { light: { @@ -113,8 +113,8 @@ export const palettes: Record = { diffModified: '#FFE066', diffRemoved: '#FF8FAB', }, - // Electric blue, cyan and near-black from the Teslatlas app icon. - teslatlas: { + // Electric blue and cyan on near-black paper. + electric: { colourScheme: 'dark', paper: '#030813', ink: '#E6F4FF', From 7db655206c656071737b0df5b784480cb2046fb4 Mon Sep 17 00:00:00 2001 From: Alex Gavrilescu Date: Sun, 11 Oct 2026 18:11:23 +0200 Subject: [PATCH 3/5] TASK-590 - Add six web colour themes --- ...> task-590 - Add-six-web-colour-themes.md} | 10 +++++---- src/viewers/web/atoms/theme.ts | 21 ++++++++++++------- src/viewers/web/page.ts | 17 ++++++++------- src/viewers/web/sharing/images.ts | 4 ++-- src/viewers/web/startup/page.ts | 5 +++-- test-bun/web-sharing.test.ts | 5 ++--- test-bun/web-theme.test.ts | 5 ++--- 7 files changed, 37 insertions(+), 30 deletions(-) rename backlog/tasks/{task-557 - Add-six-web-colour-themes.md => task-590 - Add-six-web-colour-themes.md} (72%) diff --git a/backlog/tasks/task-557 - Add-six-web-colour-themes.md b/backlog/tasks/task-590 - Add-six-web-colour-themes.md similarity index 72% rename from backlog/tasks/task-557 - Add-six-web-colour-themes.md rename to backlog/tasks/task-590 - Add-six-web-colour-themes.md index a7bdff42d..c3a2e033c 100644 --- a/backlog/tasks/task-557 - Add-six-web-colour-themes.md +++ b/backlog/tasks/task-590 - Add-six-web-colour-themes.md @@ -1,11 +1,11 @@ --- -id: TASK-557 +id: TASK-590 title: Add six web colour themes -status: Done +status: In Progress assignee: - '@codex' created_date: '2026-10-07 17:00' -updated_date: '2026-10-07 17:26' +updated_date: '2026-10-11 16:11' labels: [] dependencies: [] references: @@ -47,8 +47,8 @@ Architecture maps need palettes that suit different projects, including a dark e Extend the existing palette registry and derive page/setup CSS from it. Keep the current theme picker, URL and cover mechanisms. Extend existing URL and sharing tests because their hardcoded old choices miss unsupported new themes; verify actual switching in a browser and run the repository check. - Rename the electric-blue/cyan palette to Electric throughout the public theme registry, documentation and PR. Rebuild the website and recheck theme links and covers. +Test note for the maintainer pass. Rule and authority: acceptance criteria 1 and 2, every theme choice survives publication paths and shared URLs and gets a cover image. Wrong result caught: a new theme that the URL reader or the cover export does not know. Gap: the tests named only the original themes, and the first version of this change replaced those names with the implementation's own list, which cannot fail. Smallest change: name the themes literally and add one new theme to each loop. ## Implementation Notes @@ -57,6 +57,8 @@ Rename the electric-blue/cyan palette to Electric throughout the public theme re Focused viewer checks: 14 pass, 0 fail with the repository runner options. Chrome browser checks passed for all 9 palettes: picker, saved choice, reload and setup screen. Full check: lint, typecheck and 32 Node tests pass; Bun suite 756 pass, 51 skip, 17 fail, all Java/Scala/COBOL scanner failures due to missing Java runtime. No scanner changes. Browser screenshots retained outside source. Renamed the electric-blue/cyan palette to Electric at the owner request; removed product references from public code, documentation, task description and PR. All 14 focused tests and browser checks for all 9 palettes pass after renaming. Website export rebuilt; 57 site tests pass. + +Maintainer pass, 11 October 2026: merged main (Bun 1.4.3, OpenTUI 0.6.0) and renumbered this record from TASK-557, which main already uses. A cold simplicity review led to these changes: the theme names are one list (webThemes) that the menu, URL values, page CSS and cover images all read; cssBlock no longer emits color-scheme, so the main viewer keeps its behaviour for Light, Dark and Blueprint and only the startup screen sets it, as before; the theme menu marks are typed without casts; the theme tests name their themes literally again and include one new theme (sand). ## Final Summary diff --git a/src/viewers/web/atoms/theme.ts b/src/viewers/web/atoms/theme.ts index 6c4ee61f7..bedc4c57e 100644 --- a/src/viewers/web/atoms/theme.ts +++ b/src/viewers/web/atoms/theme.ts @@ -2,6 +2,7 @@ import { GROMA_ACCENT, GROMA_ACCENT_ON_LIGHT } from '../../../brand.ts' /** Every colour the page and the map use, as CSS variable values; the map's level tints mix paper and ink in its stylesheet. */ export interface Palette { + /** Not a CSS variable: the startup screen uses it to pick the browser's own light or dark form controls. */ colourScheme: 'light' | 'dark' paper: string ink: string @@ -30,7 +31,12 @@ export interface Palette { diffRemoved: string } -export type WebTheme = 'light' | 'dark' | 'blueprint' | 'electric' | 'solar' | 'ocean' | 'forest' | 'plum' | 'sand' +/** + * Every palette, in the order the theme menu lists them. This list is the one source for the menu, the + * `theme` URL values, the page CSS and the cover images: each name adds one cover PNG to every export. + */ +export const webThemes = ['light', 'dark', 'blueprint', 'electric', 'solar', 'ocean', 'forest', 'plum', 'sand'] as const +export type WebTheme = typeof webThemes[number] export type WebThemeMode = 'auto' | WebTheme /** Shared typography for the interactive map and its published cover. */ @@ -44,7 +50,7 @@ export function mixColour(paper: string, ink: string, share: number): string { return `#${channels}` } -export const themeModes: readonly WebThemeMode[] = ['auto', 'light', 'dark', 'blueprint', 'electric', 'solar', 'ocean', 'forest', 'plum', 'sand'] +export const themeModes: readonly WebThemeMode[] = ['auto', ...webThemes] export const palettes: Record = { light: { @@ -113,7 +119,6 @@ export const palettes: Record = { diffModified: '#FFE066', diffRemoved: '#FF8FAB', }, - // Electric blue and cyan on near-black paper. electric: { colourScheme: 'dark', paper: '#030813', @@ -270,7 +275,6 @@ export const onColour = '#020B12' export function cssBlock(palette: Palette): string { return ` - color-scheme: ${palette.colourScheme}; --paper: ${palette.paper}; --ink: ${palette.ink}; --muted: ${palette.muted}; @@ -297,9 +301,10 @@ export function cssBlock(palette: Palette): string { ` } -/** The viewer and startup screen install the same palettes. */ +/** + * One `[data-theme]` rule per palette, for the viewer and the startup screen. The viewer keeps Light as its + * `:root` default and never sets `data-theme="light"`; the startup screen sets it for a saved Light choice. + */ export function themeCss(): string { - return Object.entries(palettes) - .map(([theme, palette]) => `[data-theme="${theme}"] { ${cssBlock(palette)} }`) - .join('\n') + return webThemes.map(theme => `[data-theme="${theme}"] { ${cssBlock(palettes[theme])} }`).join('\n') } diff --git a/src/viewers/web/page.ts b/src/viewers/web/page.ts index 227f07415..1e3347c34 100644 --- a/src/viewers/web/page.ts +++ b/src/viewers/web/page.ts @@ -15,7 +15,7 @@ import { chromeCss } from './atoms/chrome.ts' import { escaped } from './atoms/escape.ts' import { anchoredPopoverCss } from './atoms/popover.ts' import { kindGlyph, kindLabel } from '../atoms/kind.ts' -import { cssBlock, palettes, themeCss, themeLabel, themeModes, webFontFamily } from './atoms/theme.ts' +import { cssBlock, palettes, themeCss, themeLabel, themeModes, webFontFamily, type WebTheme, type WebThemeMode } from './atoms/theme.ts' import { addDialogCss } from './chrome/add.ts' import { creditsControl, creditsCss } from './chrome/credits.ts' import { editorCss } from './editing/gestures.ts' @@ -395,20 +395,21 @@ function legend(): string { }).join('') } -const themeIcons = { - auto: autoIcon, +/** The original themes have their own icon; every other palette shows a dot in its highlight colour. */ +const themeIcons: Partial> = { light: sunIcon, dark: moonIcon, blueprint: blueprintIcon, } +function themeMark(mode: WebThemeMode): string { + if (mode === 'auto') return autoIcon + return themeIcons[mode] ?? icon(``) +} + function themeControl(): string { const options = themeModes - .map(mode => { - const mark = mode in themeIcons ? themeIcons[mode as keyof typeof themeIcons] - : icon(``) - return `` - }) + .map(mode => ``) .join('') return `
ThemeAuto
${options}
` } diff --git a/src/viewers/web/sharing/images.ts b/src/viewers/web/sharing/images.ts index a005b3c2d..3e388d730 100644 --- a/src/viewers/web/sharing/images.ts +++ b/src/viewers/web/sharing/images.ts @@ -2,10 +2,10 @@ import { readFileSync } from 'node:fs' import { initWasm, Resvg } from '@resvg/resvg-wasm' import wasm from '@resvg/resvg-wasm/index_bg.wasm' with { type: 'file' } import { fontBuffers } from '../atoms/fonts/index.ts' -import { themeModes, type WebTheme } from '../atoms/theme.ts' +import { webThemes, type WebTheme } from '../atoms/theme.ts' import { renderCover, type CoverPayload } from './cover.ts' -export const coverThemes = themeModes.filter(theme => theme !== 'auto') +export const coverThemes = webThemes export type CoverImages = Record> let ready: Promise | undefined diff --git a/src/viewers/web/startup/page.ts b/src/viewers/web/startup/page.ts index 9a3e74fa0..945bc9ce0 100644 --- a/src/viewers/web/startup/page.ts +++ b/src/viewers/web/startup/page.ts @@ -6,7 +6,7 @@ import { scannerSelection, scannerSelectionScript } from './scanners.ts' import type { GromaInitResult } from '../../../initialize.ts' import { escaped } from '../atoms/escape.ts' -import { cssBlock, palettes, themeCss, themeModes } from '../atoms/theme.ts' +import { cssBlock, palettes, themeCss, webThemes } from '../atoms/theme.ts' import { chromeCss } from '../atoms/chrome.ts' import { startupUpdate, type StartupProgress } from './progress.ts' @@ -26,6 +26,7 @@ const style = ` :root { ${cssBlock(palettes.light)} color-scheme: light; } @media (prefers-color-scheme: dark) { :root { ${cssBlock(palettes.dark)} color-scheme: dark; } } ${themeCss()} + ${webThemes.map(theme => `[data-theme="${theme}"] { color-scheme: ${palettes[theme].colourScheme}; }`).join('\n')} * { box-sizing: border-box; } body { margin: 0; min-height: 100dvh; padding: 32px 20px; @@ -154,7 +155,7 @@ const style = ` const script = ` ${scannerSelectionScript} const theme = localStorage.getItem('groma.theme'); - if (${JSON.stringify(themeModes.filter(mode => mode !== 'auto'))}.includes(theme)) document.documentElement.dataset.theme = theme; + if (${JSON.stringify(webThemes)}.includes(theme)) document.documentElement.dataset.theme = theme; const main = document.querySelector('main'); const content = document.querySelector('#startup-content'); const progress = document.querySelector('#startup-progress'); diff --git a/test-bun/web-sharing.test.ts b/test-bun/web-sharing.test.ts index 6e23b9aa5..1fab446aa 100644 --- a/test-bun/web-sharing.test.ts +++ b/test-bun/web-sharing.test.ts @@ -8,7 +8,6 @@ import { loadProjectProfile, saveProjectProfile } from '../src/project-profile.t import { exportWebViewer } from '../src/viewers/web/export.ts' import { startWebViewer } from '../src/viewers/web/server.ts' import { sharingMetadata } from '../src/viewers/web/sharing/metadata.ts' -import { coverThemes } from '../src/viewers/web/sharing/images.ts' const fixture = path.resolve(import.meta.dir, '../test/fixtures/empty-project') const mappedFixture = path.resolve(import.meta.dir, '../test/fixtures/source-view') @@ -51,7 +50,7 @@ test.concurrent('export writes the themed image referenced by its initial sharin assert.equal(fields.get('og:image:height'), '630') assert.ok(fields.get('og:image:alt')) assert.equal(fields.has('og:description'), false) - for (const theme of coverThemes) { + for (const theme of ['light', 'dark', 'blueprint', 'sand']) { assertCover(await readFile(path.join(output, `cover-${theme}.png`))) } } finally { @@ -67,7 +66,7 @@ test.concurrent('live initial metadata uses the project profile and links to a t const project = await loadProjectProfile(root) await saveProjectProfile(root, { ...project!, title: 'Shared project', description: 'Tracks submitted requests.' }) viewer = await startWebViewer(root, { port: 0, workSource: emptyWork() }) - for (const theme of coverThemes) { + for (const theme of ['light', 'dark', 'blueprint', 'sand']) { const url: string = `${viewer.url}/?theme=${theme}` const fields = await headMetadata(await (await fetch(url)).text()) assert.equal(fields.get('og:title'), 'Shared project') diff --git a/test-bun/web-theme.test.ts b/test-bun/web-theme.test.ts index 967abf9a4..c8cd8166c 100644 --- a/test-bun/web-theme.test.ts +++ b/test-bun/web-theme.test.ts @@ -5,13 +5,12 @@ import { test } from 'bun:test' import { loadAnnotatedArchitecture } from '../src/core.ts' import { readView, writeView } from '../src/viewers/web/url.ts' -import { themeModes } from '../src/viewers/web/atoms/theme.ts' const fixture = path.resolve(import.meta.dir, '../test/fixtures/empty-project') test.concurrent('publication paths select a theme before the saved browser preference', async () => { const world = await loadAnnotatedArchitecture(fixture) - for (const theme of themeModes) { + for (const theme of ['auto', 'light', 'dark', 'blueprint', 'sand']) { const url = new URL(`https://example.test/repository/architecture/${theme}/`) assert.equal(readView(url, world, [], [], 'light').theme, theme) } @@ -38,7 +37,7 @@ test.concurrent('theme choices survive a shared URL including Auto on a Blueprin const world = await loadAnnotatedArchitecture(fixture) const url = new URL('https://example.test/repository/architecture/blueprint/') const opened = readView(url, world, [], [], 'dark') - for (const theme of themeModes) { + for (const theme of ['auto', 'light', 'dark', 'blueprint', 'sand'] as const) { url.search = writeView({ ...opened, theme }, world, [], url.pathname) assert.equal(readView(url, world, [], [], 'dark').theme, theme) assert.equal(url.searchParams.get('theme'), theme === 'blueprint' ? null : theme) From b8bb32e5f04faa7043596bd6680e37d25a7358ea Mon Sep 17 00:00:00 2001 From: Alex Gavrilescu Date: Sun, 11 Oct 2026 18:11:34 +0200 Subject: [PATCH 4/5] TASK-590 - Add six web colour themes --- backlog/tasks/task-590 - Add-six-web-colour-themes.md | 1 + 1 file changed, 1 insertion(+) diff --git a/backlog/tasks/task-590 - Add-six-web-colour-themes.md b/backlog/tasks/task-590 - Add-six-web-colour-themes.md index c3a2e033c..9b12c80b4 100644 --- a/backlog/tasks/task-590 - Add-six-web-colour-themes.md +++ b/backlog/tasks/task-590 - Add-six-web-colour-themes.md @@ -19,6 +19,7 @@ modified_files: - test-bun/web-sharing.test.ts - docs/viewers/web/index.md - src/viewers/web/url.ts + - src/viewers/web/sharing/images.ts ordinal: 640000 --- From c7c1bc0e78666beb1ae621a897664712ef405c36 Mon Sep 17 00:00:00 2001 From: Alex Gavrilescu Date: Sun, 11 Oct 2026 18:16:45 +0200 Subject: [PATCH 5/5] TASK-590 - Add six web colour themes --- backlog/tasks/task-590 - Add-six-web-colour-themes.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/backlog/tasks/task-590 - Add-six-web-colour-themes.md b/backlog/tasks/task-590 - Add-six-web-colour-themes.md index 9b12c80b4..5ffaad937 100644 --- a/backlog/tasks/task-590 - Add-six-web-colour-themes.md +++ b/backlog/tasks/task-590 - Add-six-web-colour-themes.md @@ -1,11 +1,11 @@ --- id: TASK-590 title: Add six web colour themes -status: In Progress +status: Done assignee: - '@codex' created_date: '2026-10-07 17:00' -updated_date: '2026-10-11 16:11' +updated_date: '2026-10-11 16:16' labels: [] dependencies: [] references: @@ -60,6 +60,8 @@ Focused viewer checks: 14 pass, 0 fail with the repository runner options. Chrom Renamed the electric-blue/cyan palette to Electric at the owner request; removed product references from public code, documentation, task description and PR. All 14 focused tests and browser checks for all 9 palettes pass after renaming. Website export rebuilt; 57 site tests pass. Maintainer pass, 11 October 2026: merged main (Bun 1.4.3, OpenTUI 0.6.0) and renumbered this record from TASK-557, which main already uses. A cold simplicity review led to these changes: the theme names are one list (webThemes) that the menu, URL values, page CSS and cover images all read; cssBlock no longer emits color-scheme, so the main viewer keeps its behaviour for Light, Dark and Blueprint and only the startup screen sets it, as before; the theme menu marks are typed without casts; the theme tests name their themes literally again and include one new theme (sand). + +Maintainer verification, 11 October 2026, on Bun 1.4.3 with main merged: bun run check passed (Biome: one existing warning and two infos outside the changed files; TypeScript passed; 16 Node tests; 780 Bun tests passed, 52 skipped for missing optional toolchains, 0 failed). Live viewer on a fixture project: the theme menu lists ten choices, the four original ones with their icons and the six new ones with a dot in their highlight colour; theme=sand and theme=electric apply their palettes from the URL; the viewer's document colour scheme is unchanged from main; no console errors. Open for the owner: every export now writes nine cover images instead of three (about 2.2 s and 3.8 MB against 0.75 s and 1.2 MB on this repository's map), and the new palettes use their own highlight colour for selection instead of the brand green. Two commits in this branch's history contain a third-party product name; squash-merge to keep it out of main. ## Final Summary