From 115d91812e4bc25a56126f23b4ad13b07d451552 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=8A=E3=81=95=E3=82=80=E3=81=AE=E3=81=B2=E3=81=A8?=
 <46447427+samunohito@users.noreply.github.com>
Date: Wed, 20 Mar 2024 10:30:45 +0900
Subject: [PATCH 1/3] =?UTF-8?q?fix(frontend):=20shiki=E3=81=AE=E8=A8=80?=
 =?UTF-8?q?=E8=AA=9E=E3=83=BB=E3=83=86=E3=83=BC=E3=83=9E=E3=81=AE=E5=AE=9A?=
 =?UTF-8?q?=E7=BE=A9=E3=83=95=E3=82=A1=E3=82=A4=E3=83=AB=E3=82=92CDN(esm.s?=
 =?UTF-8?q?h)=E3=81=8B=E3=82=89=E5=8F=96=E3=82=8B=E3=82=88=E3=81=86?=
 =?UTF-8?q?=E3=81=AB=E3=81=99=E3=82=8B=20(#13598)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* fix(frontend): shikiの言語・テーマの定義ファイルをCDN(esm.sh)から取るようにする

* fix CHANGELOG.md
---
 CHANGELOG.md                                  |  2 ++
 packages/frontend/package.json                |  2 +-
 .../frontend/src/components/MkCode.core.vue   | 10 +++----
 packages/frontend/src/index.html              |  2 +-
 .../frontend/src/scripts/code-highlighter.ts  | 18 ++++++------
 packages/frontend/src/scripts/theme.ts        |  4 +--
 packages/frontend/src/store.ts                |  1 -
 packages/frontend/vite.config.ts              | 29 +++++++++++++++++++
 pnpm-lock.yaml                                | 20 ++++++++-----
 9 files changed, 61 insertions(+), 27 deletions(-)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index d948127d06..18dd07f1c3 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -18,6 +18,8 @@
   (Cherry-picked from https://activitypub.software/TransFem-org/Sharkey/-/merge_requests/459)
 - Fix: ページタイトルでローカルユーザーとリモートユーザーの区別がつかない問題を修正  
   (Cherry-picked from https://github.com/MisskeyIO/misskey/pull/528)
+- Fix: コードブロックのシンタックスハイライトで使用される定義ファイルをCDNから取得するように #13177
+  - CDNから取得せずMisskey本体にバンドルする場合は`pacakges/frontend/vite.config.ts`を修正してください。
 
 ### Server
 - Enhance: エンドポイント`antennas/update`の必須項目を`antennaId`のみに
diff --git a/packages/frontend/package.json b/packages/frontend/package.json
index 682def8e8d..db7f7b76f6 100644
--- a/packages/frontend/package.json
+++ b/packages/frontend/package.json
@@ -60,7 +60,7 @@
 		"rollup": "4.12.0",
 		"sanitize-html": "2.12.1",
 		"sass": "1.71.1",
-		"shiki": "1.1.7",
+		"shiki": "1.2.0",
 		"strict-event-emitter-types": "2.0.0",
 		"textarea-caret": "3.1.0",
 		"three": "0.162.0",
diff --git a/packages/frontend/src/components/MkCode.core.vue b/packages/frontend/src/components/MkCode.core.vue
index 872517b6aa..c0e7df5dac 100644
--- a/packages/frontend/src/components/MkCode.core.vue
+++ b/packages/frontend/src/components/MkCode.core.vue
@@ -9,9 +9,9 @@ SPDX-License-Identifier: AGPL-3.0-only
 </template>
 
 <script lang="ts" setup>
-import { ref, computed, watch } from 'vue';
-import { bundledLanguagesInfo } from 'shiki';
-import type { BuiltinLanguage } from 'shiki';
+import { computed, ref, watch } from 'vue';
+import { bundledLanguagesInfo } from 'shiki/langs';
+import type { BundledLanguage } from 'shiki/langs';
 import { getHighlighter, getTheme } from '@/scripts/code-highlighter.js';
 import { defaultStore } from '@/store.js';
 
@@ -23,7 +23,7 @@ const props = defineProps<{
 
 const highlighter = await getHighlighter();
 const darkMode = defaultStore.reactiveState.darkMode;
-const codeLang = ref<BuiltinLanguage | 'aiscript'>('js');
+const codeLang = ref<BundledLanguage | 'aiscript'>('js');
 
 const [lightThemeName, darkThemeName] = await Promise.all([
 	getTheme('light', true),
@@ -42,7 +42,7 @@ const html = computed(() => highlighter.codeToHtml(props.code, {
 }));
 
 async function fetchLanguage(to: string): Promise<void> {
-	const language = to as BuiltinLanguage;
+	const language = to as BundledLanguage;
 
 	// Check for the loaded languages, and load the language if it's not loaded yet.
 	if (!highlighter.getLoadedLanguages().includes(language)) {
diff --git a/packages/frontend/src/index.html b/packages/frontend/src/index.html
index cd84145f40..08ff0c58dd 100644
--- a/packages/frontend/src/index.html
+++ b/packages/frontend/src/index.html
@@ -18,7 +18,7 @@
 		http-equiv="Content-Security-Policy"
 		content="default-src 'self' https://newassets.hcaptcha.com/ https://challenges.cloudflare.com/ http://localhost:7493/;
 			worker-src 'self';
-			script-src 'self' 'unsafe-eval' https://*.hcaptcha.com https://challenges.cloudflare.com;
+			script-src 'self' 'unsafe-eval' https://*.hcaptcha.com https://challenges.cloudflare.com https://esm.sh;
 			style-src 'self' 'unsafe-inline';
 			img-src 'self' data: blob: www.google.com xn--931a.moe localhost:3000 localhost:5173 127.0.0.1:5173 127.0.0.1:3000;
 			media-src 'self' localhost:3000 localhost:5173 127.0.0.1:5173 127.0.0.1:3000;
diff --git a/packages/frontend/src/scripts/code-highlighter.ts b/packages/frontend/src/scripts/code-highlighter.ts
index 5dd0a3be78..e94027d302 100644
--- a/packages/frontend/src/scripts/code-highlighter.ts
+++ b/packages/frontend/src/scripts/code-highlighter.ts
@@ -3,18 +3,19 @@
  * SPDX-License-Identifier: AGPL-3.0-only
  */
 
-import { bundledThemesInfo } from 'shiki';
 import { getHighlighterCore, loadWasm } from 'shiki/core';
 import darkPlus from 'shiki/themes/dark-plus.mjs';
+import { bundledThemesInfo } from 'shiki/themes';
+import { bundledLanguagesInfo } from 'shiki/langs';
 import { unique } from './array.js';
 import { deepClone } from './clone.js';
 import { deepMerge } from './merge.js';
-import type { Highlighter, LanguageRegistration, ThemeRegistration, ThemeRegistrationRaw } from 'shiki';
+import type { HighlighterCore, LanguageRegistration, ThemeRegistration, ThemeRegistrationRaw } from 'shiki/core';
 import { ColdDeviceStorage } from '@/store.js';
 import lightTheme from '@/themes/_light.json5';
 import darkTheme from '@/themes/_dark.json5';
 
-let _highlighter: Highlighter | null = null;
+let _highlighter: HighlighterCore | null = null;
 
 export async function getTheme(mode: 'light' | 'dark', getName: true): Promise<string>;
 export async function getTheme(mode: 'light' | 'dark', getName?: false): Promise<ThemeRegistration | ThemeRegistrationRaw>;
@@ -51,16 +52,14 @@ export async function getTheme(mode: 'light' | 'dark', getName = false): Promise
 	return darkPlus;
 }
 
-export async function getHighlighter(): Promise<Highlighter> {
+export async function getHighlighter(): Promise<HighlighterCore> {
 	if (!_highlighter) {
 		return await initHighlighter();
 	}
 	return _highlighter;
 }
 
-export async function initHighlighter() {
-	const aiScriptGrammar = await import('aiscript-vscode/aiscript/syntaxes/aiscript.tmLanguage.json');
-
+async function initHighlighter() {
 	await loadWasm(import('shiki/onig.wasm?init'));
 
 	// テーマの重複を消す
@@ -69,11 +68,12 @@ export async function initHighlighter() {
 		...(await Promise.all([getTheme('light'), getTheme('dark')])),
 	]);
 
+	const jsLangInfo = bundledLanguagesInfo.find(t => t.id === 'javascript');
 	const highlighter = await getHighlighterCore({
 		themes,
 		langs: [
-			import('shiki/langs/javascript.mjs'),
-			aiScriptGrammar.default as unknown as LanguageRegistration,
+			...(jsLangInfo ? [async () => await jsLangInfo.import()] : []),
+			async () => (await import('aiscript-vscode/aiscript/syntaxes/aiscript.tmLanguage.json')).default as unknown as LanguageRegistration,
 		],
 	});
 
diff --git a/packages/frontend/src/scripts/theme.ts b/packages/frontend/src/scripts/theme.ts
index 5f7e88bd9f..c7f8b3d596 100644
--- a/packages/frontend/src/scripts/theme.ts
+++ b/packages/frontend/src/scripts/theme.ts
@@ -6,7 +6,7 @@
 import { ref } from 'vue';
 import tinycolor from 'tinycolor2';
 import { deepClone } from './clone.js';
-import type { BuiltinTheme } from 'shiki';
+import type { BundledTheme } from 'shiki/themes';
 import { globalEvents } from '@/events.js';
 import lightTheme from '@/themes/_light.json5';
 import darkTheme from '@/themes/_dark.json5';
@@ -20,7 +20,7 @@ export type Theme = {
 	base?: 'dark' | 'light';
 	props: Record<string, string>;
 	codeHighlighter?: {
-		base: BuiltinTheme;
+		base: BundledTheme;
 		overrides?: Record<string, any>;
 	} | {
 		base: '_none_';
diff --git a/packages/frontend/src/store.ts b/packages/frontend/src/store.ts
index a335ed33bb..7742acc60d 100644
--- a/packages/frontend/src/store.ts
+++ b/packages/frontend/src/store.ts
@@ -7,7 +7,6 @@ import { markRaw, ref } from 'vue';
 import * as Misskey from 'misskey-js';
 import { miLocalStorage } from './local-storage.js';
 import type { SoundType } from '@/scripts/sound.js';
-import type { BuiltinTheme as ShikiBuiltinTheme } from 'shiki';
 import { Storage } from '@/pizzax.js';
 import { hemisphere } from '@/scripts/intl-const.js';
 
diff --git a/packages/frontend/vite.config.ts b/packages/frontend/vite.config.ts
index 35d112f6ec..82eb2af464 100644
--- a/packages/frontend/vite.config.ts
+++ b/packages/frontend/vite.config.ts
@@ -5,11 +5,30 @@ import { type UserConfig, defineConfig } from 'vite';
 
 import locales from '../../locales/index.js';
 import meta from '../../package.json';
+import packageInfo from './package.json' assert { type: 'json' };
 import pluginUnwindCssModuleClassName from './lib/rollup-plugin-unwind-css-module-class-name.js';
 import pluginJson5 from './vite.json5.js';
 
 const extensions = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.json', '.json5', '.svg', '.sass', '.scss', '.css', '.vue'];
 
+/**
+ * Misskeyのフロントエンドにバンドルせず、CDNなどから別途読み込むリソースを記述する。
+ * CDNを使わずにバンドルしたい場合、以下の配列から該当要素を削除orコメントアウトすればOK
+ */
+const externalPackages = [
+	// shiki(コードブロックのシンタックスハイライトで使用中)はテーマ・言語の定義の容量が大きいため、それらはCDNから読み込む
+	{
+		name: 'shiki',
+		match: /^shiki\/(?<subPkg>(langs|themes))$/,
+		path(id: string, pattern: RegExp): string {
+			const match = pattern.exec(id)?.groups;
+			return match
+				? `https://esm.sh/shiki@${packageInfo.dependencies.shiki}/${match['subPkg']}`
+				: id;
+		},
+	},
+];
+
 const hash = (str: string, seed = 0): number => {
 	let h1 = 0xdeadbeef ^ seed,
 		h2 = 0x41c6ce57 ^ seed;
@@ -112,6 +131,7 @@ export function getConfig(): UserConfig {
 				input: {
 					app: './src/_boot_.ts',
 				},
+				external: externalPackages.map(p => p.match),
 				output: {
 					manualChunks: {
 						vue: ['vue'],
@@ -119,6 +139,15 @@ export function getConfig(): UserConfig {
 					},
 					chunkFileNames: process.env.NODE_ENV === 'production' ? '[hash:8].js' : '[name]-[hash:8].js',
 					assetFileNames: process.env.NODE_ENV === 'production' ? '[hash:8][extname]' : '[name]-[hash:8][extname]',
+					paths(id) {
+						for (const p of externalPackages) {
+							if (p.match.test(id)) {
+								return p.path(id, p.match);
+							}
+						}
+
+						return id;
+					},
 				},
 			},
 			cssCodeSplit: true,
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 5e29c1162b..2906eef4d8 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -806,8 +806,8 @@ importers:
         specifier: 1.71.1
         version: 1.71.1
       shiki:
-        specifier: 1.1.7
-        version: 1.1.7
+        specifier: 1.2.0
+        version: 1.2.0
       strict-event-emitter-types:
         specifier: 2.0.0
         version: 2.0.0
@@ -5324,8 +5324,8 @@ packages:
       string-argv: 0.3.1
     dev: true
 
-  /@shikijs/core@1.1.7:
-    resolution: {integrity: sha512-gTYLUIuD1UbZp/11qozD3fWpUTuMqPSf3svDMMrL0UmlGU7D9dPw/V1FonwAorCUJBltaaESxq90jrSjQyGixg==}
+  /@shikijs/core@1.2.0:
+    resolution: {integrity: sha512-OlFvx+nyr5C8zpcMBnSGir0YPD6K11uYhouqhNmm1qLiis4GA7SsGtu07r9gKS9omks8RtQqHrJL4S+lqWK01A==}
     dev: false
 
   /@sideway/address@4.1.4:
@@ -6646,7 +6646,7 @@ packages:
       ts-dedent: 2.2.0
       type-fest: 2.19.0
       vue: 3.4.21(typescript@5.3.3)
-      vue-component-type-helpers: 1.8.27
+      vue-component-type-helpers: 2.0.6
     transitivePeerDependencies:
       - encoding
       - supports-color
@@ -17658,10 +17658,10 @@ packages:
     resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==}
     engines: {node: '>=8'}
 
-  /shiki@1.1.7:
-    resolution: {integrity: sha512-9kUTMjZtcPH3i7vHunA6EraTPpPOITYTdA5uMrvsJRexktqP0s7P3s9HVK80b4pP42FRVe03D7fT3NmJv2yYhw==}
+  /shiki@1.2.0:
+    resolution: {integrity: sha512-xLhiTMOIUXCv5DqJ4I70GgQCtdlzsTqFLZWcMHHG3TAieBUbvEGthdrlPDlX4mL/Wszx9C6rEcxU6kMlg4YlxA==}
     dependencies:
-      '@shikijs/core': 1.1.7
+      '@shikijs/core': 1.2.0
     dev: false
 
   /side-channel@1.0.4:
@@ -19445,6 +19445,10 @@ packages:
     resolution: {integrity: sha512-6bnLkn8O0JJyiFSIF0EfCogzeqNXpnjJ0vW/SZzNHfe6sPx30lTtTXlE5TFs2qhJlAtDFybStVNpL73cPe3OMQ==}
     dev: true
 
+  /vue-component-type-helpers@2.0.6:
+    resolution: {integrity: sha512-qdGXCtoBrwqk1BT6r2+1Wcvl583ZVkuSZ3or7Y1O2w5AvWtlvvxwjGhmz5DdPJS9xqRdDlgTJ/38ehWnEi0tFA==}
+    dev: true
+
   /vue-demi@0.14.7(vue@3.4.21):
     resolution: {integrity: sha512-EOG8KXDQNwkJILkx/gPcoL/7vH+hORoBaKgGe+6W7VFMvCYJfmF2dGbvgDroVnI8LU7/kTu8mbjRZGBU1z9NTA==}
     engines: {node: '>=12'}

From d7bb6c88d3e4878486fb1f4d1655379896a5d976 Mon Sep 17 00:00:00 2001
From: Gianni Ceccarelli <dakkar@thenautilus.net>
Date: Wed, 20 Mar 2024 02:25:49 +0000
Subject: [PATCH 2/3] Cypress typescript (#13591)

* convert Cypress tests to TypeScript

this work was done by @lunaisnotaboy https://github.com/lunaisnotaboy
for their fork https://github.com/cutiekey/cutiekey/pull/7

I just repacked their changes into a minimal set

* fix call to `window` in cypress tests

this error was spotted thanks to the TypeScript compiler:

```
support/commands.ts:33:12 - error TS2559: Type '(win: any) => void'
has no properties in common with type 'Partial<Loggable &
Timeoutable>'.

33  cy.window(win => {
              ~~~~~~~~

Found 1 error in support/commands.ts:33
```

(again, @lunaisnotaboy did the actual work)
---
 cypress/e2e/{basic.cy.js => basic.cy.ts}     |  0
 cypress/e2e/{router.cy.js => router.cy.ts}   |  0
 cypress/e2e/{widgets.cy.js => widgets.cy.ts} |  0
 cypress/support/{commands.js => commands.ts} |  2 +-
 cypress/support/{e2e.js => e2e.ts}           |  0
 cypress/support/index.ts                     | 19 +++++++++++++++++++
 cypress/tsconfig.json                        |  8 ++++++++
 package.json                                 |  1 +
 pnpm-lock.yaml                               |  9 +++++++++
 9 files changed, 38 insertions(+), 1 deletion(-)
 rename cypress/e2e/{basic.cy.js => basic.cy.ts} (100%)
 rename cypress/e2e/{router.cy.js => router.cy.ts} (100%)
 rename cypress/e2e/{widgets.cy.js => widgets.cy.ts} (100%)
 rename cypress/support/{commands.js => commands.ts} (98%)
 rename cypress/support/{e2e.js => e2e.ts} (100%)
 create mode 100644 cypress/support/index.ts
 create mode 100644 cypress/tsconfig.json

diff --git a/cypress/e2e/basic.cy.js b/cypress/e2e/basic.cy.ts
similarity index 100%
rename from cypress/e2e/basic.cy.js
rename to cypress/e2e/basic.cy.ts
diff --git a/cypress/e2e/router.cy.js b/cypress/e2e/router.cy.ts
similarity index 100%
rename from cypress/e2e/router.cy.js
rename to cypress/e2e/router.cy.ts
diff --git a/cypress/e2e/widgets.cy.js b/cypress/e2e/widgets.cy.ts
similarity index 100%
rename from cypress/e2e/widgets.cy.js
rename to cypress/e2e/widgets.cy.ts
diff --git a/cypress/support/commands.js b/cypress/support/commands.ts
similarity index 98%
rename from cypress/support/commands.js
rename to cypress/support/commands.ts
index 91a4d7abe6..c2d92e1663 100644
--- a/cypress/support/commands.js
+++ b/cypress/support/commands.ts
@@ -30,7 +30,7 @@ Cypress.Commands.add('visitHome', () => {
 })
 
 Cypress.Commands.add('resetState', () => {
-	cy.window(win => {
+	cy.window().then(win => {
 		win.indexedDB.deleteDatabase('keyval-store');
 	});
 	cy.request('POST', '/api/reset-db', {}).as('reset');
diff --git a/cypress/support/e2e.js b/cypress/support/e2e.ts
similarity index 100%
rename from cypress/support/e2e.js
rename to cypress/support/e2e.ts
diff --git a/cypress/support/index.ts b/cypress/support/index.ts
new file mode 100644
index 0000000000..c1bed21979
--- /dev/null
+++ b/cypress/support/index.ts
@@ -0,0 +1,19 @@
+declare global {
+	namespace Cypress {
+		interface Chainable {
+			login(username: string, password: string): Chainable<void>;
+
+			registerUser(
+				username: string,
+				password: string,
+				isAdmin?: boolean
+			): Chainable<void>;
+
+			resetState(): Chainable<void>;
+
+			visitHome(): Chainable<void>;
+		}
+	}
+}
+
+export {}
diff --git a/cypress/tsconfig.json b/cypress/tsconfig.json
new file mode 100644
index 0000000000..6fe7f32cc4
--- /dev/null
+++ b/cypress/tsconfig.json
@@ -0,0 +1,8 @@
+{
+	"compilerOptions": {
+		"lib": ["dom", "es5"],
+		"target": "es5",
+		"types": ["cypress", "node"]
+	},
+	"include": ["./**/*.ts"]
+}
diff --git a/package.json b/package.json
index 41b865456d..8f5ab0b124 100644
--- a/package.json
+++ b/package.json
@@ -59,6 +59,7 @@
 		"typescript": "5.3.3"
 	},
 	"devDependencies": {
+		"@types/node": "^20.11.28",
 		"@typescript-eslint/eslint-plugin": "7.1.0",
 		"@typescript-eslint/parser": "7.1.0",
 		"cross-env": "7.0.3",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 2906eef4d8..4c1e228a95 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -44,6 +44,9 @@ importers:
         specifier: 4.4.0
         version: 4.4.0
     devDependencies:
+      '@types/node':
+        specifier: ^20.11.28
+        version: 20.11.28
       '@typescript-eslint/eslint-plugin':
         specifier: 7.1.0
         version: 7.1.0(@typescript-eslint/parser@7.1.0)(eslint@8.57.0)(typescript@5.3.3)
@@ -7650,6 +7653,12 @@ packages:
     dependencies:
       undici-types: 5.26.5
 
+  /@types/node@20.11.28:
+    resolution: {integrity: sha512-M/GPWVS2wLkSkNHVeLkrF2fD5Lx5UC4PxA0uZcKc6QqbIQUJyW1jVjueJYi1z8n0I5PxYrtpnPnWglE+y9A0KA==}
+    dependencies:
+      undici-types: 5.26.5
+    dev: true
+
   /@types/node@20.11.5:
     resolution: {integrity: sha512-g557vgQjUUfN76MZAN/dt1z3dzcUsimuysco0KeluHgrPdJXkP/XdAURgyO2W9fZWHRtRBiVKzKn8vyOAwlG+w==}
     dependencies:

From ca2df14a8f4e2d0d7eef699b44a2dd9580842a2a Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E3=81=8B=E3=81=A3=E3=81=93=E3=81=8B=E3=82=8A?=
 <67428053+kakkokari-gtyih@users.noreply.github.com>
Date: Wed, 20 Mar 2024 13:10:09 +0900
Subject: [PATCH 3/3] =?UTF-8?q?fix(frontend):=20woodenPanel=E3=81=AE?=
 =?UTF-8?q?=E9=85=8D=E8=89=B2=E3=82=92=E4=BF=AE=E6=AD=A3=20(#13561)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* fix(frontend): woodenPanelの配色を修正

* fix
---
 packages/frontend/src/boot/common.ts |  3 +++
 packages/frontend/src/style.scss     | 11 ++++++-----
 2 files changed, 9 insertions(+), 5 deletions(-)

diff --git a/packages/frontend/src/boot/common.ts b/packages/frontend/src/boot/common.ts
index 681beaf00f..d86ae18ffe 100644
--- a/packages/frontend/src/boot/common.ts
+++ b/packages/frontend/src/boot/common.ts
@@ -145,8 +145,11 @@ export async function common(createVue: () => App<Element>) {
 	// NOTE: この処理は必ずクライアント更新チェック処理より後に来ること(テーマ再構築のため)
 	watch(defaultStore.reactiveState.darkMode, (darkMode) => {
 		applyTheme(darkMode ? ColdDeviceStorage.get('darkTheme') : ColdDeviceStorage.get('lightTheme'));
+		document.documentElement.dataset.colorMode = darkMode ? 'dark' : 'light';
 	}, { immediate: miLocalStorage.getItem('theme') == null });
 
+	document.documentElement.dataset.colorMode = defaultStore.state.darkMode ? 'dark' : 'light';
+
 	const darkTheme = computed(ColdDeviceStorage.makeGetterSetter('darkTheme'));
 	const lightTheme = computed(ColdDeviceStorage.makeGetterSetter('lightTheme'));
 
diff --git a/packages/frontend/src/style.scss b/packages/frontend/src/style.scss
index 187d902733..250a2616a7 100644
--- a/packages/frontend/src/style.scss
+++ b/packages/frontend/src/style.scss
@@ -431,12 +431,13 @@ rt {
 	border-radius: 10px;
 
 	--bg: #F1E8DC;
-	--panel: #fff;
 	--fg: #693410;
-	--switchOffBg: rgba(0, 0, 0, 0.1);
-	--switchOffFg: rgb(255, 255, 255);
-	--switchOnBg: var(--accent);
-	--switchOnFg: rgb(255, 255, 255);
+}
+
+html[data-color-mode=dark] ._woodenFrame {
+	--bg: #1d0c02;
+	--fg: #F1E8DC;
+	--panel: #192320;
 }
 
 ._woodenFrameH {