From 9c4cdd944f9f812df7384530ab4f49d4a31c8530 Mon Sep 17 00:00:00 2001 From: Ivanruii Date: Fri, 24 Apr 2026 09:39:13 +0200 Subject: [PATCH 1/6] feat(vscode-theming): implement theme synchronization and update styling variables --- .../src/core/vscode/use-vscode-sync.hook.ts | 2 + .../src/core/vscode/use-vscode-theme.hook.ts | 33 ++++++++++++ apps/web/src/scenes/main.module.css | 4 +- apps/web/src/scenes/main.scene.tsx | 10 ++-- packages/bridge-protocol/src/constant.ts | 1 + packages/bridge-protocol/src/model.ts | 9 +++- packages/vscode-extension/src/webview/main.ts | 2 + .../vscode-extension/src/webview/theme.ts | 52 +++++++++++++++++++ 8 files changed, 106 insertions(+), 7 deletions(-) create mode 100644 apps/web/src/core/vscode/use-vscode-theme.hook.ts create mode 100644 packages/vscode-extension/src/webview/theme.ts diff --git a/apps/web/src/core/vscode/use-vscode-sync.hook.ts b/apps/web/src/core/vscode/use-vscode-sync.hook.ts index ce07fd9e..0e601418 100644 --- a/apps/web/src/core/vscode/use-vscode-sync.hook.ts +++ b/apps/web/src/core/vscode/use-vscode-sync.hook.ts @@ -1,6 +1,7 @@ import { useHeadlessRenderComplete } from './use-headless-render-complete.hook'; import { useVSCodeAutoSave } from './use-vscode-auto-save.hook'; import { useVSCodeFileLoad } from './use-vscode-file-load.hook'; +import { useVSCodeTheme } from './use-vscode-theme.hook'; /** * Wires the full VS Code webview bridge. Each inner hook no-ops when not @@ -10,4 +11,5 @@ export function useVSCodeSync(): void { const hasReceivedFileRef = useVSCodeFileLoad(); useVSCodeAutoSave(hasReceivedFileRef); useHeadlessRenderComplete(hasReceivedFileRef); + useVSCodeTheme(); } diff --git a/apps/web/src/core/vscode/use-vscode-theme.hook.ts b/apps/web/src/core/vscode/use-vscode-theme.hook.ts new file mode 100644 index 00000000..b9ce6c2f --- /dev/null +++ b/apps/web/src/core/vscode/use-vscode-theme.hook.ts @@ -0,0 +1,33 @@ +import { isVSCodeEnv } from '#common/utils/env.utils.ts'; +import { onMessage } from '#common/utils/vscode-bridge.utils.ts'; +import { + HOST_MESSAGE_TYPE, + type ThemePayload, +} from '@lemoncode/quickmock-bridge-protocol'; +import { useEffect } from 'react'; + +const CSS_VAR_MAP: Record = { + background: ['--primary-100', '--primary-500', '--primary-200'], + backgroundSecondary: ['--pure-white'], + foreground: ['--primary-700'], +}; + +const applyTheme = (theme: ThemePayload): void => { + const root = document.documentElement; + for (const [key, cssVars] of Object.entries(CSS_VAR_MAP)) { + const value = theme[key as keyof ThemePayload]; + if (!value) continue; + for (const cssVar of cssVars) { + root.style.setProperty(cssVar, value); + } + } + if (theme.background) document.body.style.backgroundColor = theme.background; + if (theme.foreground) document.body.style.color = theme.foreground; +}; + +export const useVSCodeTheme = (): void => { + useEffect(() => { + if (!isVSCodeEnv()) return; + return onMessage(HOST_MESSAGE_TYPE.THEME, applyTheme); + }, []); +}; diff --git a/apps/web/src/scenes/main.module.css b/apps/web/src/scenes/main.module.css index 7028d8f5..ccf17a0c 100644 --- a/apps/web/src/scenes/main.module.css +++ b/apps/web/src/scenes/main.module.css @@ -1,7 +1,7 @@ .leftTools { position: relative; z-index: 2; - background-color: white; + background-color: var(--pure-white); grid-area: leftTools; border-right: 1px solid black; display: inline-flex; @@ -12,7 +12,7 @@ .rightTools { position: relative; z-index: 2; - background-color: white; + background-color: var(--pure-white); grid-area: rightTools; border-left: 1px solid black; } diff --git a/apps/web/src/scenes/main.scene.tsx b/apps/web/src/scenes/main.scene.tsx index 8cb9fc2c..8572b88d 100644 --- a/apps/web/src/scenes/main.scene.tsx +++ b/apps/web/src/scenes/main.scene.tsx @@ -1,7 +1,7 @@ import { MainLayout } from '#layout/main.layout'; import classes from './main.module.css'; -import { isHeadlessEnv } from '#common/utils/env.utils.ts'; +import { isHeadlessEnv, isVSCodeEnv } from '#common/utils/env.utils.ts'; import { useInteractionModeContext } from '#core/providers'; import { BasicShapesGalleryPod, @@ -81,9 +81,11 @@ export const MainScene = () => { )} -
- -
+ {!isVSCodeEnv() && ( +
+ +
+ )} ); }; diff --git a/packages/bridge-protocol/src/constant.ts b/packages/bridge-protocol/src/constant.ts index 85cdad50..5caee2c2 100644 --- a/packages/bridge-protocol/src/constant.ts +++ b/packages/bridge-protocol/src/constant.ts @@ -2,6 +2,7 @@ export const HOST_MESSAGE_TYPE = { LOAD: 'qm:load', SAVED: 'qm:saved', LOAD_FILE: 'LOAD_FILE', + THEME: 'qm:theme', } as const; export const APP_MESSAGE_TYPE = { diff --git a/packages/bridge-protocol/src/model.ts b/packages/bridge-protocol/src/model.ts index 055466e7..513b2501 100644 --- a/packages/bridge-protocol/src/model.ts +++ b/packages/bridge-protocol/src/model.ts @@ -12,13 +12,20 @@ export interface LoadFilePayload { fileName: string; } +export interface ThemePayload { + background: string; + backgroundSecondary: string; + foreground: string; +} + export type HostMessage = | { type: typeof HOST_MESSAGE_TYPE.LOAD; payload: { content: string; fileName: string }; } | { type: typeof HOST_MESSAGE_TYPE.SAVED } - | { type: typeof HOST_MESSAGE_TYPE.LOAD_FILE; payload: LoadFilePayload }; + | { type: typeof HOST_MESSAGE_TYPE.LOAD_FILE; payload: LoadFilePayload } + | { type: typeof HOST_MESSAGE_TYPE.THEME; payload: ThemePayload }; export type AppMessage = | { type: typeof APP_MESSAGE_TYPE.READY } diff --git a/packages/vscode-extension/src/webview/main.ts b/packages/vscode-extension/src/webview/main.ts index 63e4057d..36634d59 100644 --- a/packages/vscode-extension/src/webview/main.ts +++ b/packages/vscode-extension/src/webview/main.ts @@ -1,4 +1,5 @@ import { setupBridge } from './bridge'; +import { setupThemeSync } from './theme'; const appUrl = document.body.dataset.appUrl; if (!appUrl) { @@ -18,3 +19,4 @@ iframe.title = 'QuickMock Application'; document.body.appendChild(iframe); setupBridge(iframe, appOrigin); +setupThemeSync(iframe, appOrigin); diff --git a/packages/vscode-extension/src/webview/theme.ts b/packages/vscode-extension/src/webview/theme.ts new file mode 100644 index 00000000..5f74b31b --- /dev/null +++ b/packages/vscode-extension/src/webview/theme.ts @@ -0,0 +1,52 @@ +import { + APP_MESSAGE_TYPE, + HOST_MESSAGE_TYPE, + type ThemePayload, +} from '@lemoncode/quickmock-bridge-protocol'; + +const readVar = (style: CSSStyleDeclaration, name: string): string => + style.getPropertyValue(name).trim(); + +export const extractTheme = (): ThemePayload => { + const style = getComputedStyle(document.documentElement); + return { + background: readVar(style, '--vscode-editor-background'), + backgroundSecondary: readVar(style, '--vscode-sideBar-background'), + foreground: readVar(style, '--vscode-editor-foreground'), + }; +}; + +const IFRAME_READY_TYPES: ReadonlySet = new Set([ + APP_MESSAGE_TYPE.WEBVIEW_READY, + APP_MESSAGE_TYPE.READY, +]); + +export const setupThemeSync = ( + iframe: HTMLIFrameElement, + appOrigin: string +): (() => void) => { + const sendTheme = (): void => { + iframe.contentWindow?.postMessage( + { type: HOST_MESSAGE_TYPE.THEME, payload: extractTheme() }, + appOrigin + ); + }; + + const onIframeReady = (event: MessageEvent): void => { + if (event.origin !== appOrigin) return; + const type = (event.data as { type?: string } | undefined)?.type; + if (type && IFRAME_READY_TYPES.has(type)) sendTheme(); + }; + window.addEventListener('message', onIframeReady); + + const observer = new MutationObserver(sendTheme); + observer.observe(document.body, { + attributes: true, + attributeFilter: ['class', 'style'], + }); + + return () => { + window.removeEventListener('message', onIframeReady); + observer.disconnect(); + }; +}; From c0575fee344e7cd3ab512fa73af736d2dbfbe914 Mon Sep 17 00:00:00 2001 From: Ivanruii Date: Fri, 8 May 2026 13:09:48 +0200 Subject: [PATCH 2/6] fix(vscode-theming): refine background CSS variable mapping --- apps/web/src/core/vscode/use-vscode-theme.hook.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/src/core/vscode/use-vscode-theme.hook.ts b/apps/web/src/core/vscode/use-vscode-theme.hook.ts index b9ce6c2f..bd6ef3da 100644 --- a/apps/web/src/core/vscode/use-vscode-theme.hook.ts +++ b/apps/web/src/core/vscode/use-vscode-theme.hook.ts @@ -7,7 +7,7 @@ import { import { useEffect } from 'react'; const CSS_VAR_MAP: Record = { - background: ['--primary-100', '--primary-500', '--primary-200'], + background: ['--primary-100', '--primary-200'], backgroundSecondary: ['--pure-white'], foreground: ['--primary-700'], }; From 4ca0046956a60c59f9a69056794640a08ade4208 Mon Sep 17 00:00:00 2001 From: Ivanruii Date: Fri, 8 May 2026 13:13:41 +0200 Subject: [PATCH 3/6] fix(vscode-theming): debounce theme updates to improve performance --- packages/vscode-extension/src/webview/theme.ts | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/packages/vscode-extension/src/webview/theme.ts b/packages/vscode-extension/src/webview/theme.ts index 5f74b31b..194a6a72 100644 --- a/packages/vscode-extension/src/webview/theme.ts +++ b/packages/vscode-extension/src/webview/theme.ts @@ -39,7 +39,13 @@ export const setupThemeSync = ( }; window.addEventListener('message', onIframeReady); - const observer = new MutationObserver(sendTheme); + let rafId = 0; + const sendThemeDebounced = (): void => { + cancelAnimationFrame(rafId); + rafId = requestAnimationFrame(sendTheme); + }; + + const observer = new MutationObserver(sendThemeDebounced); observer.observe(document.body, { attributes: true, attributeFilter: ['class', 'style'], @@ -48,5 +54,6 @@ export const setupThemeSync = ( return () => { window.removeEventListener('message', onIframeReady); observer.disconnect(); + cancelAnimationFrame(rafId); }; }; From c7c082f0121b0ba1ae867b79bf158e736a620f39 Mon Sep 17 00:00:00 2001 From: Ivanruii Date: Fri, 8 May 2026 13:19:03 +0200 Subject: [PATCH 4/6] fix(vscode-theming): standardize background and foreground style property setting --- apps/web/src/core/vscode/use-vscode-theme.hook.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/apps/web/src/core/vscode/use-vscode-theme.hook.ts b/apps/web/src/core/vscode/use-vscode-theme.hook.ts index bd6ef3da..9b668e7f 100644 --- a/apps/web/src/core/vscode/use-vscode-theme.hook.ts +++ b/apps/web/src/core/vscode/use-vscode-theme.hook.ts @@ -21,8 +21,10 @@ const applyTheme = (theme: ThemePayload): void => { root.style.setProperty(cssVar, value); } } - if (theme.background) document.body.style.backgroundColor = theme.background; - if (theme.foreground) document.body.style.color = theme.foreground; + if (theme.background) + document.body.style.setProperty('background-color', theme.background); + if (theme.foreground) + document.body.style.setProperty('color', theme.foreground); }; export const useVSCodeTheme = (): void => { From a726ee1d777692d1a107d7294b7220f874f8af99 Mon Sep 17 00:00:00 2001 From: Ivanruii Date: Fri, 8 May 2026 13:24:33 +0200 Subject: [PATCH 5/6] fix(vscode-theming): remove unused cleanup function from setupThemeSync --- packages/vscode-extension/src/webview/theme.ts | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/packages/vscode-extension/src/webview/theme.ts b/packages/vscode-extension/src/webview/theme.ts index 194a6a72..530137b3 100644 --- a/packages/vscode-extension/src/webview/theme.ts +++ b/packages/vscode-extension/src/webview/theme.ts @@ -24,7 +24,7 @@ const IFRAME_READY_TYPES: ReadonlySet = new Set([ export const setupThemeSync = ( iframe: HTMLIFrameElement, appOrigin: string -): (() => void) => { +): void => { const sendTheme = (): void => { iframe.contentWindow?.postMessage( { type: HOST_MESSAGE_TYPE.THEME, payload: extractTheme() }, @@ -50,10 +50,4 @@ export const setupThemeSync = ( attributes: true, attributeFilter: ['class', 'style'], }); - - return () => { - window.removeEventListener('message', onIframeReady); - observer.disconnect(); - cancelAnimationFrame(rafId); - }; }; From 57dc5c11875433acb8410a2073563bd2cc1c15d5 Mon Sep 17 00:00:00 2001 From: Ivanruii Date: Fri, 8 May 2026 13:33:40 +0200 Subject: [PATCH 6/6] fix: remove unnecessary file extensions in import statements --- apps/web/src/core/vscode/use-vscode-theme.hook.ts | 4 ++-- apps/web/src/scenes/main.scene.tsx | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/web/src/core/vscode/use-vscode-theme.hook.ts b/apps/web/src/core/vscode/use-vscode-theme.hook.ts index 9b668e7f..7c94d3ce 100644 --- a/apps/web/src/core/vscode/use-vscode-theme.hook.ts +++ b/apps/web/src/core/vscode/use-vscode-theme.hook.ts @@ -1,5 +1,5 @@ -import { isVSCodeEnv } from '#common/utils/env.utils.ts'; -import { onMessage } from '#common/utils/vscode-bridge.utils.ts'; +import { isVSCodeEnv } from '#common/utils/env.utils'; +import { onMessage } from '#common/utils/vscode-bridge.utils'; import { HOST_MESSAGE_TYPE, type ThemePayload, diff --git a/apps/web/src/scenes/main.scene.tsx b/apps/web/src/scenes/main.scene.tsx index 8572b88d..07842f70 100644 --- a/apps/web/src/scenes/main.scene.tsx +++ b/apps/web/src/scenes/main.scene.tsx @@ -1,7 +1,7 @@ import { MainLayout } from '#layout/main.layout'; import classes from './main.module.css'; -import { isHeadlessEnv, isVSCodeEnv } from '#common/utils/env.utils.ts'; +import { isHeadlessEnv, isVSCodeEnv } from '#common/utils/env.utils'; import { useInteractionModeContext } from '#core/providers'; import { BasicShapesGalleryPod,