@@ -40,19 +41,19 @@ export function WebclientNode({ id, data, selected }: NodeProps) {
const baseStyle: React.CSSProperties = {
position: 'relative', display: 'flex', alignItems: 'center', gap: '7px',
width: '100%', height: '100%', padding: '8px 10px', boxSizing: 'border-box',
- background: '#e0f2fe', border: '1.5px solid #0369a1', borderRadius: '10px',
+ background: onSurface(theme.blue, 22), border: `1.5px solid ${theme.blue}`, borderRadius: '10px',
cursor: 'default', userSelect: 'none',
};
const buildingBlockStyle: React.CSSProperties = {
- background: CUSTOM_BG, borderColor: CUSTOM_BORDER, borderWidth: '2px', borderRadius: '6px',
+ background: theme.badgeBg, borderColor: theme.badgeBg, borderWidth: '2px', borderRadius: '6px',
};
const selectedStyle: React.CSSProperties = {
- borderColor: '#6366f1', boxShadow: '0 0 0 1.5px #6366f1',
+ borderColor: theme.focus, boxShadow: `0 0 0 1.5px ${theme.focus}`,
};
const iconStyle: React.CSSProperties = { display: 'flex', alignItems: 'center', flexShrink: 0 };
const labelStyle: React.CSSProperties = {
- fontSize: '10px', fontWeight: 600, color: '#1e293b',
+ fontSize: '10px', fontWeight: 600, color: theme.fg,
overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap',
};
-const labelBlockStyle: React.CSSProperties = { color: '#ffffff' };
-const handleStyle: React.CSSProperties = { width: '8px', height: '8px', background: '#94a3b8', border: '1px solid #64748b' };
+const labelBlockStyle: React.CSSProperties = { color: theme.badgeFg };
+const handleStyle: React.CSSProperties = { width: '8px', height: '8px', background: theme.fgMuted, border: `1px solid ${theme.borderWidget}` };
diff --git a/calm-plugins/vscode/src/webview/canvas/theme-colors.test.ts b/calm-plugins/vscode/src/webview/canvas/theme-colors.test.ts
new file mode 100644
index 0000000000..d349cf8ddf
--- /dev/null
+++ b/calm-plugins/vscode/src/webview/canvas/theme-colors.test.ts
@@ -0,0 +1,53 @@
+import { describe, expect, it } from 'vitest';
+import {
+ THEME_BG_FALLBACK,
+ cssColorToHex,
+ onSurface,
+ theme,
+ tint,
+} from './theme-colors';
+
+describe('theme tokens', () => {
+ it('points every canvas colour at a VS Code theme variable', () => {
+ for (const value of Object.values(theme)) {
+ expect(value).toContain('--vscode-');
+ }
+ });
+
+ it('mixes theme colours instead of a fixed rgb', () => {
+ expect(tint(theme.blue, 12)).toBe(
+ `color-mix(in srgb, ${theme.blue} 12%, transparent)`,
+ );
+ expect(onSurface(theme.green, 22)).toContain('--vscode-charts-green');
+ expect(onSurface(theme.green, 22)).toContain('--vscode-editor-background');
+ });
+});
+
+describe('cssColorToHex', () => {
+ it('normalises hex colours for the colour input', () => {
+ expect(cssColorToHex('#1e293b', THEME_BG_FALLBACK)).toBe('#1e293b');
+ expect(cssColorToHex(' #F8FAFC ', THEME_BG_FALLBACK)).toBe('#f8fafc');
+ expect(cssColorToHex('#AABBCCDD', THEME_BG_FALLBACK)).toBe('#aabbcc');
+ expect(cssColorToHex('#abc', THEME_BG_FALLBACK)).toBe('#aabbcc');
+ expect(cssColorToHex('#abcd', THEME_BG_FALLBACK)).toBe('#aabbcc');
+ });
+
+ it('converts rgb() and rgba() colours', () => {
+ expect(cssColorToHex('rgb(30, 41, 59)', THEME_BG_FALLBACK)).toBe('#1e293b');
+ expect(cssColorToHex('rgba(255, 128, 0, 0.2)', THEME_BG_FALLBACK)).toBe('#ff8000');
+ expect(cssColorToHex('rgb(30 41 59 / 0.5)', THEME_BG_FALLBACK)).toBe('#1e293b');
+ expect(cssColorToHex('rgb(30.2, 41.6, 59.4)', THEME_BG_FALLBACK)).toBe('#1e2a3b');
+ });
+
+ it('returns the fallback when the value is not a usable colour', () => {
+ expect(cssColorToHex('', THEME_BG_FALLBACK)).toBe(THEME_BG_FALLBACK);
+ expect(cssColorToHex(' ', THEME_BG_FALLBACK)).toBe(THEME_BG_FALLBACK);
+ expect(cssColorToHex('red', THEME_BG_FALLBACK)).toBe(THEME_BG_FALLBACK);
+ expect(cssColorToHex('var(--vscode-foreground)', THEME_BG_FALLBACK)).toBe(THEME_BG_FALLBACK);
+ expect(cssColorToHex('#gggggg', THEME_BG_FALLBACK)).toBe(THEME_BG_FALLBACK);
+ expect(cssColorToHex('#12', THEME_BG_FALLBACK)).toBe(THEME_BG_FALLBACK);
+ expect(cssColorToHex('#12345', THEME_BG_FALLBACK)).toBe(THEME_BG_FALLBACK);
+ expect(cssColorToHex('rgb(999, 0, 0)', THEME_BG_FALLBACK)).toBe(THEME_BG_FALLBACK);
+ expect(cssColorToHex('rgb(-1, 0, 0)', THEME_BG_FALLBACK)).toBe(THEME_BG_FALLBACK);
+ });
+});
diff --git a/calm-plugins/vscode/src/webview/canvas/theme-colors.ts b/calm-plugins/vscode/src/webview/canvas/theme-colors.ts
new file mode 100644
index 0000000000..d220f8ba27
--- /dev/null
+++ b/calm-plugins/vscode/src/webview/canvas/theme-colors.ts
@@ -0,0 +1,73 @@
+/**
+ * Colours for the canvas webview, taken from the active VS Code theme.
+ * Fallbacks match the dark-theme defaults in canvas.css.
+ */
+
+export const THEME_BG_VAR = '--vscode-editorWidget-background';
+export const THEME_TEXT_VAR = '--vscode-foreground';
+export const THEME_BG_FALLBACK = '#252526';
+export const THEME_TEXT_FALLBACK = '#cccccc';
+
+export const theme = {
+ fg: `var(${THEME_TEXT_VAR}, ${THEME_TEXT_FALLBACK})`,
+ fgMuted: 'var(--vscode-descriptionForeground, #888888)',
+ bg: 'var(--vscode-editor-background, #1e1e1e)',
+ bgWidget: `var(${THEME_BG_VAR}, ${THEME_BG_FALLBACK})`,
+ border: 'var(--vscode-panel-border, #2d2d2d)',
+ borderWidget: 'var(--vscode-widget-border, #444444)',
+ borderInput: 'var(--vscode-input-border, #555555)',
+ focus: 'var(--vscode-focusBorder, #007fd4)',
+ link: 'var(--vscode-textLink-foreground, #3794ff)',
+ badgeBg: 'var(--vscode-badge-background, #0e639c)',
+ badgeFg: 'var(--vscode-badge-foreground, #ffffff)',
+ shadow: 'var(--vscode-widget-shadow, rgba(0, 0, 0, 0.36))',
+ errorFg: 'var(--vscode-editorError-foreground, #f14c4c)',
+ errorBg: 'var(--vscode-inputValidation-errorBackground, #5a1d1d)',
+ warningFg: 'var(--vscode-editorWarning-foreground, #cca700)',
+ warningBg: 'var(--vscode-inputValidation-warningBackground, #352a05)',
+ green: 'var(--vscode-charts-green, #89d185)',
+ blue: 'var(--vscode-charts-blue, #3794ff)',
+} as const;
+
+/** A translucent wash of a theme colour. Containers stay see-through. */
+export function tint(color: string, percent: number): string {
+ return `color-mix(in srgb, ${color} ${percent}%, transparent)`;
+}
+
+/** An opaque mix of a theme colour onto the editor background. */
+export function onSurface(color: string, percent: number): string {
+ return `color-mix(in srgb, ${color} ${percent}%, ${theme.bg})`;
+}
+
+/**
+ * `
` only accepts `#rrggbb`. Theme variables arrive as
+ * hex or rgb(), so the appearance picker resolves them before display.
+ */
+export function cssColorToHex(raw: string, fallback: string): string {
+ const value = raw.trim();
+ if (!value) return fallback;
+
+ const hex = /^#([0-9a-f]{3,8})$/i.exec(value);
+ if (hex) {
+ const digits = hex[1];
+ if (digits.length === 3 || digits.length === 4) {
+ const [r, g, b] = digits;
+ return `#${r}${r}${g}${g}${b}${b}`.toLowerCase();
+ }
+ if (digits.length === 6 || digits.length === 8) {
+ return `#${digits.slice(0, 6).toLowerCase()}`;
+ }
+ return fallback;
+ }
+
+ const rgb = /^rgba?\(\s*([0-9.]+)[,\s]+([0-9.]+)[,\s]+([0-9.]+)/i.exec(value);
+ if (!rgb) return fallback;
+
+ const channels: string[] = [];
+ for (const part of [rgb[1], rgb[2], rgb[3]]) {
+ const n = Number(part);
+ if (!Number.isFinite(n) || n < 0 || n > 255) return fallback;
+ channels.push(Math.round(n).toString(16).padStart(2, '0'));
+ }
+ return `#${channels.join('')}`;
+}
diff --git a/calm-plugins/vscode/src/webview/panels/NodeAppearance.tsx b/calm-plugins/vscode/src/webview/panels/NodeAppearance.tsx
index 738ed1fa82..661ae811d7 100644
--- a/calm-plugins/vscode/src/webview/panels/NodeAppearance.tsx
+++ b/calm-plugins/vscode/src/webview/panels/NodeAppearance.tsx
@@ -1,8 +1,16 @@
-import React, { useRef, useState } from 'react';
+import React, { useEffect, useRef, useState } from 'react';
import type { NodeStyleOverride } from '../utils/building-block-style';
+import { THEME_BG_FALLBACK, THEME_BG_VAR, THEME_TEXT_FALLBACK, THEME_TEXT_VAR, cssColorToHex } from '../canvas/theme-colors';
-const DEFAULT_BG = '#f8fafc';
-const DEFAULT_TEXT = '#1e293b';
+function useThemeHex(variable: string, fallback: string): string {
+ const [hex, setHex] = useState(fallback);
+ useEffect(() => {
+ if (typeof document === 'undefined') return;
+ const raw = getComputedStyle(document.documentElement).getPropertyValue(variable);
+ setHex(cssColorToHex(raw, fallback));
+ }, [variable, fallback]);
+ return hex;
+}
interface NodeAppearanceProps {
/** Current overrides from the node's `metadata['building-block-style']` (undefined = defaults). */
@@ -25,6 +33,8 @@ export function NodeAppearance({
}: NodeAppearanceProps) {
const [bg, setBg] = useState(style?.background ?? '');
const [text, setText] = useState(style?.text ?? '');
+ const defaultBg = useThemeHex(THEME_BG_VAR, THEME_BG_FALLBACK);
+ const defaultText = useThemeHex(THEME_TEXT_VAR, THEME_TEXT_FALLBACK);
const timer = useRef
| null>(null);
const push = (nextBg: string, nextText: string) => {
@@ -64,7 +74,7 @@ export function NodeAppearance({
onBg(e.target.value)}
style={colorSwatch}
title="Background colour"
@@ -86,7 +96,7 @@ export function NodeAppearance({
onText(e.target.value)}
style={colorSwatch}
title="Text colour"
diff --git a/calm-plugins/vscode/src/webview/panels/NodePalette.tsx b/calm-plugins/vscode/src/webview/panels/NodePalette.tsx
index a0bab9b2f2..44d85830de 100644
--- a/calm-plugins/vscode/src/webview/panels/NodePalette.tsx
+++ b/calm-plugins/vscode/src/webview/panels/NodePalette.tsx
@@ -1,5 +1,6 @@
import React, { useState, useMemo } from 'react';
import { initAllPacks, getAllPacks, type PackDefinition, type NodeTypeEntry } from '../../packs/index.js';
+import { theme, tint } from '../canvas/theme-colors';
initAllPacks();
@@ -215,7 +216,7 @@ export function NodePalette({ buildingBlocks }: NodePaletteProps) {
function Section({ title, collapsed, onToggle, badge, borderColor, children }: { title: string; collapsed: boolean; onToggle: () => void; badge?: string; borderColor?: string; children: React.ReactNode }) {
const sectionStyle: React.CSSProperties = {
marginBottom: '4px',
- ...(badge ? { borderLeft: '2px solid #2e7d32', marginLeft: '4px', background: 'rgba(46,125,50,0.04)' } : {}),
+ ...(badge ? { borderLeft: `2px solid ${theme.green}`, marginLeft: '4px', background: tint(theme.green, 12) } : {}),
...(borderColor && !badge ? { borderLeft: `2px solid ${borderColor}`, marginLeft: '4px' } : {}),
};
return (
@@ -247,6 +248,6 @@ const headerStyle: React.CSSProperties = { display: 'flex', alignItems: 'center'
const searchInputStyle: React.CSSProperties = { width: '100%', height: '28px', padding: '0 24px 0 8px', fontSize: '11px', color: 'var(--calm-fg)', background: 'var(--calm-bg-input)', border: '1px solid var(--calm-border-input)', borderRadius: '4px', outline: 'none' };
const clearBtnStyle: React.CSSProperties = { position: 'absolute', right: '14px', top: '4px', width: '20px', height: '20px', display: 'flex', alignItems: 'center', justifyContent: 'center', background: 'none', border: 'none', cursor: 'pointer', color: 'var(--calm-fg-muted)', fontSize: '14px' };
const groupNameStyle: React.CSSProperties = { display: 'flex', alignItems: 'center', gap: '6px', padding: '4px 12px', fontSize: '10px', fontWeight: 700, letterSpacing: '0.3px', color: 'var(--calm-fg-muted)', background: 'none', border: 'none', width: '100%', cursor: 'pointer', textAlign: 'left' };
-const wsBadgeStyle: React.CSSProperties = { fontSize: '8px', fontWeight: 800, padding: '1px 4px', borderRadius: '3px', background: '#2e7d32', color: '#fff', letterSpacing: '0.5px' };
+const wsBadgeStyle: React.CSSProperties = { fontSize: '8px', fontWeight: 800, padding: '1px 4px', borderRadius: '3px', background: theme.green, color: theme.bg, letterSpacing: '0.5px' };
const subgroupStyle: React.CSSProperties = { padding: '3px 12px 3px 20px', fontSize: '9px', fontWeight: 600, color: 'var(--calm-fg-muted)', textTransform: 'capitalize' };
const itemStyle: React.CSSProperties = { display: 'flex', alignItems: 'center', gap: '8px', padding: '6px 12px', cursor: 'grab', borderRadius: '4px', margin: '1px 6px' };
diff --git a/calm-plugins/vscode/src/webview/panels/StandardsPanel.tsx b/calm-plugins/vscode/src/webview/panels/StandardsPanel.tsx
index 2108c58f58..1f49e16293 100644
--- a/calm-plugins/vscode/src/webview/panels/StandardsPanel.tsx
+++ b/calm-plugins/vscode/src/webview/panels/StandardsPanel.tsx
@@ -1,4 +1,5 @@
import React from 'react';
+import { theme } from '../canvas/theme-colors';
interface StandardsPanelProps {
requirementUrl: string;
@@ -28,7 +29,7 @@ const panelStyle: React.CSSProperties = {
position: 'absolute', top: '10%', right: '10%', width: '400px', maxHeight: '60%',
display: 'flex', flexDirection: 'column',
background: 'var(--calm-bg)', border: '1px solid var(--calm-border-heavy)', borderRadius: '8px',
- boxShadow: '0 8px 24px rgba(0,0,0,0.5)', zIndex: 200,
+ boxShadow: `0 8px 24px ${theme.shadow}`, zIndex: 200,
};
const headerStyle: React.CSSProperties = {
display: 'flex', alignItems: 'center', gap: '8px', padding: '10px 14px',
diff --git a/calm-plugins/vscode/src/webview/panels/ToolbarMenu.tsx b/calm-plugins/vscode/src/webview/panels/ToolbarMenu.tsx
index 4b771af509..7907ac7c50 100644
--- a/calm-plugins/vscode/src/webview/panels/ToolbarMenu.tsx
+++ b/calm-plugins/vscode/src/webview/panels/ToolbarMenu.tsx
@@ -1,4 +1,5 @@
import React, { useState, useRef, useEffect } from 'react';
+import { theme } from '../canvas/theme-colors';
export interface ToolbarMenuItem {
label: string;
@@ -35,7 +36,7 @@ export function ToolbarMenu({ items }: Readonly<{ items: ToolbarMenuItem[] }>) {
aria-haspopup="menu"
aria-expanded={open}
onClick={() => setOpen((o) => !o)}
- style={{ padding: '3px 7px', border: '1px solid #555', borderRadius: '4px' }}
+ style={{ padding: '3px 7px', border: `1px solid ${theme.borderInput}`, borderRadius: '4px' }}
>
@@ -70,7 +71,7 @@ const menuStyle: React.CSSProperties = {
background: 'var(--calm-bg-secondary)',
border: '1px solid var(--calm-border-heavy)',
borderRadius: '6px',
- boxShadow: '0 4px 12px rgba(0,0,0,0.4)',
+ boxShadow: `0 4px 12px ${theme.shadow}`,
minWidth: '170px',
zIndex: 1000,
padding: '4px',
diff --git a/calm-plugins/vscode/src/webview/utils/building-block-style.ts b/calm-plugins/vscode/src/webview/utils/building-block-style.ts
index 09d9f000d5..2a02668d9c 100644
--- a/calm-plugins/vscode/src/webview/utils/building-block-style.ts
+++ b/calm-plugins/vscode/src/webview/utils/building-block-style.ts
@@ -1,7 +1,3 @@
-export const CUSTOM_BG = '#1a4d8f';
-export const CUSTOM_BORDER = '#0f3461';
-export const CUSTOM_TEXT = '#ffffff';
-
export function isBuildingBlock(data: Record): boolean {
const metadata = data.metadata as Record | undefined;
return !!metadata?.['source-building-block'] || !!metadata?.['source-fidelity-node'];