Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions apps/web/src/core/vscode/use-vscode-sync.hook.ts
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -10,4 +11,5 @@ export function useVSCodeSync(): void {
const hasReceivedFileRef = useVSCodeFileLoad();
useVSCodeAutoSave(hasReceivedFileRef);
useHeadlessRenderComplete(hasReceivedFileRef);
useVSCodeTheme();
}
33 changes: 33 additions & 0 deletions apps/web/src/core/vscode/use-vscode-theme.hook.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
import { isVSCodeEnv } from '#common/utils/env.utils.ts';

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

import { isVSCodeEnv, onMessage } from '#common/utils';

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<keyof ThemePayload, readonly string[]> = {
background: ['--primary-100', '--primary-500', '--primary-200'],

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

background se mapea al mismo valor en tres CSS vars distintas: --primary-100, --primary-500, --primary-200. Normalmente representan shades distintos del color primario; igualarlos puede producir una UI sin contraste en modo VS Code. Si es un placeholder intencional, añadir un comentario que lo indique.

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;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

document.body.style.backgroundColor y document.body.style.color se establecen como estilos inline. Mezclarlo con el sistema de CSS vars puede causar conflictos de especificidad. Preferir document.documentElement.style.setProperty para mantener todo en el mismo mecanismo, igual que se hace con el resto de CSS vars en el bucle de arriba.

if (theme.foreground) document.body.style.color = theme.foreground;
};

export const useVSCodeTheme = (): void => {
useEffect(() => {
if (!isVSCodeEnv()) return;
return onMessage(HOST_MESSAGE_TYPE.THEME, applyTheme);
}, []);
};
4 changes: 2 additions & 2 deletions apps/web/src/scenes/main.module.css
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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;
}
Expand Down
10 changes: 6 additions & 4 deletions apps/web/src/scenes/main.scene.tsx
Original file line number Diff line number Diff line change
@@ -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';

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
import { isHeadlessEnv, isVSCodeEnv } from '#common/utils/env.utils.ts';
import { isHeadlessEnv, isVSCodeEnv } from '#common/utils';

import { useInteractionModeContext } from '#core/providers';
import {
BasicShapesGalleryPod,
Expand Down Expand Up @@ -81,9 +81,11 @@ export const MainScene = () => {
<PropertiesPod />
</div>
)}
<div className={classes.footer}>
<FooterPod />
</div>
{!isVSCodeEnv() && (
<div className={classes.footer}>
<FooterPod />
</div>
)}
</MainLayout>
);
};
1 change: 1 addition & 0 deletions packages/bridge-protocol/src/constant.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
9 changes: 8 additions & 1 deletion packages/bridge-protocol/src/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down
2 changes: 2 additions & 0 deletions packages/vscode-extension/src/webview/main.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { setupBridge } from './bridge';
import { setupThemeSync } from './theme';

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

setupBridge(iframe, appOrigin);
setupThemeSync(iframe, appOrigin);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

El retorno de setupThemeSync (función de cleanup que elimina el listener y desconecta el MutationObserver) se descarta. Para un webview cuyo ciclo de vida es el de la página es tolerable, pero es preferible guardar la referencia para hacer explícita la intención:

const _cleanupTheme = setupThemeSync(iframe, appOrigin);

52 changes: 52 additions & 0 deletions packages/vscode-extension/src/webview/theme.ts
Original file line number Diff line number Diff line change
@@ -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<string> = 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);

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MutationObserver dispara sendTheme() en cada mutación de atributo, sin debounce. Un cambio de tema en VS Code puede generar varias mutaciones seguidas enviando el payload al iframe repetidamente. Sugerencia: agrupar con requestAnimationFrame:

let rafId = 0;
const sendThemeDebounced = () => {
  cancelAnimationFrame(rafId);
  rafId = requestAnimationFrame(sendTheme);
};
const observer = new MutationObserver(sendThemeDebounced);

const observer = new MutationObserver(sendTheme);
observer.observe(document.body, {
attributes: true,
attributeFilter: ['class', 'style'],
});

return () => {
window.removeEventListener('message', onIframeReady);
observer.disconnect();
};
};
Loading