Updates example to use theme colors and adds border radius

This commit is contained in:
martinalong 2025-12-12 22:15:50 -08:00
commit 2fa243ae8c
No known key found for this signature in database
2 changed files with 212 additions and 98 deletions

View file

@ -1,42 +1,45 @@
/* Default fallback values for host style variables */
:root {
--color-bg: #ffffff;
--color-text: #1f2937;
--color-text-muted: #6b7280;
--color-primary: #2563eb;
--color-primary-hover: #1d4ed8;
--color-card-bg: #f9fafb;
--color-border: #e5e7eb;
--color-enterprise: #1e40af;
--color-midmarket: #0d9488;
--color-smb: #059669;
--color-startup: #6366f1;
}
color-scheme: light dark;
@media (prefers-color-scheme: dark) {
:root {
--color-bg: #111827;
--color-text: #f9fafb;
--color-text-muted: #9ca3af;
--color-primary: #3b82f6;
--color-primary-hover: #60a5fa;
--color-card-bg: #1f2937;
--color-border: #374151;
--color-enterprise: #3b82f6;
--color-midmarket: #14b8a6;
--color-smb: #10b981;
--color-startup: #818cf8;
}
/* Background colors */
--color-background-primary: light-dark(#ffffff, #111827);
--color-background-secondary: light-dark(#f9fafb, #1f2937);
--color-background-tertiary: light-dark(#f3f4f6, #374151);
/* Text colors */
--color-text-primary: light-dark(#1f2937, #f9fafb);
--color-text-secondary: light-dark(#6b7280, #9ca3af);
--color-text-tertiary: light-dark(#9ca3af, #6b7280);
/* Border colors */
--color-border-primary: light-dark(#e5e7eb, #374151);
--color-border-secondary: light-dark(#d1d5db, #4b5563);
/* Accent colors */
--color-accent-info: light-dark(#2563eb, #3b82f6);
/* Border radius */
--border-radius-small: 4px;
--border-radius-medium: 8px;
/* App-specific colors (not from host) */
--color-enterprise: light-dark(#1e40af, #3b82f6);
--color-midmarket: light-dark(#0d9488, #14b8a6);
--color-smb: light-dark(#059669, #10b981);
--color-startup: light-dark(#6366f1, #818cf8);
}
html, body {
margin: 0;
padding: 0;
background: var(--color-bg);
color: var(--color-text);
color: var(--color-text-primary);
overflow: hidden;
}
.main {
border-radius: var(--border-radius-medium);
background: var(--color-background-primary);
width: 100%;
height: 600px;
margin: 0 auto;
@ -85,21 +88,21 @@ html, body {
gap: 6px;
font-size: 0.8125rem;
font-weight: 500;
color: var(--color-text-muted);
color: var(--color-text-secondary);
}
.select {
padding: 4px 8px;
font-size: 0.8125rem;
border: 1px solid var(--color-border);
border-radius: 4px;
background: var(--color-card-bg);
color: var(--color-text);
border: 1px solid var(--color-border-primary);
border-radius: var(--border-radius-small);
background: var(--color-background-secondary);
color: var(--color-text-primary);
cursor: pointer;
}
.select:focus {
outline: 2px solid var(--color-primary);
outline: 2px solid var(--color-accent-info);
outline-offset: 1px;
}
@ -107,10 +110,10 @@ html, body {
.chart-section {
flex: 1;
min-height: 0;
background: var(--color-card-bg);
border-radius: 8px;
background: var(--color-background-secondary);
border-radius: var(--border-radius-medium);
padding: 8px;
border: 1px solid var(--color-border);
border: 1px solid var(--color-border-primary);
}
.chart-container {
@ -141,13 +144,13 @@ html, body {
cursor: pointer;
padding: 4px 10px;
border-radius: 16px;
border: 1px solid var(--color-border);
background: var(--color-card-bg);
border: 1px solid var(--color-border-primary);
background: var(--color-background-secondary);
transition: all 0.15s ease;
}
.legend-item:hover {
border-color: var(--color-text-muted);
border-color: var(--color-text-secondary);
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
@ -171,7 +174,7 @@ html, body {
}
.legend-count {
color: var(--color-text-muted);
color: var(--color-text-secondary);
}
/* Detail section - ~44px */
@ -186,15 +189,15 @@ html, body {
justify-content: center;
gap: 16px;
height: 100%;
background: var(--color-card-bg);
border-radius: 6px;
background: var(--color-background-secondary);
border-radius: var(--border-radius-small);
padding: 0 12px;
border: 1px solid var(--color-border);
border: 1px solid var(--color-border-primary);
font-size: 0.8125rem;
}
.detail-placeholder {
color: var(--color-text-muted);
color: var(--color-text-secondary);
}
.detail-name {
@ -203,7 +206,7 @@ html, body {
.detail-segment {
padding: 2px 8px;
border-radius: 4px;
border-radius: var(--border-radius-small);
font-size: 0.75rem;
font-weight: 500;
color: white;
@ -215,10 +218,10 @@ html, body {
.detail-segment.startup { background: var(--color-startup); }
.detail-metric {
color: var(--color-text-muted);
color: var(--color-text-secondary);
}
.detail-metric strong {
color: var(--color-text);
color: var(--color-text-primary);
font-weight: 600;
}

View file

@ -1,7 +1,12 @@
/**
* @file Customer Segmentation Explorer - interactive scatter/bubble visualization
*/
import { App, PostMessageTransport } from "@modelcontextprotocol/ext-apps";
import {
App,
PostMessageTransport,
applyHostStyles,
applyDocumentTheme,
} from "@modelcontextprotocol/ext-apps";
import { Chart, registerables } from "chart.js";
import "./global.css";
import "./mcp-app.css";
@ -138,11 +143,40 @@ function buildDatasets(): Chart["data"]["datasets"] {
});
}
// Hidden element for resolving CSS color values (reused to avoid DOM thrashing)
let colorResolver: HTMLDivElement | null = null;
// Resolve a CSS color value (handles light-dark() function)
function resolveColor(cssValue: string, fallback: string): string {
if (!cssValue) return fallback;
// If it's a simple color value, return it directly
if (!cssValue.includes("light-dark(")) return cssValue;
// Create resolver element once and keep it hidden
if (!colorResolver) {
colorResolver = document.createElement("div");
colorResolver.style.position = "absolute";
colorResolver.style.visibility = "hidden";
colorResolver.style.pointerEvents = "none";
document.body.appendChild(colorResolver);
}
colorResolver.style.color = cssValue;
return getComputedStyle(colorResolver).color || fallback;
}
// Get colors from CSS variables
function getChartColors(): { textColor: string; gridColor: string } {
const style = getComputedStyle(document.documentElement);
const rawTextColor = style.getPropertyValue("--color-text-secondary").trim();
const rawGridColor = style.getPropertyValue("--color-border-primary").trim();
return {
textColor: resolveColor(rawTextColor, "#6b7280"),
gridColor: resolveColor(rawGridColor, "#e5e7eb"),
};
}
// Initialize Chart.js
function initChart(): Chart {
const isDarkMode = window.matchMedia("(prefers-color-scheme: dark)").matches;
const textColor = isDarkMode ? "#9ca3af" : "#6b7280";
const gridColor = isDarkMode ? "#374151" : "#e5e7eb";
const { textColor, gridColor } = getChartColors();
return new Chart(chartCanvas, {
type: "bubble",
@ -243,8 +277,7 @@ function initChart(): Chart {
function updateChart(): void {
if (!state.chart) return;
const isDarkMode = window.matchMedia("(prefers-color-scheme: dark)").matches;
const textColor = isDarkMode ? "#9ca3af" : "#6b7280";
const { textColor, gridColor } = getChartColors();
state.chart.data.datasets = buildDatasets();
@ -252,11 +285,13 @@ function updateChart(): void {
const scales = state.chart.options.scales as {
x: {
title: { text: string; color: string };
ticks: { callback: (value: number) => string };
ticks: { color: string; callback: (value: number) => string };
grid: { color: string };
};
y: {
title: { text: string; color: string };
ticks: { callback: (value: number) => string };
ticks: { color: string; callback: (value: number) => string };
grid: { color: string };
};
};
@ -264,8 +299,12 @@ function updateChart(): void {
scales.y.title.text = METRIC_LABELS[state.yAxis];
scales.x.title.color = textColor;
scales.y.title.color = textColor;
scales.x.ticks.color = textColor;
scales.y.ticks.color = textColor;
scales.x.ticks.callback = (value: number) => formatValue(value, state.xAxis);
scales.y.ticks.callback = (value: number) => formatValue(value, state.yAxis);
scales.x.grid.color = gridColor;
scales.y.grid.color = gridColor;
state.chart.update();
}
@ -388,20 +427,52 @@ document.addEventListener("click", (e) => {
}
});
// Handle theme changes
// Handle system theme changes (fallback when host doesn't provide styles)
window
.matchMedia("(prefers-color-scheme: dark)")
.addEventListener("change", () => {
if (state.chart) {
state.chart.destroy();
state.chart = initChart();
.addEventListener("change", (e) => {
// Only apply if we haven't received host theme
if (!app.getHostContext()?.theme) {
applyDocumentTheme(e.matches ? "dark" : "light");
if (state.chart) {
state.chart.destroy();
state.chart = initChart();
}
}
});
// Apply initial theme based on system preference (before host context is available)
const systemDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
applyDocumentTheme(systemDark ? "dark" : "light");
// Register handlers and connect
app.onerror = log.error;
app.connect(new PostMessageTransport(window.parent));
// Handle host context changes (theme and styles from host)
app.onhostcontextchanged = (params) => {
if (params.theme) {
applyDocumentTheme(params.theme);
}
if (params.styles) {
applyHostStyles(params.styles);
}
// Recreate chart to pick up new colors
if (state.chart && (params.theme || params.styles)) {
state.chart.destroy();
state.chart = initChart();
}
};
app.connect(new PostMessageTransport(window.parent)).then(() => {
// Apply initial host context after connection
const ctx = app.getHostContext();
if (ctx?.theme) {
applyDocumentTheme(ctx.theme);
}
if (ctx?.styles) {
applyHostStyles(ctx.styles);
}
});
// Fetch data after connection
setTimeout(fetchData, 100);