From bd246644c962f754a8477353d49ed39ba9b87ffd Mon Sep 17 00:00:00 2001 From: Guilhem Date: Tue, 28 Jul 2026 18:34:41 +0200 Subject: [PATCH] feat: add github dark mode variant switchable in user settings (#10002) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: add github dark mode variant switchable in user settings Co-Authored-By: Claude Opus 4.8 (1M context) * feat: boot the UI Builder iframe into the current theme Seed the UI Builder iframe URL with the live dark/variant state (`?dark=&variant=`) so the VS Code workbench boots straight into GitHub Dark / Nord / Light instead of flashing the dark default until the host's `setDarkMode` message lands. Read from the classes rather than the reactive state so the src is computed once at mount — a later theme toggle still updates the workbench via postMessage and does not reload the iframe. Co-Authored-By: Claude Opus 4.8 * feat: align github-dark surfaces to GitHub Primer's elevation scale Snap the invented mid-greys to Primer's real dark canvas scale (inset=neutral.0, default=neutral.1, muted=neutral.2). Recessed surfaces now use true inset (#010409) instead of washed-out half-steps that sat in no-man's-land between inset and the page; raised surfaces align to muted. - surface-secondary #0b0e13 → #010409 (true inset) - surface-input #0c0f14 → #010409 (true inset) - surface-disabled #0b0e13 → #151b23 (muted) - surface-tertiary #161b22 → #151b23 (Primer neutral.2) - component-virtual-node #161b22 → #151b23 - surface-selected #21262d → #212830 (Primer neutral.3) surface-sunken (#010409) and surface-primary (#0d1117) already matched Primer inset/default exactly. Co-Authored-By: Claude Opus 4.8 * fix: keep github-dark inputs subtle instead of full inset surface-input at true inset (#010409) made form fields read as recessed as the sidebar rail — too much contrast against the canvas. Inputs aren't sunken, so keep the subtle #0c0f14 (a hair below the page) while the recessed panel surfaces stay at inset. Co-Authored-By: Claude Opus 4.8 * chore: bump UI Builder pin to the github-dark build (76ee616) windmill-code-ui-builder PR #16 is merged and published to R2, so pin the artifact to that build. The embedded raw-app editor now renders GitHub Dark (and honors the `variant` message) natively from the pinned build — no local swap needed. Reword the variant comment now that the dependency is resolved. Co-Authored-By: Claude Opus 4.8 * fix: address codex review nits on the github-dark theme - UserSettings: re-read the dark variant when the settings drawer opens, so a change made through one mounted instance (page-local drawer) isn't shown stale by another (the always-mounted layout instance). - PublicApp / OAuth login callback: restore the `github-dark` variant class, not just the base `dark` class. These routes bypass the (root) layout, so they previously fell back to the default dark palette despite the saved preference. - RawAppEditor: condense the two new theme comments to the durable constraints per AGENTS.md (drop narration and the ephemeral artifact-pin history). Co-Authored-By: Claude Opus 4.8 * fix: address CI review findings on the github-dark theme - Move the hand-authored `github-dark` token set out of the Figma-generated tokens.json into githubDark.json, merged back in at the two consumers (tailwind.config.cjs before the rgb pass, utils.ts) so a Figma re-export of tokens.json can no longer drop the set and crash the build. - Restore the default-dark sidebar divider to #374151 — the PR must leave the Default variant unchanged; only the github variant adapts it to border-light. - Extract the `github-dark` DOM-class read into getAppliedDarkModeVariant() and reuse it across RawAppEditor and vscode.ts; drop the redundant `void darkVariant`. - Override the Monaco popup vars (suggest/hover widgets) in the github variant so they match the GitHub palette instead of VS Code's greys. Co-Authored-By: Claude Opus 4.8 --------- Co-authored-by: Claude Opus 4.8 (1M context) Co-authored-by: Ruben Fiszel --- frontend/scripts/ui_builder_artifact.json | 4 +- .../src/lib/assets/tokens/githubDark.json | 35 +++++++++++ frontend/src/lib/assets/tokens/tokens.json | 7 ++- .../src/lib/components/EditorTheme.svelte | 8 +-- .../src/lib/components/ScriptEditor.svelte | 2 +- .../src/lib/components/UserSettings.svelte | 38 +++++++++++- .../components/apps/editor/PublicApp.svelte | 3 + .../components/raw_apps/RawAppEditor.svelte | 35 +++++++++-- .../lib/components/sidebar/sidebarChrome.ts | 9 +-- frontend/src/lib/components/vscode.ts | 57 ++++++++++++++++-- frontend/src/lib/darkModeVariant.ts | 27 +++++++++ frontend/src/lib/utils.ts | 7 ++- .../src/routes/(root)/(logged)/+layout.svelte | 2 +- frontend/src/routes/(root)/+layout.svelte | 3 + .../login_callback/[client_name]/+page.svelte | 3 + frontend/tailwind.config.cjs | 59 ++++++++++++++----- 16 files changed, 254 insertions(+), 45 deletions(-) create mode 100644 frontend/src/lib/assets/tokens/githubDark.json create mode 100644 frontend/src/lib/darkModeVariant.ts diff --git a/frontend/scripts/ui_builder_artifact.json b/frontend/scripts/ui_builder_artifact.json index 4360794dbe..01cfbb2888 100644 --- a/frontend/scripts/ui_builder_artifact.json +++ b/frontend/scripts/ui_builder_artifact.json @@ -1,5 +1,5 @@ { "baseUrl": "https://pub-06154ed168a24e73a86ab84db6bf15d8.r2.dev", - "version": "1f1fe4f", - "sha256": "4c20b2b51f324e93dda3b46d914ebf7bc7d0cb4dc7eb2d37068408191220d206" + "version": "76ee616", + "sha256": "1d696d38a152179ef6e1cada5df302d078cc677b6dbc244b5c42335e11feabac" } diff --git a/frontend/src/lib/assets/tokens/githubDark.json b/frontend/src/lib/assets/tokens/githubDark.json new file mode 100644 index 0000000000..ea5b36145e --- /dev/null +++ b/frontend/src/lib/assets/tokens/githubDark.json @@ -0,0 +1,35 @@ +{ + "surface-accent-primary": "#506ab8", + "surface-accent-hover": "#5873c8", + "surface-accent-clicked": "#425797", + "text-primary": "#d5dbe2", + "text-secondary": "#8b949e", + "text-primary-inverse": "#24292f", + "text-secondary-inverse": "#57606a", + "text-tertiary-inverse": "#6e7781", + "surface-selected": "#212830", + "surface-disabled": "#151b23", + "surface-secondary": "#010409", + "surface-hover": "#6e768119", + "surface-primary": "#0d1117", + "border-light": "#242a32", + "border-normal": "#6e7681", + "border-accent": "#4a6ed6", + "surface-accent-selected": "#141a2e", + "surface-accent-secondary": "#e3e8ee", + "surface-tertiary": "#151b23", + "text-emphasis": "#dfe5eb", + "text-hint": "#6e7681", + "text-disabled": "#484f58", + "surface-accent-secondary-hover": "#d0d7de", + "surface-accent-secondary-clicked": "#afb8c1", + "component-button-accent-secondary": "#f0f6fc", + "text-emphasis-inverse": "#1f2328", + "reserved-ai": "#d2a8ff", + "component-virtual-node": "#151b23", + "text-accent": "#bdc8ee", + "border-selected": "#5a72c4", + "surface-sunken": "#010409", + "text-tertiary": "#6e7681", + "surface-input": "#0c0f14" +} diff --git a/frontend/src/lib/assets/tokens/tokens.json b/frontend/src/lib/assets/tokens/tokens.json index f4f35c81d9..349b4376b8 100644 --- a/frontend/src/lib/assets/tokens/tokens.json +++ b/frontend/src/lib/assets/tokens/tokens.json @@ -239,7 +239,12 @@ "magenta-950": "#6e2ba1" } }, - "guidelines": { "mode-1": { "blue": "#5e81ac", "demo-background": "#ffffff00" } }, + "guidelines": { + "mode-1": { + "blue": "#5e81ac", + "demo-background": "#ffffff00" + } + }, "tailwind-c-s-s-v-3-3-2": { "mode-1": { "black": "#000000", diff --git a/frontend/src/lib/components/EditorTheme.svelte b/frontend/src/lib/components/EditorTheme.svelte index 9b24acab45..ba123e3dd5 100644 --- a/frontend/src/lib/components/EditorTheme.svelte +++ b/frontend/src/lib/components/EditorTheme.svelte @@ -1,5 +1,5 @@ - + (darkVariant = getDarkModeVariant())} + on:close={removeHash} + {disableChatOffset} +>
{#if scopes == undefined}
-
- Theme +
+
+ Theme +
+
+ Dark variant + { + darkVariant = v + setDarkModeVariant(v) + }} + > + {#snippet children({ item })} + + + {/snippet} + +
Windmill diff --git a/frontend/src/lib/components/apps/editor/PublicApp.svelte b/frontend/src/lib/components/apps/editor/PublicApp.svelte index 8995684e28..c0cf693e0f 100644 --- a/frontend/src/lib/components/apps/editor/PublicApp.svelte +++ b/frontend/src/lib/components/apps/editor/PublicApp.svelte @@ -1,5 +1,6 @@ - + { + darkVariant = getAppliedDarkModeVariant() + }} +/> diff --git a/frontend/src/lib/components/sidebar/sidebarChrome.ts b/frontend/src/lib/components/sidebar/sidebarChrome.ts index 110435c058..71ff703d27 100644 --- a/frontend/src/lib/components/sidebar/sidebarChrome.ts +++ b/frontend/src/lib/components/sidebar/sidebarChrome.ts @@ -1,5 +1,6 @@ -// Sidebar rail background. Deliberately raw hex — no theme token exists for -// the rail chrome — kept here as the single source for every host that renders -// or mimics the sidebar (app layout, kitchen-sink harness). +// Sidebar rail background. Kept here as the single source for every host that +// renders or mimics the sidebar (app layout, kitchen-sink harness). +// The dark value is a theme-driven CSS variable so it adapts across dark +// variants (default vs GitHub dark) — see tailwind.config.cjs `--sidebar-bg-dark`. export const SIDEBAR_BG = '#F3F3F7' -export const SIDEBAR_BG_DARK = '#1e232e' +export const SIDEBAR_BG_DARK = 'var(--sidebar-bg-dark)' diff --git a/frontend/src/lib/components/vscode.ts b/frontend/src/lib/components/vscode.ts index 1277ac1b4a..f30ffcd780 100644 --- a/frontend/src/lib/components/vscode.ts +++ b/frontend/src/lib/components/vscode.ts @@ -1,6 +1,7 @@ import '@codingame/monaco-vscode-standalone-typescript-language-features' import { editor as meditor, Uri as mUri } from 'monaco-editor' +import { getAppliedDarkModeVariant } from '$lib/darkModeVariant' export let isInitialized = false export let isInitializing = false @@ -12,6 +13,15 @@ import { import getLanguagesServiceOverride from '@codingame/monaco-vscode-languages-service-override' import { getCssColor } from '$lib/utils' +// Monaco theme name matching the current document theme (light / dark / GitHub dark variant). +export function getEditorTheme(): string { + const classes = document.documentElement.classList + if (!classes.contains('dark')) { + return 'myTheme' + } + return getAppliedDarkModeVariant() === 'github' ? 'github-dark' : 'nord' +} + export function buildWorkerDefinition() { const envEnhanced = getEnhancedMonacoEnvironment() const getWorker = (moduleId: string, label: string) => { @@ -248,6 +258,47 @@ export async function initializeVscode(caller?: string, htmlContainer?: HTMLElem } }) + meditor.defineTheme('github-dark', { + base: 'vs-dark', + inherit: true, + rules: [ + { background: '0D1117', token: '' }, + { foreground: '8b949e', token: 'comment' }, + { foreground: 'a5d6ff', token: 'string' }, + { foreground: '79c0ff', token: 'constant.numeric' }, + { foreground: '79c0ff', token: 'constant.language' }, + { foreground: 'ff7b72', token: 'keyword' }, + { foreground: 'ff7b72', token: 'storage' }, + { foreground: 'ff7b72', token: 'storage.type' }, + { foreground: 'ffa657', token: 'entity.name.class' }, + { foreground: 'ffa657', fontStyle: 'bold', token: 'entity.other.inherited-class' }, + { foreground: 'd2a8ff', token: 'entity.name.function' }, + { foreground: '7ee787', token: 'entity.name.tag' }, + { foreground: '79c0ff', token: 'entity.other.attribute-name' }, + { foreground: 'd2a8ff', token: 'support.function' }, + { foreground: 'f0f6fc', background: 'f85149', token: 'invalid' }, + { foreground: 'f0f6fc', background: 'bd561d', token: 'invalid.deprecated' }, + { foreground: '79c0ff', token: 'constant.color.other.rgb-value' }, + { foreground: '79c0ff', token: 'constant.character.escape' }, + { foreground: 'ffa657', token: 'variable.other.constant' }, + { token: 'string.value.json', foreground: 'a5d6ff' }, // string values in JSON + { token: 'keyword.json', foreground: '79c0ff' } // true, false, null in JSON + ], + colors: { + 'editor.foreground': '#e6edf3', + 'editor.background': getCssColor('surface-input', { format: 'hex-github-dark' }), + 'editor.selectionBackground': '#3392FF44', + 'editor.inactiveSelectionBackground': '#3392FF22', + 'editor.lineHighlightBackground': '#161b22', + 'editorCursor.foreground': '#e6edf3', + 'editorWhitespace.foreground': '#484f58', + 'editorIndentGuide.background1': '#21262d', + 'editorIndentGuide.activeBackground1': '#30363d', + 'editorLineNumber.foreground': '#6e7681', + 'editorLineNumber.activeForeground': '#e6edf3' + } + }) + meditor.defineTheme('myTheme', { base: 'vs', inherit: true, @@ -262,11 +313,7 @@ export async function initializeVscode(caller?: string, htmlContainer?: HTMLElem } }) - if (document.documentElement.classList.contains('dark')) { - meditor.setTheme('nord') - } else { - meditor.setTheme('myTheme') - } + meditor.setTheme(getEditorTheme()) } catch (e) { console.error('Failed to initialize monaco services', e) } finally { diff --git a/frontend/src/lib/darkModeVariant.ts b/frontend/src/lib/darkModeVariant.ts new file mode 100644 index 0000000000..1e1bbff28b --- /dev/null +++ b/frontend/src/lib/darkModeVariant.ts @@ -0,0 +1,27 @@ +export type DarkModeVariant = 'default' | 'github' + +const DARK_MODE_VARIANT_KEY = 'dark-mode-variant' + +export function getDarkModeVariant(): DarkModeVariant { + return window.localStorage.getItem(DARK_MODE_VARIANT_KEY) === 'github' ? 'github' : 'default' +} + +// The variant currently applied to the DOM (the `github-dark` class) — the +// runtime source of truth, as opposed to the persisted preference above. +export function getAppliedDarkModeVariant(): DarkModeVariant { + return typeof document !== 'undefined' && + document.documentElement.classList.contains('github-dark') + ? 'github' + : 'default' +} + +// The `github-dark` class only takes effect when `dark` is also present (see tailwind.config.cjs). +// It lives on permanently so it survives the many independent `dark` toggle sites. +export function applyDarkModeVariant(variant: DarkModeVariant = getDarkModeVariant()): void { + document.documentElement.classList.toggle('github-dark', variant === 'github') +} + +export function setDarkModeVariant(variant: DarkModeVariant): void { + window.localStorage.setItem(DARK_MODE_VARIANT_KEY, variant) + applyDarkModeVariant(variant) +} diff --git a/frontend/src/lib/utils.ts b/frontend/src/lib/utils.ts index 28d33335ae..e9acfdacab 100644 --- a/frontend/src/lib/utils.ts +++ b/frontend/src/lib/utils.ts @@ -2029,6 +2029,8 @@ export function validateRetryConfig(retry: Retry | undefined): string | null { } export type CssColor = keyof (typeof tokensFile)['tokens']['light'] import tokensFile from './assets/tokens/tokens.json' +// Hand-authored variant kept out of the Figma-generated tokens.json. +import githubDarkTokens from './assets/tokens/githubDark.json' import { darkModeName, lightModeName } from './assets/tokens/colorTokensConfig' import BarsStaggered from './components/icons/BarsStaggered.svelte' import { GitIcon } from './components/icons' @@ -2041,7 +2043,7 @@ export function getCssColor( format = 'css-var' }: { alpha?: number - format?: 'css-var' | 'hex-dark' | 'hex-light' + format?: 'css-var' | 'hex-dark' | 'hex-light' | 'hex-github-dark' } ): string { if (format === 'hex-light') { @@ -2050,6 +2052,9 @@ export function getCssColor( if (format === 'hex-dark') { return tokensFile.tokens[darkModeName][color] } + if (format === 'hex-github-dark') { + return githubDarkTokens[color] + } return `rgb(var(--color-${color}) / ${alpha})` } diff --git a/frontend/src/routes/(root)/(logged)/+layout.svelte b/frontend/src/routes/(root)/(logged)/+layout.svelte index 7dbe5ca376..9de1123ff9 100644 --- a/frontend/src/routes/(root)/(logged)/+layout.svelte +++ b/frontend/src/routes/(root)/(logged)/+layout.svelte @@ -1052,7 +1052,7 @@ style:width="{railWidth}rem" >
{#if !isCollapsed} diff --git a/frontend/src/routes/(root)/+layout.svelte b/frontend/src/routes/(root)/+layout.svelte index 8b7736965d..3db5efcf60 100644 --- a/frontend/src/routes/(root)/+layout.svelte +++ b/frontend/src/routes/(root)/+layout.svelte @@ -19,6 +19,7 @@ // import EditorTheme from '$lib/components/EditorTheme.svelte' import { computeDrift } from '$lib/forLater' import { setLicense } from '$lib/enterpriseUtils' + import { applyDarkModeVariant } from '$lib/darkModeVariant' import { deepEqual } from 'fast-equals' interface Props { children?: import('svelte').Snippet @@ -255,6 +256,8 @@ } else { document.documentElement.classList.remove('dark') } + + applyDarkModeVariant() {@render children?.()} diff --git a/frontend/src/routes/user/login_callback/[client_name]/+page.svelte b/frontend/src/routes/user/login_callback/[client_name]/+page.svelte index 737da14321..1aeb57f223 100644 --- a/frontend/src/routes/user/login_callback/[client_name]/+page.svelte +++ b/frontend/src/routes/user/login_callback/[client_name]/+page.svelte @@ -1,5 +1,6 @@