diff --git a/src/app.ts b/src/app.ts index 2321374..b5233df 100644 --- a/src/app.ts +++ b/src/app.ts @@ -47,7 +47,11 @@ import { Transport } from "@modelcontextprotocol/sdk/shared/transport.js"; export { PostMessageTransport } from "./message-transport"; export * from "./types"; -export { applyHostStyles } from "./styles"; +export { + applyHostStyles, + getDocumentTheme, + applyDocumentTheme, +} from "./styles"; /** * Metadata key for associating a resource URI with a tool call. diff --git a/src/react/index.tsx b/src/react/index.tsx index 82867df..09e3b87 100644 --- a/src/react/index.tsx +++ b/src/react/index.tsx @@ -10,6 +10,7 @@ * * - {@link useApp} - React hook to create and connect an MCP App * - {@link useHostStyles} - React hook to apply host styles as CSS variables + * - {@link useDocumentTheme} - React hook for reactive document theme * - {@link useAutoResize} - React hook for manual auto-resize control (rarely needed) * * @module @modelcontextprotocol/ext-apps/react @@ -33,4 +34,5 @@ */ export * from "./useApp"; export * from "./useAutoResize"; +export * from "./useDocumentTheme"; export * from "./useHostStyles"; diff --git a/src/react/useDocumentTheme.ts b/src/react/useDocumentTheme.ts new file mode 100644 index 0000000..2355c68 --- /dev/null +++ b/src/react/useDocumentTheme.ts @@ -0,0 +1,69 @@ +import { useEffect, useState } from "react"; +import { getDocumentTheme } from "../styles"; +import { McpUiTheme } from "../types"; + +/** + * React hook that provides the current document theme reactively. + * + * Uses a MutationObserver to watch for changes to the `data-theme` attribute + * or `class` on `document.documentElement`. When the theme changes (e.g., from + * host context updates), the hook automatically re-renders your component with + * the new theme value. + * + * @returns The current theme ("light" or "dark") + * + * @example Conditionally render based on theme + * ```tsx + * import { useDocumentTheme } from '@modelcontextprotocol/ext-apps/react'; + * + * function MyApp() { + * const theme = useDocumentTheme(); + * + * return ( + *
+ * {theme === 'dark' ? : } + *
+ * ); + * } + * ``` + * + * @example Use with theme-aware styling + * ```tsx + * function ThemedButton() { + * const theme = useDocumentTheme(); + * + * return ( + * + * ); + * } + * ``` + * + * @see {@link getDocumentTheme} for the underlying function + * @see {@link applyDocumentTheme} to set the theme + */ +export function useDocumentTheme(): McpUiTheme { + const [theme, setTheme] = useState(getDocumentTheme); + + useEffect(() => { + const observer = new MutationObserver(() => { + setTheme(getDocumentTheme()); + }); + + observer.observe(document.documentElement, { + attributes: true, + attributeFilter: ["data-theme", "class"], + characterData: false, + childList: false, + subtree: false, + }); + + return () => observer.disconnect(); + }, []); + + return theme; +} diff --git a/src/styles.ts b/src/styles.ts index 52ab6a1..4544248 100644 --- a/src/styles.ts +++ b/src/styles.ts @@ -1,4 +1,77 @@ -import { McpUiStyles } from "./types"; +import { McpUiStyles, McpUiTheme } from "./types"; + +/** + * Get the current document theme from the root HTML element. + * + * Reads the theme from the `data-theme` attribute on `document.documentElement`. + * Falls back to checking for a `dark` class for compatibility with Tailwind CSS + * dark mode conventions. + * + * @returns The current theme ("light" or "dark") + * + * @example Check current theme + * ```typescript + * import { getDocumentTheme } from '@modelcontextprotocol/ext-apps'; + * + * const theme = getDocumentTheme(); + * console.log(`Current theme: ${theme}`); + * ``` + * + * @see {@link applyDocumentTheme} to set the theme + * @see {@link McpUiTheme} for the theme type + */ +export function getDocumentTheme(): McpUiTheme { + const theme = document.documentElement.getAttribute("data-theme"); + + if (theme === "dark" || theme === "light") { + return theme; + } + + // Fallback: check for "dark" class (Tailwind CSS convention) + const darkMode = document.documentElement.classList.contains("dark"); + + return darkMode ? "dark" : "light"; +} + +/** + * Apply a theme to the document root element. + * + * Sets the `data-theme` attribute and CSS `color-scheme` property on + * `document.documentElement`. This enables CSS selectors like + * `[data-theme="dark"]` and ensures native elements (scrollbars, form controls) + * respect the theme. + * + * @param theme - The theme to apply ("light" or "dark") + * + * @example Apply theme from host context + * ```typescript + * import { applyDocumentTheme } from '@modelcontextprotocol/ext-apps'; + * + * app.onhostcontextchanged = (params) => { + * if (params.theme) { + * applyDocumentTheme(params.theme); + * } + * }; + * ``` + * + * @example Use with CSS selectors + * ```css + * [data-theme="dark"] { + * --bg-color: #1a1a1a; + * } + * [data-theme="light"] { + * --bg-color: #ffffff; + * } + * ``` + * + * @see {@link getDocumentTheme} to read the current theme + * @see {@link McpUiTheme} for the theme type + */ +export function applyDocumentTheme(theme: McpUiTheme): void { + const root = document.documentElement; + root.setAttribute("data-theme", theme); + root.style.colorScheme = theme; +} /** * Apply host styles as CSS custom properties on an element.