Updates example to use theme colors and adds border radius
This commit is contained in:
parent
35781865af
commit
2fa243ae8c
2 changed files with 212 additions and 98 deletions
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
Loading…
Reference in a new issue