-
Notifications
You must be signed in to change notification settings - Fork 43
[MCP Apps] Add styles prop to host context
#127
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
9e97bf3
aa89688
a8b3bf0
f276ec6
bcc0543
aaedd85
db28726
3578186
2fa243a
445a583
aa79cb7
5db8076
2f8d1ec
277a41b
8f591b6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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; | ||
| } | ||
|
|
||
| @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; | ||
| } | ||
| color-scheme: light dark; | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Changes in this file are me updating this one example to use the host context styles (with its original styles as fallbacks) |
||
|
|
||
| /* 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-sm: 6px; | ||
| --border-radius-md: 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-md); | ||
| background: var(--color-background-primary); | ||
| width: 100%; | ||
| height: 600px; | ||
| margin: 0 auto; | ||
|
|
@@ -85,32 +88,32 @@ 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-sm); | ||
| 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; | ||
| } | ||
|
|
||
| /* Chart section - ~420px */ | ||
| .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-md); | ||
| 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-sm); | ||
| 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-sm); | ||
| 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; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,7 +1,12 @@ | ||
| /** | ||
| * @file Customer Segmentation Explorer - interactive scatter/bubble visualization | ||
| */ | ||
| import { App, PostMessageTransport } from "@modelcontextprotocol/ext-apps"; | ||
| import { | ||
| App, | ||
| PostMessageTransport, | ||
| applyHostStyleVariables, | ||
| 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 { | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This is necessary for this particular example b/c chartJS doesn't play nicely with |
||
| 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,29 +277,34 @@ 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(); | ||
|
|
||
| // Update axis titles and formatters (using type assertions for Chart.js scale options) | ||
| 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 }; | ||
| }; | ||
| }; | ||
|
|
||
| scales.x.title.text = METRIC_LABELS[state.xAxis]; | ||
| 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?.variables) { | ||
| applyHostStyleVariables(params.styles.variables); | ||
| } | ||
| // Recreate chart to pick up new colors | ||
| if (state.chart && (params.theme || params.styles?.variables)) { | ||
| 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?.variables) { | ||
| applyHostStyleVariables(ctx.styles.variables); | ||
| } | ||
| }); | ||
|
|
||
| // Fetch data after connection | ||
| setTimeout(fetchData, 100); | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I made this change to all the examples so that they can properly have a transparent background. Without it, iframe sets a default black/white background