33import { useEffect , useState } from "react"
44import type { BeforeMount } from "@monaco-editor/react"
55import type { editor as MonacoEditorNs } from "monaco-editor"
6+ import {
7+ THEME_COLORS ,
8+ DEFAULT_THEME_COLOR ,
9+ type ThemeColor ,
10+ } from "./theme-presets"
611
7- export const MONACO_LIGHT_THEME = "codeg-light"
8- export const MONACO_DARK_THEME = "codeg-dark"
12+ // Editor canvas background per theme color = that theme's `--card` token (see the
13+ // [data-theme="…"] blocks in globals.css). The file editor is a card surface, so it
14+ // stays pure white for the neutral/gray presets in light mode and #171717
15+ // (= oklch(0.205 0 0)) for neutral dark — exactly today's values — while the accent
16+ // presets carry their hue. Keep in sync with the --card values in globals.css.
17+ export const EDITOR_CANVAS_BG : Record <
18+ ThemeColor ,
19+ { light : string ; dark : string }
20+ > = {
21+ neutral : { light : "#ffffff" , dark : "#171717" } ,
22+ zinc : { light : "#ffffff" , dark : "#18181b" } ,
23+ slate : { light : "#ffffff" , dark : "#0f172b" } ,
24+ stone : { light : "#ffffff" , dark : "#1c1917" } ,
25+ gray : { light : "#ffffff" , dark : "#101828" } ,
26+ red : { light : "#fffcfc" , dark : "#1d1514" } ,
27+ rose : { light : "#fffcfc" , dark : "#1d1515" } ,
28+ orange : { light : "#fffcfb" , dark : "#1d1512" } ,
29+ green : { light : "#fbfefc" , dark : "#131914" } ,
30+ blue : { light : "#fcfdff" , dark : "#14171e" } ,
31+ yellow : { light : "#fefdfa" , dark : "#1a1710" } ,
32+ violet : { light : "#fdfdff" , dark : "#17161d" } ,
33+ }
34+
35+ // A Monaco theme name encodes both axes: light/dark mode and the active theme color.
36+ // `defineMonacoThemes` registers one per (color, mode); `useMonacoThemeSync` returns
37+ // the matching name so flipping either axis re-applies through the editor `theme` prop.
38+ export function monacoThemeName ( color : ThemeColor , dark : boolean ) : string {
39+ return `codeg-${ dark ? "dark" : "light" } -${ color } `
40+ }
41+
42+ // Neutral-preset names, exported as the stable defaults (e.g. the initial value
43+ // before the DOM is read).
44+ export const MONACO_LIGHT_THEME = monacoThemeName ( DEFAULT_THEME_COLOR , false )
45+ export const MONACO_DARK_THEME = monacoThemeName ( DEFAULT_THEME_COLOR , true )
946
1047// Monaco's "unicode highlight" feature boxes characters it deems ambiguous with
1148// ASCII or non-basic-ASCII. Its default flags ordinary CJK full-width
@@ -442,24 +479,46 @@ export const configureLanguageValidation: BeforeMount = (monaco) => {
442479 }
443480}
444481
482+ // Overlay the theme color's canvas background onto the base surface colors so the
483+ // editor / gutter / peek surfaces read as the app's card instead of a fixed
484+ // white/black. Only the opaque canvas is themed; widgets, selection and the
485+ // line-highlight band stay on their neutral shadcn values.
486+ function withCanvasBackground (
487+ base : ( typeof monacoThemeColors ) [ "light" ] ,
488+ color : ThemeColor ,
489+ dark : boolean
490+ ) : Record < string , string > {
491+ const bg = EDITOR_CANVAS_BG [ color ] [ dark ? "dark" : "light" ]
492+ return {
493+ ...base ,
494+ "editor.background" : bg ,
495+ "editorGutter.background" : bg ,
496+ "peekViewEditor.background" : bg ,
497+ "peekViewEditorGutter.background" : bg ,
498+ }
499+ }
500+
445501export const defineMonacoThemes : BeforeMount = ( monaco ) => {
446502 defineDiffLanguage ( monaco )
447503 fixPythonTripleQuotes ( monaco )
448504 configureLanguageValidation ( monaco )
449505
450- monaco . editor . defineTheme ( MONACO_LIGHT_THEME , {
451- base : "vs" ,
452- inherit : true ,
453- rules : monacoTokenRules . light ,
454- colors : monacoThemeColors . light ,
455- } )
456-
457- monaco . editor . defineTheme ( MONACO_DARK_THEME , {
458- base : "vs-dark" ,
459- inherit : true ,
460- rules : monacoTokenRules . dark ,
461- colors : monacoThemeColors . dark ,
462- } )
506+ // One theme per (color, mode). `defineTheme` just stores a config, so this is
507+ // cheap and idempotent even re-run on every editor mount.
508+ for ( const color of THEME_COLORS ) {
509+ monaco . editor . defineTheme ( monacoThemeName ( color , false ) , {
510+ base : "vs" ,
511+ inherit : true ,
512+ rules : monacoTokenRules . light ,
513+ colors : withCanvasBackground ( monacoThemeColors . light , color , false ) ,
514+ } )
515+ monaco . editor . defineTheme ( monacoThemeName ( color , true ) , {
516+ base : "vs-dark" ,
517+ inherit : true ,
518+ rules : monacoTokenRules . dark ,
519+ colors : withCanvasBackground ( monacoThemeColors . dark , color , true ) ,
520+ } )
521+ }
463522}
464523
465524export function useMonacoThemeSync ( ) {
@@ -470,17 +529,22 @@ export function useMonacoThemeSync() {
470529 const root = document . documentElement
471530
472531 const syncTheme = ( ) => {
473- setTheme (
474- root . classList . contains ( "dark" ) ? MONACO_DARK_THEME : MONACO_LIGHT_THEME
475- )
532+ const dark = root . classList . contains ( "dark" )
533+ const attr = root . getAttribute ( "data-theme" )
534+ const color = ( THEME_COLORS as readonly string [ ] ) . includes ( attr ?? "" )
535+ ? ( attr as ThemeColor )
536+ : DEFAULT_THEME_COLOR
537+ setTheme ( monacoThemeName ( color , dark ) )
476538 }
477539
478540 syncTheme ( )
479541
542+ // Watch both axes: `class` (light/dark via next-themes) and `data-theme`
543+ // (theme color via AppearanceProvider). Either flip re-applies the matching theme.
480544 const observer = new MutationObserver ( syncTheme )
481545 observer . observe ( root , {
482546 attributes : true ,
483- attributeFilter : [ "class" ] ,
547+ attributeFilter : [ "class" , "data-theme" ] ,
484548 } )
485549 return ( ) => {
486550 observer . disconnect ( )
0 commit comments