Skip to content

Commit 2a44175

Browse files
authored
Merge pull request #833 from Lemoncode/feature/vscode-theming
Feature/vscode-theming
2 parents 7d14768 + 57dc5c1 commit 2a44175

8 files changed

Lines changed: 109 additions & 7 deletions

File tree

apps/web/src/core/vscode/use-vscode-sync.hook.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { useHeadlessRenderComplete } from './use-headless-render-complete.hook';
22
import { useVSCodeAutoSave } from './use-vscode-auto-save.hook';
33
import { useVSCodeFileLoad } from './use-vscode-file-load.hook';
4+
import { useVSCodeTheme } from './use-vscode-theme.hook';
45

56
/**
67
* Wires the full VS Code webview bridge. Each inner hook no-ops when not
@@ -10,4 +11,5 @@ export function useVSCodeSync(): void {
1011
const hasReceivedFileRef = useVSCodeFileLoad();
1112
useVSCodeAutoSave(hasReceivedFileRef);
1213
useHeadlessRenderComplete(hasReceivedFileRef);
14+
useVSCodeTheme();
1315
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import { isVSCodeEnv } from '#common/utils/env.utils';
2+
import { onMessage } from '#common/utils/vscode-bridge.utils';
3+
import {
4+
HOST_MESSAGE_TYPE,
5+
type ThemePayload,
6+
} from '@lemoncode/quickmock-bridge-protocol';
7+
import { useEffect } from 'react';
8+
9+
const CSS_VAR_MAP: Record<keyof ThemePayload, readonly string[]> = {
10+
background: ['--primary-100', '--primary-200'],
11+
backgroundSecondary: ['--pure-white'],
12+
foreground: ['--primary-700'],
13+
};
14+
15+
const applyTheme = (theme: ThemePayload): void => {
16+
const root = document.documentElement;
17+
for (const [key, cssVars] of Object.entries(CSS_VAR_MAP)) {
18+
const value = theme[key as keyof ThemePayload];
19+
if (!value) continue;
20+
for (const cssVar of cssVars) {
21+
root.style.setProperty(cssVar, value);
22+
}
23+
}
24+
if (theme.background)
25+
document.body.style.setProperty('background-color', theme.background);
26+
if (theme.foreground)
27+
document.body.style.setProperty('color', theme.foreground);
28+
};
29+
30+
export const useVSCodeTheme = (): void => {
31+
useEffect(() => {
32+
if (!isVSCodeEnv()) return;
33+
return onMessage(HOST_MESSAGE_TYPE.THEME, applyTheme);
34+
}, []);
35+
};

apps/web/src/scenes/main.module.css

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
.leftTools {
22
position: relative;
33
z-index: 2;
4-
background-color: white;
4+
background-color: var(--pure-white);
55
grid-area: leftTools;
66
border-right: 1px solid black;
77
display: inline-flex;
@@ -12,7 +12,7 @@
1212
.rightTools {
1313
position: relative;
1414
z-index: 2;
15-
background-color: white;
15+
background-color: var(--pure-white);
1616
grid-area: rightTools;
1717
border-left: 1px solid black;
1818
}

apps/web/src/scenes/main.scene.tsx

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { MainLayout } from '#layout/main.layout';
22
import classes from './main.module.css';
33

4-
import { isHeadlessEnv } from '#common/utils/env.utils.ts';
4+
import { isHeadlessEnv, isVSCodeEnv } from '#common/utils/env.utils';
55
import { useInteractionModeContext } from '#core/providers';
66
import {
77
BasicShapesGalleryPod,
@@ -81,9 +81,11 @@ export const MainScene = () => {
8181
<PropertiesPod />
8282
</div>
8383
)}
84-
<div className={classes.footer}>
85-
<FooterPod />
86-
</div>
84+
{!isVSCodeEnv() && (
85+
<div className={classes.footer}>
86+
<FooterPod />
87+
</div>
88+
)}
8789
</MainLayout>
8890
);
8991
};

packages/bridge-protocol/src/constant.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ export const HOST_MESSAGE_TYPE = {
22
LOAD: 'qm:load',
33
SAVED: 'qm:saved',
44
LOAD_FILE: 'LOAD_FILE',
5+
THEME: 'qm:theme',
56
} as const;
67

78
export const APP_MESSAGE_TYPE = {

packages/bridge-protocol/src/model.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,20 @@ export interface LoadFilePayload {
1212
fileName: string;
1313
}
1414

15+
export interface ThemePayload {
16+
background: string;
17+
backgroundSecondary: string;
18+
foreground: string;
19+
}
20+
1521
export type HostMessage =
1622
| {
1723
type: typeof HOST_MESSAGE_TYPE.LOAD;
1824
payload: { content: string; fileName: string };
1925
}
2026
| { type: typeof HOST_MESSAGE_TYPE.SAVED }
21-
| { type: typeof HOST_MESSAGE_TYPE.LOAD_FILE; payload: LoadFilePayload };
27+
| { type: typeof HOST_MESSAGE_TYPE.LOAD_FILE; payload: LoadFilePayload }
28+
| { type: typeof HOST_MESSAGE_TYPE.THEME; payload: ThemePayload };
2229

2330
export type AppMessage =
2431
| { type: typeof APP_MESSAGE_TYPE.READY }

packages/vscode-extension/src/webview/main.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { setupBridge } from './bridge';
2+
import { setupThemeSync } from './theme';
23

34
const appUrl = document.body.dataset.appUrl;
45
if (!appUrl) {
@@ -18,3 +19,4 @@ iframe.title = 'QuickMock Application';
1819
document.body.appendChild(iframe);
1920

2021
setupBridge(iframe, appOrigin);
22+
setupThemeSync(iframe, appOrigin);
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import {
2+
APP_MESSAGE_TYPE,
3+
HOST_MESSAGE_TYPE,
4+
type ThemePayload,
5+
} from '@lemoncode/quickmock-bridge-protocol';
6+
7+
const readVar = (style: CSSStyleDeclaration, name: string): string =>
8+
style.getPropertyValue(name).trim();
9+
10+
export const extractTheme = (): ThemePayload => {
11+
const style = getComputedStyle(document.documentElement);
12+
return {
13+
background: readVar(style, '--vscode-editor-background'),
14+
backgroundSecondary: readVar(style, '--vscode-sideBar-background'),
15+
foreground: readVar(style, '--vscode-editor-foreground'),
16+
};
17+
};
18+
19+
const IFRAME_READY_TYPES: ReadonlySet<string> = new Set([
20+
APP_MESSAGE_TYPE.WEBVIEW_READY,
21+
APP_MESSAGE_TYPE.READY,
22+
]);
23+
24+
export const setupThemeSync = (
25+
iframe: HTMLIFrameElement,
26+
appOrigin: string
27+
): void => {
28+
const sendTheme = (): void => {
29+
iframe.contentWindow?.postMessage(
30+
{ type: HOST_MESSAGE_TYPE.THEME, payload: extractTheme() },
31+
appOrigin
32+
);
33+
};
34+
35+
const onIframeReady = (event: MessageEvent): void => {
36+
if (event.origin !== appOrigin) return;
37+
const type = (event.data as { type?: string } | undefined)?.type;
38+
if (type && IFRAME_READY_TYPES.has(type)) sendTheme();
39+
};
40+
window.addEventListener('message', onIframeReady);
41+
42+
let rafId = 0;
43+
const sendThemeDebounced = (): void => {
44+
cancelAnimationFrame(rafId);
45+
rafId = requestAnimationFrame(sendTheme);
46+
};
47+
48+
const observer = new MutationObserver(sendThemeDebounced);
49+
observer.observe(document.body, {
50+
attributes: true,
51+
attributeFilter: ['class', 'style'],
52+
});
53+
};

0 commit comments

Comments
 (0)