diff --git a/.changeset/theming-lib-and-docs.md b/.changeset/theming-lib-and-docs.md
new file mode 100644
index 0000000..550c646
--- /dev/null
+++ b/.changeset/theming-lib-and-docs.md
@@ -0,0 +1,14 @@
+---
+'@mobile-reality/mdma-renderer-react': minor
+'@mobile-reality/mdma-renderer-react-native': minor
+---
+
+Add opt-in theming to both renderers via a `theme` prop on `MdmaDocument`.
+
+- Pass `"light"`, `"dark"`, `"auto"` (follows the OS preference), or a full `MdmaTheme` token object; omit it for the unchanged default light look.
+- Web renderer: `styles.css` is now driven by `--mdma-*` CSS variables, so themes apply via `data-theme`/inline variables and can be overridden directly in CSS. Exposes `MdmaThemeProvider`, `useMdmaTheme`, `lightTheme`, `darkTheme`, and helpers.
+- Web renderer: a `MdmaDocument` with no `theme` prop now inherits the theme from an ancestor `MdmaThemeProvider`, so one provider can theme a whole app; an explicit `theme` prop still wins.
+- Web renderer: a custom `MdmaTheme` object now also selects the light/dark base (by its `background` luminance) so the stylesheet's internal derived colors (heading text, code backgrounds, …) match — a custom *dark* theme no longer renders near-black text on a dark surface.
+- Web renderer: interactive states — button hover (`--mdma-color-*-hover`) and the input focus ring — now derive from the base tokens via `color-mix`, so a custom `primary` gets a coherent matching hover/focus automatically instead of the built-in purple.
+- React Native renderer: `theme="auto"` now follows the OS color scheme via `useColorScheme()`.
+- Both renderers share the same `MdmaTheme` token shape, so a custom theme object is portable between web and native.
diff --git a/.changeset/unify-rn-palette.md b/.changeset/unify-rn-palette.md
new file mode 100644
index 0000000..689263b
--- /dev/null
+++ b/.changeset/unify-rn-palette.md
@@ -0,0 +1,7 @@
+---
+'@mobile-reality/mdma-renderer-react-native': minor
+---
+
+Unify the React Native renderer's built-in `lightTheme`/`darkTheme` palettes with the web renderer's, so MDMA content looks consistent across web and native. The native defaults were previously a blue-primary palette; they now match the web renderer's purple-primary light/dark themes exactly (same `colors`, and `fontSize` `small`/`title` aligned). Pass a custom `MdmaTheme` to `MdmaDocument`/`MdmaThemeProvider` to override.
+
+Also fixes the RN `TableRenderer` and `ChartRenderer` (which renders as a table), whose bodies had no themed background (only the header did) — on a dark theme the rows showed through to whatever was behind them. They now fill their container with `colors.background` (header stays `colors.surface`), matching the web renderer and the other RN card renderers.
diff --git a/README.md b/README.md
index 7689479..7942061 100644
--- a/README.md
+++ b/README.md
@@ -358,6 +358,18 @@ function App({ ast, store }) {
> **Note:** The `styles.css` import provides default styling for all MDMA components (forms, tables, callouts, animations, etc.). It's optional — you can write your own styles targeting the `.mdma-*` CSS classes instead.
+### Theming
+
+Both renderers accept a `theme` prop on `MdmaDocument`, so theming is entirely opt-in — omit it and you get the default light look. Pass a built-in palette, follow the OS preference, or hand over a full custom token object:
+
+```tsx
+ // built-in dark palette
+ // follows OS light/dark
+ // custom MdmaTheme tokens
+```
+
+The web (`renderer-react`) and native (`renderer-react-native`) renderers share the same `MdmaTheme` token shape, so a theme object is portable between them. On the web, tokens are applied as `--mdma-*` CSS variables (still fully overridable in your own CSS); on native, renderers read them via `useMdmaTheme()`. See the [Theming guide](docs/guides/theming.md) for the full token reference.
+
## Packages
| Package | Description |
diff --git a/demo/src/App.tsx b/demo/src/App.tsx
index bd5dcb3..6b93966 100644
--- a/demo/src/App.tsx
+++ b/demo/src/App.tsx
@@ -1,5 +1,7 @@
import { useState, useEffect, useRef } from 'react';
+import { MdmaThemeProvider } from '@mobile-reality/mdma-renderer-react';
import logoUrl from '../../assets/logo.svg';
+import { DemoThemeContext, type ThemeMode } from './theme-context.js';
import { AgentChatView } from './AgentChatView.js';
import { ChatView } from './ChatView.js';
import { CustomChatView } from './CustomChatView.js';
@@ -24,6 +26,60 @@ function navigate(to: string) {
window.location.hash = to;
}
+// ── Theme (applied to the demo chrome + the rendered MDMA examples) ────────────
+
+const THEME_MODES: ThemeMode[] = ['light', 'dark', 'auto'];
+const THEME_ICON: Record = { light: '☀️', dark: '🌙', auto: '🖥️' };
+
+function useThemeMode(): [ThemeMode, (m: ThemeMode) => void] {
+ const [mode, setMode] = useState(() => {
+ const saved = window.localStorage.getItem('mdma-demo-theme');
+ return saved === 'light' || saved === 'dark' || saved === 'auto' ? saved : 'light';
+ });
+ useEffect(() => {
+ window.localStorage.setItem('mdma-demo-theme', mode);
+ }, [mode]);
+ return [mode, setMode];
+}
+
+/**
+ * Resolve `'auto'` to a concrete `'light'`/`'dark'` by watching the OS
+ * preference, so the demo can drive both its chrome and the MDMA examples off a
+ * single attribute (no `prefers-color-scheme` duplication in the CSS).
+ */
+function useResolvedTheme(mode: ThemeMode): 'light' | 'dark' {
+ const [systemDark, setSystemDark] = useState(
+ () => window.matchMedia('(prefers-color-scheme: dark)').matches,
+ );
+ useEffect(() => {
+ const mq = window.matchMedia('(prefers-color-scheme: dark)');
+ const sync = () => setSystemDark(mq.matches);
+ mq.addEventListener('change', sync);
+ return () => mq.removeEventListener('change', sync);
+ }, []);
+ if (mode === 'auto') return systemDark ? 'dark' : 'light';
+ return mode;
+}
+
+function ThemeToggle({ mode, onChange }: { mode: ThemeMode; onChange: (m: ThemeMode) => void }) {
+ return (
+
+ {THEME_MODES.map((m) => (
+ onChange(m)}
+ title={`${m[0].toUpperCase()}${m.slice(1)} theme for examples`}
+ aria-pressed={mode === m}
+ >
+ {THEME_ICON[m]}
+
+ ))}
+
+ );
+}
+
// ── Nav config ───────────────────────────────────────────────────────────────
type Route = '/' | '/chat' | '/preview' | '/author' | '/custom' | '/validator' | '/docs';
@@ -96,6 +152,8 @@ export function App() {
const [dropdownOpen, setDropdownOpen] = useState(false);
const dropdownRef = useRef(null);
const stars = useGitHubStars('MobileReality/mdma');
+ const [themeMode, setThemeMode] = useThemeMode();
+ const resolvedTheme = useResolvedTheme(themeMode);
useEffect(() => {
function handleClick(e: MouseEvent) {
@@ -107,103 +165,116 @@ export function App() {
return () => document.removeEventListener('mousedown', handleClick);
}, []);
+ // Drive dark mode from the root element so the whole page (including the body
+ // background behind transparent panels) picks up the `[data-theme='dark']`
+ // chrome overrides.
+ useEffect(() => {
+ document.documentElement.setAttribute('data-theme', resolvedTheme);
+ return () => document.documentElement.removeAttribute('data-theme');
+ }, [resolvedTheme]);
+
return (
-
-
-
-
navigate('/')}>
-
-
-
The open standard for AI-generated interactive UI
-
-
-
-
-
-
-
-
-
- Star
- {stars !== null && {stars.toLocaleString()} }
-
- {route !== '/' && (
-
-
setDropdownOpen((v) => !v)}
- >
- {labelForPath(route)}
-
+
+
+
+
+ {route === '/' ? (
+
+ ) : route.startsWith('/docs') ? (
+
+ ) : route === '/validator' ? (
+
+ ) : route === '/custom' ? (
+
+ ) : route === '/author' ? (
+
+ ) : route === '/preview' ? (
+
+ ) : (
+
)}
-
-
-
- {route === '/' ? (
-
- ) : route.startsWith('/docs') ? (
-
- ) : route === '/validator' ? (
-
- ) : route === '/custom' ? (
-
- ) : route === '/author' ? (
-
- ) : route === '/preview' ? (
-
- ) : (
-
- )}
-
+
+
+
);
}
diff --git a/demo/src/ChatView.tsx b/demo/src/ChatView.tsx
index 50efc1b..8deacbe 100644
--- a/demo/src/ChatView.tsx
+++ b/demo/src/ChatView.tsx
@@ -128,21 +128,7 @@ export function ChatView({
Describe an interactive document and the AI will generate it, or try an example
flow:
-
+
Load an example flow…
diff --git a/demo/src/docs/DocsView.tsx b/demo/src/docs/DocsView.tsx
index 9357f2e..3b6671a 100644
--- a/demo/src/docs/DocsView.tsx
+++ b/demo/src/docs/DocsView.tsx
@@ -12,6 +12,8 @@ import { PACKAGES, PackageDetail } from './sections/PackageDetail.js';
import { Packages } from './sections/Packages.js';
import { PromptMatrix } from './sections/PromptMatrix.js';
import { ReactNative, ReactNativeSnack } from './sections/ReactNative.js';
+import { ReactWeb } from './sections/ReactWeb.js';
+import { Theming, ThemingPreview, ThemingProvider } from './sections/Theming.js';
import { Usage, UsageHydrationPreview } from './sections/Usage.js';
import { Validator } from './sections/Validator.js';
@@ -26,14 +28,13 @@ interface Section {
component?: React.ComponentType;
}
-const SECTIONS: Section[] = [
+const MAIN_SECTIONS: Section[] = [
{ slug: 'introduction', label: 'Introduction', component: Introduction },
{ slug: 'packages', label: 'Packages' },
{ slug: 'installation', label: 'Installation', component: Installation },
{ slug: 'usage', label: 'Usage', component: Usage },
- { slug: 'integrations', label: 'Integrations' },
{ slug: 'components', label: 'Components' },
- { slug: 'react-native', label: 'React Native', component: ReactNative },
+ { slug: 'theming', label: 'Theming', component: Theming },
{ slug: 'validator', label: 'Validator', component: Validator },
{ slug: 'mcp', label: 'MCP & Skills', component: Mcp },
{ slug: 'cli', label: 'CLI', component: Cli },
@@ -45,6 +46,31 @@ const SECTIONS: Section[] = [
{ slug: 'prompt-matrix', label: 'Prompt Matrix', component: PromptMatrix },
];
+const INTEGRATION_SECTIONS: Section[] = INTEGRATIONS.map((i) => ({
+ slug: `integrations/${i.slug}`,
+ label: i.label,
+}));
+
+const RENDERER_SECTIONS: Section[] = [
+ { slug: 'react', label: 'React', component: ReactWeb },
+ { slug: 'react-native', label: 'React Native', component: ReactNative },
+];
+
+const SECTIONS: Section[] = [...MAIN_SECTIONS, ...INTEGRATION_SECTIONS, ...RENDERER_SECTIONS];
+
+/** Grouped page list for the mobile breadcrumb picker. Includes the Packages
+ * sub-pages (which expand under "Packages" in the desktop sidebar) so they're
+ * reachable on mobile too. */
+const BREADCRUMB_GROUPS = [
+ { label: 'Documentation', items: MAIN_SECTIONS },
+ {
+ label: 'Packages',
+ items: PACKAGES.map((p) => ({ slug: `packages/${p.slug}`, label: p.label })),
+ },
+ { label: 'Integrations', items: INTEGRATION_SECTIONS },
+ { label: 'Renderers', items: RENDERER_SECTIONS },
+];
+
function getDocsSlug(): string {
const hash = window.location.hash.slice(1); // e.g. /docs/packages/runtime
const sub = hash.startsWith('/docs/') ? hash.slice('/docs/'.length) : '';
@@ -59,6 +85,8 @@ export function DocsView() {
const [active, setActiveState] = useState(getDocsSlug);
const [selectedComponent, setSelectedComponent] = useState('form');
const [usageExampleOpen, setUsageExampleOpen] = useState(false);
+ // Mobile hamburger: whether the nav drawer is open.
+ const [navOpen, setNavOpen] = useState(false);
useEffect(() => {
function sync() {
@@ -71,12 +99,15 @@ export function DocsView() {
function setActive(slug: string) {
navigateDocs(slug);
setActiveState(slug);
+ setNavOpen(false); // close the mobile drawer after navigating
}
const showComponentsPreview = active === 'components';
const showUsagePreview = active === 'usage' && usageExampleOpen;
const showRnPreview = active === 'react-native';
- const showPreview = showComponentsPreview || showUsagePreview || showRnPreview;
+ const showThemingPreview = active === 'theming';
+ const showPreview =
+ showComponentsPreview || showUsagePreview || showRnPreview || showThemingPreview;
const previewEntry = COMPONENTS.find((c) => c.type === selectedComponent) ?? COMPONENTS[0];
const isPackagesActive = active === 'packages' || active.startsWith('packages/');
@@ -121,70 +152,110 @@ export function DocsView() {
(s.slug === 'packages' && isPackagesActive) ||
(s.slug === 'integrations' && isIntegrationsActive);
- return (
-
-
- Documentation
- {SECTIONS.map((s) => (
-
+ const renderNavItem = (s: Section) => (
+
+
setActive(s.slug)}
+ >
+ {s.label}
+
+
+ {s.slug === 'packages' && isPackagesActive && (
+
+ {PACKAGES.map((pkg) => (
setActive(s.slug)}
+ className={`docs-nav-sub-item${activePackageSlug === pkg.slug ? ' docs-nav-sub-item--active' : ''}`}
+ onClick={() => setActive(`packages/${pkg.slug}`)}
>
- {s.label}
+ {pkg.label}
+ ))}
+
+ )}
- {s.slug === 'packages' && isPackagesActive && (
-
- {PACKAGES.map((pkg) => (
- setActive(`packages/${pkg.slug}`)}
- >
- {pkg.label}
-
- ))}
-
- )}
-
- {s.slug === 'integrations' && isIntegrationsActive && (
-
- {INTEGRATIONS.map((integration) => (
- setActive(`integrations/${integration.slug}`)}
- >
- {integration.label}
-
- ))}
-
- )}
-
- ))}
-
-
-
{renderContent()}
-
- {showPreview && (
-
- {showComponentsPreview ? (
-
- ) : showRnPreview ? (
-
- ) : (
-
- )}
-
+ {s.slug === 'integrations' && isIntegrationsActive && (
+
+ {INTEGRATIONS.map((integration) => (
+ setActive(`integrations/${integration.slug}`)}
+ >
+ {integration.label}
+
+ ))}
+
)}
);
+
+ // Mobile breadcrumb trail: which group the current page is in + its label.
+ const bcValue = SECTIONS.some((s) => s.slug === active)
+ ? active
+ : active.startsWith('packages/')
+ ? active
+ : 'introduction';
+ const bcItem = BREADCRUMB_GROUPS.flatMap((g) => g.items).find((s) => s.slug === bcValue);
+ const bcGroup =
+ BREADCRUMB_GROUPS.find((g) => g.items.some((s) => s.slug === bcValue))?.label ??
+ 'Documentation';
+
+ return (
+
+
+ {/* Mobile-only: hamburger toggles the nav drawer; breadcrumb trail below. */}
+
+
setNavOpen((v) => !v)}
+ aria-label="Toggle documentation navigation"
+ aria-expanded={navOpen}
+ >
+ {navOpen ? '✕' : '☰'}
+
+
+ Docs
+ ›
+ {bcGroup}
+ ›
+ {bcItem?.label ?? 'Introduction'}
+
+
+
+ Documentation
+ {MAIN_SECTIONS.map(renderNavItem)}
+ Integrations
+ {INTEGRATION_SECTIONS.map(renderNavItem)}
+ Renderers
+ {RENDERER_SECTIONS.map(renderNavItem)}
+
+
+
{renderContent()}
+
+ {showPreview && (
+
+ {showComponentsPreview ? (
+
+ ) : showRnPreview ? (
+
+ ) : showThemingPreview ? (
+
+ ) : (
+
+ )}
+
+ )}
+
+
+ );
}
diff --git a/demo/src/docs/sections/Components.tsx b/demo/src/docs/sections/Components.tsx
index a9df8a7..78001d5 100644
--- a/demo/src/docs/sections/Components.tsx
+++ b/demo/src/docs/sections/Components.tsx
@@ -4,7 +4,6 @@ import type { MdmaRoot } from '@mobile-reality/mdma-spec';
import type { DocumentStore } from '@mobile-reality/mdma-runtime';
import { parseMarkdown } from '../../chat/parse-markdown.js';
import { ChartRenderer } from '../../chart-components.js';
-import { Code } from '../Code.js';
const CUSTOMIZATIONS = {
components: { chart: ChartRenderer },
@@ -226,8 +225,8 @@ export function Components({ selected, onSelect }: ComponentsProps) {
<>
Components
- 9 built-in component types, rendered out of the box by{' '}
- @mobile-reality/mdma-renderer-react. Click a row to preview.
+ 9 built-in component types defined by the MDMA spec — every renderer (React, React Native)
+ renders them out of the box. Click a row to preview.
@@ -256,32 +255,6 @@ export function Components({ selected, onSelect }: ComponentsProps) {
-
- Custom Chart Renderer
-
- The built-in chart renderer renders data as a plain table so the library stays lightweight.
- To get actual charts, register a custom renderer:
-
- {`import { MdmaDocument } from '@mobile-reality/mdma-renderer-react';
-import { MyRechartsRenderer } from './MyRechartsRenderer';
-
-function App({ ast, store }) {
- return (
-
- );
-}`}
-
- This pattern works for overriding any built-in component — pass a custom React component
- under customizations.components.<type>.
-
>
);
}
diff --git a/demo/src/docs/sections/PackageDetail.tsx b/demo/src/docs/sections/PackageDetail.tsx
index 9c92267..1f72c26 100644
--- a/demo/src/docs/sections/PackageDetail.tsx
+++ b/demo/src/docs/sections/PackageDetail.tsx
@@ -121,16 +121,6 @@ const ast = await processor.run(tree) as MdmaRoot;`}
purpose:
"Bridges the runtime's action dispatcher and each component's specific state shape. Covers form, button, tasklist, table, callout, approval-gate, and webhook — the components that manage state. Rendering layers such as mdma-renderer-react use these handlers to wire UI events to the runtime without re-implementing interaction logic per renderer.",
},
- {
- slug: 'renderer-react',
- label: 'mdma-renderer-react',
- npm: '@mobile-reality/mdma-renderer-react',
- dir: 'renderer-react',
- tagline: 'React rendering layer for all 9 MDMA component types.',
- purpose:
- 'Ships accessible, styled React components for every MDMA type plus hooks for reading and dispatching runtime state. Supports a customizations.components prop so you can swap any built-in component for your own renderer — useful for replacing the default chart table with a real charting library. The companion styles.css provides default visual styling and is entirely optional.',
- seeAlso: { label: 'Components', slug: 'components' },
- },
{
slug: 'prompt-pack',
label: 'mdma-prompt-pack',
diff --git a/demo/src/docs/sections/Packages.tsx b/demo/src/docs/sections/Packages.tsx
index b239cb1..1c2326b 100644
--- a/demo/src/docs/sections/Packages.tsx
+++ b/demo/src/docs/sections/Packages.tsx
@@ -41,10 +41,6 @@ export function Packages({ onNavigate }: PackagesProps) {
navLink('attachables-core', 'mdma-attachables-core', onNavigate),
'Handlers for 7 of the 9 component types — the ones that manage state (form, button, tasklist, table, callout, approval-gate, webhook).',
],
- [
- navLink('renderer-react', 'mdma-renderer-react', onNavigate),
- 'React rendering layer with components for all 9 MDMA types and hooks for state access.',
- ],
[
navLink('prompt-pack', 'mdma-prompt-pack', onNavigate),
'System prompts that teach LLMs how to author valid MDMA documents. Ships model-specialised variants for OpenAI, Anthropic, Google, and xAI.',
@@ -71,7 +67,7 @@ export function Packages({ onNavigate }: PackagesProps) {
├── mdma-validator Document validation
└── mdma-runtime State / events / policy engine
└── mdma-attachables-core Component handlers
- └── mdma-renderer-react React components
+ └── mdma-renderer-* Renderers
mdma-cli CLI prompt builder + validation
mdma-mcp MCP server for AI assistants`}
>
diff --git a/demo/src/docs/sections/ReactNativePreview.tsx b/demo/src/docs/sections/ReactNativePreview.tsx
index a2b39c2..956023b 100644
--- a/demo/src/docs/sections/ReactNativePreview.tsx
+++ b/demo/src/docs/sections/ReactNativePreview.tsx
@@ -1,11 +1,17 @@
-import { MdmaDocument } from '@mobile-reality/mdma-renderer-react-native';
+import {
+ MdmaDocument,
+ lightTheme,
+ darkTheme,
+ type MdmaTheme,
+} from '@mobile-reality/mdma-renderer-react-native';
import { type DocumentStore, createDocumentStore } from '@mobile-reality/mdma-runtime';
import type { MdmaRoot } from '@mobile-reality/mdma-spec';
// Renders live in the browser: `react-native` is aliased to `react-native-web`
// (see vite.config.ts), so this is the exact renderer-react-native code running
// as a web preview — the "emulator" shown in the docs. No code editor, no API
// key; the four responses are pre-parsed and matched by keyword.
-import { useEffect, useRef, useState } from 'react';
+import { useEffect, useMemo, useRef, useState } from 'react';
+import { useDemoThemeMode } from '../../theme-context.js';
import {
ActivityIndicator,
Pressable,
@@ -35,6 +41,9 @@ function pick(text: string, n: number) {
}
export function ReactNativePreview() {
+ const themeMode = useDemoThemeMode();
+ const t = themeMode === 'dark' ? darkTheme : lightTheme;
+ const styles = useMemo(() => makeStyles(t), [t]);
const [messages, setMessages] = useState([]);
const [input, setInput] = useState('');
const countRef = useRef(0);
@@ -112,13 +121,13 @@ export function ReactNativePreview() {
) : (
-
+
Generating…
)}
{m.done && m.ast && m.store ? (
-
+
) : null}
@@ -140,7 +149,7 @@ export function ReactNativePreview() {
value={input}
onChangeText={setInput}
placeholder="Message…"
- placeholderTextColor="#9ca3af"
+ placeholderTextColor={t.colors.textMuted}
onSubmitEditing={() => send()}
returnKeyType="send"
/>
@@ -156,111 +165,124 @@ export function ReactNativePreview() {
);
}
-const BLUE = '#2563eb';
-const BORDER = '#e5e7eb';
+/**
+ * Build the phone-mock chrome styles from the MDMA theme so the preview shell
+ * (bubbles, bars, chips) tracks the same light/dark palette as the rendered
+ * MDMA content inside it. `screen` is the area behind the bubbles — a touch
+ * darker/lighter than the card surfaces.
+ */
+function makeStyles(t: MdmaTheme) {
+ const { colors: c } = t;
+ const screen = themeIsDark(c) ? '#0e1119' : '#f3f4f6';
+ return StyleSheet.create({
+ page: { flex: 1, backgroundColor: screen },
+ flex: { flex: 1 },
-const styles = StyleSheet.create({
- page: { flex: 1, backgroundColor: '#f3f4f6' },
- flex: { flex: 1 },
+ header: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ gap: 10,
+ paddingHorizontal: 14,
+ paddingVertical: 12,
+ backgroundColor: c.background,
+ borderBottomWidth: 1,
+ borderBottomColor: c.border,
+ },
+ avatar: {
+ width: 34,
+ height: 34,
+ borderRadius: 17,
+ backgroundColor: c.primary,
+ alignItems: 'center',
+ justifyContent: 'center',
+ },
+ avatarText: { color: c.onPrimary, fontWeight: '700', fontSize: 16 },
+ title: { fontSize: 15, fontWeight: '700', color: c.text },
+ subtitle: { fontSize: 12, color: c.textMuted, marginTop: 1 },
- header: {
- flexDirection: 'row',
- alignItems: 'center',
- gap: 10,
- paddingHorizontal: 14,
- paddingVertical: 12,
- backgroundColor: '#ffffff',
- borderBottomWidth: 1,
- borderBottomColor: BORDER,
- },
- avatar: {
- width: 34,
- height: 34,
- borderRadius: 17,
- backgroundColor: BLUE,
- alignItems: 'center',
- justifyContent: 'center',
- },
- avatarText: { color: '#ffffff', fontWeight: '700', fontSize: 16 },
- title: { fontSize: 15, fontWeight: '700', color: '#111827' },
- subtitle: { fontSize: 12, color: '#6b7280', marginTop: 1 },
+ scroll: { padding: 12, gap: 12 },
+ empty: { paddingVertical: 40, paddingHorizontal: 12, gap: 8, alignItems: 'center' },
+ emptyTitle: { fontSize: 15, fontWeight: '700', color: c.text, textAlign: 'center' },
+ emptyHint: { fontSize: 13, color: c.textMuted, textAlign: 'center', lineHeight: 19 },
- scroll: { padding: 12, gap: 12 },
- empty: { paddingVertical: 40, paddingHorizontal: 12, gap: 8, alignItems: 'center' },
- emptyTitle: { fontSize: 15, fontWeight: '700', color: '#111827', textAlign: 'center' },
- emptyHint: { fontSize: 13, color: '#6b7280', textAlign: 'center', lineHeight: 19 },
+ userRow: { alignItems: 'flex-end' },
+ user: {
+ maxWidth: '85%',
+ backgroundColor: c.primary,
+ borderRadius: 18,
+ borderBottomRightRadius: 5,
+ paddingVertical: 9,
+ paddingHorizontal: 13,
+ },
+ userText: { color: c.onPrimary, fontSize: 15, lineHeight: 20 },
- userRow: { alignItems: 'flex-end' },
- user: {
- maxWidth: '85%',
- backgroundColor: BLUE,
- borderRadius: 18,
- borderBottomRightRadius: 5,
- paddingVertical: 9,
- paddingHorizontal: 13,
- },
- userText: { color: '#ffffff', fontSize: 15, lineHeight: 20 },
+ bot: {
+ alignSelf: 'stretch',
+ backgroundColor: c.background,
+ borderRadius: 18,
+ borderBottomLeftRadius: 5,
+ borderWidth: 1,
+ borderColor: c.border,
+ padding: 12,
+ },
+ botText: { color: c.text, fontSize: 15, lineHeight: 22 },
+ typing: { flexDirection: 'row', alignItems: 'center', gap: 8 },
+ typingText: { color: c.textMuted, fontSize: 14 },
+ doc: { marginTop: 10 },
- bot: {
- alignSelf: 'stretch',
- backgroundColor: '#ffffff',
- borderRadius: 18,
- borderBottomLeftRadius: 5,
- borderWidth: 1,
- borderColor: BORDER,
- padding: 12,
- },
- botText: { color: '#1f2937', fontSize: 15, lineHeight: 22 },
- typing: { flexDirection: 'row', alignItems: 'center', gap: 8 },
- typingText: { color: '#9ca3af', fontSize: 14 },
- doc: { marginTop: 10 },
+ chipsWrap: {
+ flexDirection: 'row',
+ flexWrap: 'wrap',
+ gap: 8,
+ paddingHorizontal: 10,
+ paddingVertical: 10,
+ backgroundColor: c.background,
+ borderTopWidth: 1,
+ borderTopColor: c.border,
+ },
+ chip: {
+ paddingVertical: 7,
+ paddingHorizontal: 13,
+ borderRadius: 999,
+ borderWidth: 1,
+ borderColor: c.border,
+ backgroundColor: c.surface,
+ },
+ chipText: { color: c.text, fontSize: 13, fontWeight: '500' },
- chipsWrap: {
- flexDirection: 'row',
- flexWrap: 'wrap',
- gap: 8,
- paddingHorizontal: 10,
- paddingVertical: 10,
- backgroundColor: '#ffffff',
- borderTopWidth: 1,
- borderTopColor: BORDER,
- },
- chip: {
- paddingVertical: 7,
- paddingHorizontal: 13,
- borderRadius: 999,
- borderWidth: 1,
- borderColor: BORDER,
- backgroundColor: '#f9fafb',
- },
- chipText: { color: '#374151', fontSize: 13, fontWeight: '500' },
+ bar: {
+ flexDirection: 'row',
+ alignItems: 'center',
+ gap: 8,
+ paddingHorizontal: 10,
+ paddingVertical: 10,
+ backgroundColor: c.background,
+ borderTopWidth: 1,
+ borderTopColor: c.border,
+ },
+ input: {
+ flex: 1,
+ borderWidth: 1,
+ borderColor: c.border,
+ borderRadius: 22,
+ paddingHorizontal: 15,
+ paddingVertical: 10,
+ fontSize: 15,
+ color: c.text,
+ backgroundColor: c.surface,
+ },
+ send: {
+ backgroundColor: c.primary,
+ borderRadius: 22,
+ paddingHorizontal: 18,
+ paddingVertical: 10,
+ },
+ sendDisabled: { opacity: 0.45 },
+ sendText: { color: c.onPrimary, fontWeight: '600', fontSize: 15 },
+ });
+}
- bar: {
- flexDirection: 'row',
- alignItems: 'center',
- gap: 8,
- paddingHorizontal: 10,
- paddingVertical: 10,
- backgroundColor: '#ffffff',
- borderTopWidth: 1,
- borderTopColor: BORDER,
- },
- input: {
- flex: 1,
- borderWidth: 1,
- borderColor: BORDER,
- borderRadius: 22,
- paddingHorizontal: 15,
- paddingVertical: 10,
- fontSize: 15,
- backgroundColor: '#f9fafb',
- },
- send: {
- backgroundColor: BLUE,
- borderRadius: 22,
- paddingHorizontal: 18,
- paddingVertical: 10,
- },
- sendDisabled: { opacity: 0.45 },
- sendText: { color: '#ffffff', fontWeight: '600', fontSize: 15 },
-});
+/** The dark palette's background is a dark navy; the light one is white. */
+function themeIsDark(colors: MdmaTheme['colors']): boolean {
+ return colors.background.toLowerCase() !== '#ffffff';
+}
diff --git a/demo/src/docs/sections/ReactWeb.tsx b/demo/src/docs/sections/ReactWeb.tsx
new file mode 100644
index 0000000..38d1de6
--- /dev/null
+++ b/demo/src/docs/sections/ReactWeb.tsx
@@ -0,0 +1,72 @@
+import { Code } from '../Code.js';
+
+export function ReactWeb() {
+ return (
+ <>
+ React
+
+ @mobile-reality/mdma-renderer-react renders MDMA documents as web UI — every
+ live example on this site is produced by it. It consumes the headless spec +{' '}
+ runtime stack unchanged and turns a parsed document into interactive React
+ components with full state, bindings, actions, policy, audit, and PII redaction.
+
+
+ Install
+ {`npm install @mobile-reality/mdma-renderer-react \\
+ @mobile-reality/mdma-spec @mobile-reality/mdma-runtime react`}
+
+ Usage
+
+ Parse a document to an AST + store (with @mobile-reality/mdma-parser), import
+ the stylesheet once, then hand both to MdmaDocument.
+
+ {`import { unified } from 'unified';
+import remarkParse from 'remark-parse';
+import { remarkMdma } from '@mobile-reality/mdma-parser';
+import { createDocumentStore } from '@mobile-reality/mdma-runtime';
+import { MdmaDocument } from '@mobile-reality/mdma-renderer-react';
+import '@mobile-reality/mdma-renderer-react/styles.css';
+
+const processor = unified().use(remarkParse).use(remarkMdma, {});
+
+export function Document({ markdown }: { markdown: string }) {
+ const [doc, setDoc] = useState(null);
+ useEffect(() => {
+ (async () => {
+ const ast = await processor.run(processor.parse(markdown), markdown);
+ setDoc({ ast, store: createDocumentStore(ast) });
+ })();
+ }, [markdown]);
+
+ if (!doc) return null;
+ return ;
+}`}
+
+ Styling & theming
+
+ The imported styles.css is driven by --mdma-* CSS variables. Pass
+ a theme prop ("light" | "dark" | "auto", or a full token object)
+ to MdmaDocument — the Theming page has a live editor.
+
+
+ Customizing components
+
+ Override any built-in component through customizations.components.<type>.
+ For example, the built-in chart renderer draws data as a plain table so the library stays
+ lightweight — swap in a real chart with a custom renderer:
+
+ {`import { MdmaDocument } from '@mobile-reality/mdma-renderer-react';
+import { MyRechartsRenderer } from './MyRechartsRenderer';
+
+function App({ ast, store }) {
+ return (
+
+ );
+}`}
+ >
+ );
+}
diff --git a/demo/src/docs/sections/Theming.tsx b/demo/src/docs/sections/Theming.tsx
new file mode 100644
index 0000000..cc85db2
--- /dev/null
+++ b/demo/src/docs/sections/Theming.tsx
@@ -0,0 +1,313 @@
+import {
+ createContext,
+ useContext,
+ useEffect,
+ useMemo,
+ useRef,
+ useState,
+ type ReactNode,
+} from 'react';
+import {
+ MdmaDocument,
+ darkTheme,
+ lightTheme,
+ type MdmaTheme,
+} from '@mobile-reality/mdma-renderer-react';
+import type { MdmaRoot } from '@mobile-reality/mdma-spec';
+import type { DocumentStore } from '@mobile-reality/mdma-runtime';
+import { parseMarkdown } from '../../chat/parse-markdown.js';
+import { useDemoThemeMode } from '../../theme-context.js';
+import { Code } from '../Code.js';
+import { Table } from '../Table.js';
+
+/** The five styleable tokens exposed as color pickers in the playground. */
+const TOKENS = [
+ { key: 'primary', label: 'Accent' },
+ { key: 'background', label: 'Background' },
+ { key: 'text', label: 'Text' },
+ { key: 'textMuted', label: 'Muted' },
+ { key: 'border', label: 'Border' },
+] as const;
+
+type TokenKey = (typeof TOKENS)[number]['key'];
+type Editable = Record;
+
+function pickEditable(theme: MdmaTheme): Editable {
+ return {
+ primary: theme.colors.primary,
+ background: theme.colors.background,
+ text: theme.colors.text,
+ textMuted: theme.colors.textMuted,
+ border: theme.colors.border,
+ };
+}
+
+/** A small doc that exercises all five tokens: card backgrounds + borders
+ * (background/border), body + header text (text/muted), and the accent
+ * (primary) on the button, checkbox, and input focus. */
+const PLAYGROUND_DOC = `\`\`\`mdma
+type: form
+id: play-form
+label: "Create account"
+onSubmit: play-submit
+fields:
+ - name: full-name
+ type: text
+ label: "Full name"
+ - name: notify
+ type: checkbox
+ label: "Email me product updates"
+\`\`\`
+
+\`\`\`mdma
+type: button
+id: play-button
+text: "Get started"
+variant: primary
+\`\`\`
+
+\`\`\`mdma
+type: table
+id: play-table
+columns:
+ - key: plan
+ label: "Plan"
+ type: text
+ - key: seats
+ label: "Seats"
+ type: number
+data:
+ - plan: "Starter"
+ seats: 3
+ - plan: "Team"
+ seats: 12
+\`\`\``;
+
+interface ThemingCtxValue {
+ colors: Editable;
+ setColor: (key: TokenKey, value: string) => void;
+ reset: () => void;
+ theme: MdmaTheme;
+ parsed: { ast: MdmaRoot; store: DocumentStore } | null;
+}
+
+const ThemingContext = createContext(null);
+
+function useThemingCtx(): ThemingCtxValue {
+ const ctx = useContext(ThemingContext);
+ if (!ctx) throw new Error('Theming components must be used within ');
+ return ctx;
+}
+
+/** Holds the live-editor state so the controls (in the docs content) and the
+ * preview (in the right-hand panel) share it. */
+export function ThemingProvider({ children }: { children: ReactNode }) {
+ const mode = useDemoThemeMode();
+ const base = mode === 'dark' ? darkTheme : lightTheme;
+ const [colors, setColors] = useState(() => pickEditable(base));
+ const [parsed, setParsed] = useState<{ ast: MdmaRoot; store: DocumentStore } | null>(null);
+ const cancelRef = useRef(false);
+
+ useEffect(() => {
+ cancelRef.current = false;
+ parseMarkdown(PLAYGROUND_DOC).then((result) => {
+ if (!cancelRef.current) setParsed(result);
+ });
+ return () => {
+ cancelRef.current = true;
+ };
+ }, []);
+
+ // Reset the pickers to the built-in palette when the light/dark toggle flips.
+ useEffect(() => {
+ setColors(pickEditable(mode === 'dark' ? darkTheme : lightTheme));
+ }, [mode]);
+
+ const theme: MdmaTheme = useMemo(
+ () => ({ ...base, colors: { ...base.colors, ...colors } }),
+ [base, colors],
+ );
+
+ const value = useMemo(
+ () => ({
+ colors,
+ setColor: (key, v) => setColors((c) => ({ ...c, [key]: v })),
+ reset: () => setColors(pickEditable(base)),
+ theme,
+ parsed,
+ }),
+ [colors, base, theme, parsed],
+ );
+
+ return {children} ;
+}
+
+/** The color pickers — rendered inline in the docs content. */
+function ThemingControls() {
+ const { colors, setColor, reset } = useThemingCtx();
+ return (
+
+ {TOKENS.map((t) => (
+
+ setColor(t.key, e.target.value)}
+ />
+ {t.label}
+ {colors[t.key]}
+
+ ))}
+
+ Reset
+
+
+ );
+}
+
+/** The live render — shown in the docs right-hand preview panel. */
+export function ThemingPreview() {
+ const { theme, parsed } = useThemingCtx();
+ return (
+ <>
+
+ Live preview
+ theme
+
+
+
+ {parsed ? (
+
+ ) : (
+ Loading…
+ )}
+
+
+ >
+ );
+}
+
+export function Theming() {
+ return (
+ <>
+ Theming
+
+ Both renderers ship with a polished default look and a first-class theming layer on top of
+ it. Theming is opt-in : render a document with no theme and you
+ get the built-in light palette, unchanged. When you want more, switch to a dark palette,
+ follow the operating-system preference, or hand over a fully custom set of design tokens.
+
+ Try it
+
+ Pick colors for five styleable tokens — accent , background
+ , text , muted , and border . Each one is
+ written onto a custom MdmaTheme and the components below re-render instantly.
+ Hover a button or focus the input — the hover and focus states derive from your accent
+ automatically. Toggle the top-bar theme to reset the pickers to the light or dark palette.
+
+
+
+ The three modes
+
+ Everything is driven by the theme prop on MdmaDocument:
+
+ Default, 'Default light palette — fully backward compatible.'],
+ ["light", 'The built-in light palette.'],
+ ["dark", 'The built-in dark palette.'],
+ ["auto", 'Follows the OS preference (prefers-color-scheme).'],
+ [MdmaTheme, 'Your custom tokens.'],
+ ]}
+ />
+ {` // not themed (default light)
+ // built-in dark
+ // follow the OS
+ // custom`}
+
+ The MdmaTheme token shape
+ A theme is a plain object. This is the whole contract:
+ {`interface MdmaTheme {
+ colors: {
+ background: string;
+ surface: string;
+ text: string;
+ textMuted: string;
+ border: string;
+ primary: string;
+ onPrimary: string;
+ secondary: string;
+ onSecondary: string;
+ danger: string;
+ onDanger: string;
+ // Callout accents + soft backgrounds
+ info: string; infoBg: string;
+ warning: string; warningBg: string;
+ error: string; errorBg: string;
+ success: string; successBg: string;
+ };
+ spacing: { xs: number; sm: number; md: number; lg: number };
+ radius: { sm: number; md: number };
+ fontSize: { small: number; body: number; label: number; title: number };
+}`}
+
+ The lightTheme and darkTheme presets are exported from both
+ renderer packages, so the easiest way to build a custom theme is to spread a preset and
+ override just what you need (exactly what the playground above does):
+
+ {`import { lightTheme, type MdmaTheme } from '@mobile-reality/mdma-renderer-react';
+
+const brandTheme: MdmaTheme = {
+ ...lightTheme,
+ colors: { ...lightTheme.colors, primary: '#e91e63', onPrimary: '#ffffff' },
+ radius: { sm: 10, md: 16 },
+};`}
+
+ Web renderer
+
+ Import the stylesheet once, then pass a theme:
+
+ {`import { MdmaDocument } from '@mobile-reality/mdma-renderer-react';
+import '@mobile-reality/mdma-renderer-react/styles.css';
+
+ ;`}
+
+ Under the hood the stylesheet expresses every color, radius, and size as a{' '}
+ --mdma-* CSS variable scoped to .mdma-document. Built-in palettes
+ apply via a data-theme attribute; a custom MdmaTheme is written as
+ inline CSS variables. Because it's all CSS variables, you can theme{' '}
+ without the prop at all — just set the variables in your own CSS:
+
+ {`.mdma-document {
+ --mdma-color-primary: #e91e63;
+ --mdma-radius-md: 16px;
+}`}
+
+ A MdmaDocument with no theme of its own inherits the theme from an
+ ancestor MdmaThemeProvider, so one provider can theme a whole app. Rendering a
+ lone <MdmaBlock> outside MdmaDocument? Wrap it so the
+ variables cascade:
+
+ {`import { MdmaThemeProvider, MdmaBlock } from '@mobile-reality/mdma-renderer-react';
+
+
+
+ ;`}
+
+ React Native renderer
+
+ Same prop, no stylesheet — each renderer builds its StyleSheet from the
+ resolved tokens, which any custom component can read with useMdmaTheme():
+
+ {`import { MdmaDocument, useMdmaTheme } from '@mobile-reality/mdma-renderer-react-native';
+
+ ;
+
+function Badge() {
+ const theme = useMdmaTheme();
+ return ;
+}`}
+ >
+ );
+}
diff --git a/demo/src/styles.css b/demo/src/styles.css
index 0fba458..bb3e21f 100644
--- a/demo/src/styles.css
+++ b/demo/src/styles.css
@@ -16,6 +16,40 @@ body {
background: #f5f6fa;
}
+/* ===== Scrollbars (thin, subtle, theme-aware) ===== */
+* {
+ scrollbar-width: thin;
+ scrollbar-color: rgba(120, 120, 140, 0.4) transparent;
+}
+::-webkit-scrollbar {
+ width: 10px;
+ height: 10px;
+}
+::-webkit-scrollbar-track {
+ background: transparent;
+}
+::-webkit-scrollbar-thumb {
+ background-color: rgba(120, 120, 140, 0.35);
+ border-radius: 8px;
+ border: 2px solid transparent;
+ background-clip: padding-box;
+}
+::-webkit-scrollbar-thumb:hover {
+ background-color: rgba(120, 120, 140, 0.6);
+}
+::-webkit-scrollbar-corner {
+ background: transparent;
+}
+[data-theme="dark"] * {
+ scrollbar-color: rgba(255, 255, 255, 0.22) transparent;
+}
+[data-theme="dark"] ::-webkit-scrollbar-thumb {
+ background-color: rgba(255, 255, 255, 0.18);
+}
+[data-theme="dark"] ::-webkit-scrollbar-thumb:hover {
+ background-color: rgba(255, 255, 255, 0.32);
+}
+
/* ===== Home View ===== */
.home-view {
flex: 1;
@@ -207,6 +241,43 @@ body {
gap: 12px;
}
+/* Segmented light/dark/auto control — themes the rendered MDMA examples. */
+.demo-theme-toggle {
+ display: flex;
+ align-items: center;
+ gap: 2px;
+ padding: 3px;
+ background: #f5f3fb;
+ border: 1px solid #d1c9f0;
+ border-radius: 8px;
+ flex-shrink: 0;
+}
+
+.demo-theme-btn {
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 28px;
+ height: 26px;
+ padding: 0;
+ font-size: 14px;
+ line-height: 1;
+ background: transparent;
+ border: none;
+ border-radius: 6px;
+ cursor: pointer;
+ transition: background 0.15s;
+}
+
+.demo-theme-btn:hover {
+ background: #ede9fe;
+}
+
+.demo-theme-btn--active {
+ background: #6c5ce7;
+ box-shadow: 0 1px 2px rgba(108, 92, 231, 0.4);
+}
+
.demo-ph-badge {
display: flex;
align-items: center;
@@ -501,6 +572,275 @@ body {
word-break: break-all;
}
+/* ===== MDMA Theme Tokens =====
+ *
+ * Mirrors packages/renderer-react/styles.css so the demo's MDMA styling is
+ * driven by the same `--mdma-*` custom properties. The default values encode
+ * the built-in *light* theme (identical to the demo's previous hardcoded look);
+ * the `[data-theme='dark']` block and the `prefers-color-scheme` block (for
+ * `[data-theme='auto']`) swap the palette. `MdmaThemeProvider` sets the
+ * `data-theme` attribute on each `.mdma-document`.
+ *
+ * The trailing "Demo-only component tokens" section covers components the demo
+ * styles but the library does not (progress / rating / metric widgets, file &
+ * sensitive-field inputs, sensitive table cells).
+ */
+.mdma-document,
+.mdma-theme-root {
+ /* Public palette (mirrors the MdmaTheme `colors` shape) */
+ --mdma-color-background: #ffffff;
+ --mdma-color-surface: #f8f9fa;
+ --mdma-color-text: #333333;
+ --mdma-color-text-muted: #666666;
+ --mdma-color-border: #e0e0e0;
+ --mdma-color-primary: #6c5ce7;
+ --mdma-color-on-primary: #ffffff;
+ --mdma-color-secondary: #dfe6e9;
+ --mdma-color-on-secondary: #2d3436;
+ --mdma-color-danger: #e74c3c;
+ --mdma-color-on-danger: #ffffff;
+ --mdma-color-info: #3498db;
+ --mdma-color-info-bg: #ebf5fb;
+ --mdma-color-warning: #f39c12;
+ --mdma-color-warning-bg: #fef9e7;
+ --mdma-color-error: #e74c3c;
+ --mdma-color-error-bg: #fdedec;
+ --mdma-color-success: #27ae60;
+ --mdma-color-success-bg: #eafaf1;
+
+ /* Internal derivations (follow the palette; not on the public TS type) */
+ --mdma-color-heading: #1a1a2e;
+ --mdma-color-text-subtle: #555555;
+ --mdma-color-border-subtle: #f0f0f0;
+ --mdma-color-primary-hover: color-mix(in srgb, var(--mdma-color-primary) 85%, #000000);
+ --mdma-color-secondary-hover: color-mix(in srgb, var(--mdma-color-secondary) 88%, #000000);
+ --mdma-color-danger-hover: color-mix(in srgb, var(--mdma-color-danger) 84%, #000000);
+ --mdma-color-input-bg: #fafafa;
+ --mdma-color-focus-ring: color-mix(in srgb, var(--mdma-color-primary) 22%, transparent);
+ --mdma-color-info-text: #1a5276;
+ --mdma-color-warning-text: #7d6608;
+ --mdma-color-error-text: #78281f;
+ --mdma-color-success-text: #1e8449;
+ --mdma-color-code-bg: #1e1e2e;
+ --mdma-color-code-text: #e0e0e0;
+ --mdma-color-inline-code-bg: #f0f0f5;
+ --mdma-color-inline-code-text: #c0392b;
+ --mdma-color-loading-bg: #ffffff;
+ --mdma-color-loading-border: #c4bfff;
+ --mdma-color-loading-accent: #6c63ff;
+ --mdma-color-loading-text: #8b82d4;
+ --mdma-color-table-header-bg: #f8f9fa;
+ --mdma-color-row-hover: #f8f9fa;
+ --mdma-color-blockquote-bg: #f8f8ff;
+ --mdma-color-chart-bg: rgba(255, 255, 255, 0.02);
+ --mdma-color-chart-border: rgba(108, 92, 231, 0.15);
+ --mdma-color-chart-label: #a29bfe;
+ --mdma-color-chart-axis: #636e72;
+ --mdma-color-chart-legend: #b2bec3;
+ --mdma-color-chart-tooltip-bg: #1a1a2e;
+ --mdma-color-unknown-bg: #fff3cd;
+ --mdma-color-unknown-border: #ffc107;
+ --mdma-color-unknown-text: #856404;
+
+ /* Radius scales (mirror the MdmaTheme numeric tokens) */
+ --mdma-radius-sm: 6px;
+ --mdma-radius-md: 10px;
+
+ /* Demo-only component tokens (progress / rating / metric widgets) */
+ --mdma-color-widget-strong: #dfe6e9;
+ --mdma-color-widget-muted: #b2bec3;
+ --mdma-color-widget-faint: #636e72;
+ --mdma-color-widget-track: rgba(255, 255, 255, 0.08);
+ --mdma-color-widget-surface: rgba(255, 255, 255, 0.04);
+ --mdma-color-widget-border: rgba(255, 255, 255, 0.08);
+ --mdma-color-widget-positive: #00b894;
+ --mdma-color-widget-star: #fdcb6e;
+
+ /* Demo-only component tokens (sensitive fields, file inputs, cells) */
+ --mdma-color-sensitive-border: #f59e0b;
+ --mdma-color-sensitive-bg: #fffbeb;
+ --mdma-color-sensitive-border-focus: #d97706;
+ --mdma-color-sensitive-focus-ring: rgba(245, 158, 11, 0.15);
+ --mdma-color-file-text: #374151;
+ --mdma-color-file-bg: #f3f4f6;
+ --mdma-color-file-size: #9ca3af;
+ --mdma-color-file-btn-bg: rgba(108, 92, 231, 0.08);
+ --mdma-color-file-btn-border: rgba(108, 92, 231, 0.3);
+ --mdma-color-file-btn-bg-hover: rgba(108, 92, 231, 0.14);
+ --mdma-color-file-btn-border-hover: rgba(108, 92, 231, 0.5);
+ --mdma-color-sensitive-cell-text: #000000;
+ --mdma-color-sensitive-cell-bg: #fef3c7;
+ --mdma-color-sensitive-cell-bg-hover: #fde68a;
+}
+
+/* ===== Built-in dark palette ===== */
+.mdma-document[data-theme="dark"],
+.mdma-theme-root[data-theme="dark"] {
+ --mdma-color-background: #151a26;
+ --mdma-color-surface: #1c2333;
+ --mdma-color-text: #e5e7eb;
+ --mdma-color-text-muted: #9ca3af;
+ --mdma-color-border: #2a3140;
+ --mdma-color-primary: #8b7ff0;
+ --mdma-color-on-primary: #ffffff;
+ --mdma-color-secondary: #2a3140;
+ --mdma-color-on-secondary: #e5e7eb;
+ --mdma-color-danger: #ef4444;
+ --mdma-color-on-danger: #ffffff;
+ --mdma-color-info: #5dade2;
+ --mdma-color-info-bg: #16283a;
+ --mdma-color-warning: #f5b041;
+ --mdma-color-warning-bg: #2a2412;
+ --mdma-color-error: #ef4444;
+ --mdma-color-error-bg: #2a1717;
+ --mdma-color-success: #2ecc71;
+ --mdma-color-success-bg: #12251a;
+
+ --mdma-color-heading: #f3f4f6;
+ --mdma-color-text-subtle: #cbd0d8;
+ --mdma-color-border-subtle: #232a38;
+ --mdma-color-primary-hover: color-mix(in srgb, var(--mdma-color-primary) 80%, #ffffff);
+ --mdma-color-secondary-hover: color-mix(in srgb, var(--mdma-color-secondary) 78%, #ffffff);
+ --mdma-color-danger-hover: color-mix(in srgb, var(--mdma-color-danger) 80%, #ffffff);
+ --mdma-color-input-bg: #10141d;
+ --mdma-color-focus-ring: color-mix(in srgb, var(--mdma-color-primary) 35%, transparent);
+ --mdma-color-info-text: #aed6f1;
+ --mdma-color-warning-text: #f8c471;
+ --mdma-color-error-text: #f5b7b1;
+ --mdma-color-success-text: #a9dfbf;
+ --mdma-color-code-bg: #0d1017;
+ --mdma-color-code-text: #e0e0e0;
+ --mdma-color-inline-code-bg: #232a38;
+ --mdma-color-inline-code-text: #f19999;
+ --mdma-color-loading-bg: #1c2333;
+ --mdma-color-loading-border: #3a3466;
+ --mdma-color-loading-accent: #8b7ff0;
+ --mdma-color-loading-text: #a29bfe;
+ --mdma-color-table-header-bg: #1c2333;
+ --mdma-color-row-hover: #1c2333;
+ --mdma-color-blockquote-bg: #1c2333;
+ --mdma-color-chart-bg: rgba(255, 255, 255, 0.02);
+ --mdma-color-chart-border: rgba(162, 155, 254, 0.2);
+ --mdma-color-chart-label: #a29bfe;
+ --mdma-color-chart-axis: #9ca3af;
+ --mdma-color-chart-legend: #b2bec3;
+ --mdma-color-chart-tooltip-bg: #0d1017;
+ --mdma-color-unknown-bg: #2a2412;
+ --mdma-color-unknown-border: #b7950b;
+ --mdma-color-unknown-text: #f8c471;
+
+ /* Demo-only component tokens (progress / rating / metric widgets) */
+ --mdma-color-widget-strong: #e5e7eb;
+ --mdma-color-widget-muted: #9ca3af;
+ --mdma-color-widget-faint: #6b7280;
+ --mdma-color-widget-track: rgba(255, 255, 255, 0.08);
+ --mdma-color-widget-surface: rgba(255, 255, 255, 0.04);
+ --mdma-color-widget-border: rgba(255, 255, 255, 0.12);
+ --mdma-color-widget-positive: #00d6a4;
+ --mdma-color-widget-star: #ffd97d;
+
+ /* Demo-only component tokens (sensitive fields, file inputs, cells) */
+ --mdma-color-sensitive-border: #f5b041;
+ --mdma-color-sensitive-bg: #2a2412;
+ --mdma-color-sensitive-border-focus: #fbbf24;
+ --mdma-color-sensitive-focus-ring: rgba(245, 176, 65, 0.3);
+ --mdma-color-file-text: #cbd5e1;
+ --mdma-color-file-bg: #1c2333;
+ --mdma-color-file-size: #9ca3af;
+ --mdma-color-file-btn-bg: rgba(139, 127, 240, 0.12);
+ --mdma-color-file-btn-border: rgba(139, 127, 240, 0.35);
+ --mdma-color-file-btn-bg-hover: rgba(139, 127, 240, 0.2);
+ --mdma-color-file-btn-border-hover: rgba(139, 127, 240, 0.5);
+ --mdma-color-sensitive-cell-text: #f8c471;
+ --mdma-color-sensitive-cell-bg: #3a2f12;
+ --mdma-color-sensitive-cell-bg-hover: #4a3d18;
+}
+
+/* theme="auto" — same dark palette, gated on the OS preference. */
+@media (prefers-color-scheme: dark) {
+ .mdma-document[data-theme="auto"],
+ .mdma-theme-root[data-theme="auto"] {
+ --mdma-color-background: #151a26;
+ --mdma-color-surface: #1c2333;
+ --mdma-color-text: #e5e7eb;
+ --mdma-color-text-muted: #9ca3af;
+ --mdma-color-border: #2a3140;
+ --mdma-color-primary: #8b7ff0;
+ --mdma-color-on-primary: #ffffff;
+ --mdma-color-secondary: #2a3140;
+ --mdma-color-on-secondary: #e5e7eb;
+ --mdma-color-danger: #ef4444;
+ --mdma-color-on-danger: #ffffff;
+ --mdma-color-info: #5dade2;
+ --mdma-color-info-bg: #16283a;
+ --mdma-color-warning: #f5b041;
+ --mdma-color-warning-bg: #2a2412;
+ --mdma-color-error: #ef4444;
+ --mdma-color-error-bg: #2a1717;
+ --mdma-color-success: #2ecc71;
+ --mdma-color-success-bg: #12251a;
+
+ --mdma-color-heading: #f3f4f6;
+ --mdma-color-text-subtle: #cbd0d8;
+ --mdma-color-border-subtle: #232a38;
+ --mdma-color-primary-hover: color-mix(in srgb, var(--mdma-color-primary) 80%, #ffffff);
+ --mdma-color-secondary-hover: color-mix(in srgb, var(--mdma-color-secondary) 78%, #ffffff);
+ --mdma-color-danger-hover: color-mix(in srgb, var(--mdma-color-danger) 80%, #ffffff);
+ --mdma-color-input-bg: #10141d;
+ --mdma-color-focus-ring: color-mix(in srgb, var(--mdma-color-primary) 35%, transparent);
+ --mdma-color-info-text: #aed6f1;
+ --mdma-color-warning-text: #f8c471;
+ --mdma-color-error-text: #f5b7b1;
+ --mdma-color-success-text: #a9dfbf;
+ --mdma-color-code-bg: #0d1017;
+ --mdma-color-code-text: #e0e0e0;
+ --mdma-color-inline-code-bg: #232a38;
+ --mdma-color-inline-code-text: #f19999;
+ --mdma-color-loading-bg: #1c2333;
+ --mdma-color-loading-border: #3a3466;
+ --mdma-color-loading-accent: #8b7ff0;
+ --mdma-color-loading-text: #a29bfe;
+ --mdma-color-table-header-bg: #1c2333;
+ --mdma-color-row-hover: #1c2333;
+ --mdma-color-blockquote-bg: #1c2333;
+ --mdma-color-chart-bg: rgba(255, 255, 255, 0.02);
+ --mdma-color-chart-border: rgba(162, 155, 254, 0.2);
+ --mdma-color-chart-label: #a29bfe;
+ --mdma-color-chart-axis: #9ca3af;
+ --mdma-color-chart-legend: #b2bec3;
+ --mdma-color-chart-tooltip-bg: #0d1017;
+ --mdma-color-unknown-bg: #2a2412;
+ --mdma-color-unknown-border: #b7950b;
+ --mdma-color-unknown-text: #f8c471;
+
+ /* Demo-only component tokens (progress / rating / metric widgets) */
+ --mdma-color-widget-strong: #e5e7eb;
+ --mdma-color-widget-muted: #9ca3af;
+ --mdma-color-widget-faint: #6b7280;
+ --mdma-color-widget-track: rgba(255, 255, 255, 0.08);
+ --mdma-color-widget-surface: rgba(255, 255, 255, 0.04);
+ --mdma-color-widget-border: rgba(255, 255, 255, 0.12);
+ --mdma-color-widget-positive: #00d6a4;
+ --mdma-color-widget-star: #ffd97d;
+
+ /* Demo-only component tokens (sensitive fields, file inputs, cells) */
+ --mdma-color-sensitive-border: #f5b041;
+ --mdma-color-sensitive-bg: #2a2412;
+ --mdma-color-sensitive-border-focus: #fbbf24;
+ --mdma-color-sensitive-focus-ring: rgba(245, 176, 65, 0.3);
+ --mdma-color-file-text: #cbd5e1;
+ --mdma-color-file-bg: #1c2333;
+ --mdma-color-file-size: #9ca3af;
+ --mdma-color-file-btn-bg: rgba(139, 127, 240, 0.12);
+ --mdma-color-file-btn-border: rgba(139, 127, 240, 0.35);
+ --mdma-color-file-btn-bg-hover: rgba(139, 127, 240, 0.2);
+ --mdma-color-file-btn-border-hover: rgba(139, 127, 240, 0.5);
+ --mdma-color-sensitive-cell-text: #f8c471;
+ --mdma-color-sensitive-cell-bg: #3a2f12;
+ --mdma-color-sensitive-cell-bg-hover: #4a3d18;
+ }
+}
+
/* ===== MDMA Document ===== */
.mdma-document {
max-width: 720px;
@@ -538,9 +878,9 @@ body {
.mdma-block-loading {
position: relative;
overflow: hidden;
- background: #fff;
- border: 1px dashed #c4bfff;
- border-radius: 10px;
+ background: var(--mdma-color-loading-bg);
+ border: 1px dashed var(--mdma-color-loading-border);
+ border-radius: var(--mdma-radius-md);
padding: 28px 24px;
margin-bottom: 20px;
min-height: 80px;
@@ -578,7 +918,7 @@ body {
display: flex;
align-items: center;
gap: 10px;
- color: #8b82d4;
+ color: var(--mdma-color-loading-text);
font-size: 13px;
}
@@ -586,7 +926,7 @@ body {
width: 18px;
height: 18px;
border: 2px solid rgba(108, 99, 255, 0.2);
- border-top-color: #6c63ff;
+ border-top-color: var(--mdma-color-loading-accent);
border-radius: 50%;
animation: mdma-spin 0.8s linear infinite;
}
@@ -639,7 +979,7 @@ body {
margin: 16px 0 8px;
font-weight: 600;
line-height: 1.3;
- color: #1a1a2e;
+ color: var(--mdma-color-heading);
}
.mdma-markdown-content h1 {
@@ -681,9 +1021,9 @@ body {
.mdma-markdown-content blockquote {
margin: 0 0 10px;
padding: 8px 16px;
- border-left: 3px solid #6c63ff;
- background: #f8f8ff;
- color: #444;
+ border-left: 3px solid var(--mdma-color-loading-accent);
+ background: var(--mdma-color-blockquote-bg);
+ color: var(--mdma-color-text-subtle);
}
.mdma-markdown-content blockquote p:last-child {
@@ -693,9 +1033,9 @@ body {
.mdma-markdown-content pre.mdast-code-block {
margin: 0 0 10px;
padding: 12px 16px;
- background: #1e1e2e;
- color: #e0e0e0;
- border-radius: 6px;
+ background: var(--mdma-color-code-bg);
+ color: var(--mdma-color-code-text);
+ border-radius: var(--mdma-radius-sm);
overflow-x: auto;
font-size: 13px;
line-height: 1.5;
@@ -709,21 +1049,21 @@ body {
}
.mdma-markdown-content code.mdast-inline-code {
- background: #f0f0f5;
+ background: var(--mdma-color-inline-code-bg);
padding: 2px 6px;
border-radius: 3px;
font-size: 0.9em;
- color: #c0392b;
+ color: var(--mdma-color-inline-code-text);
}
.mdma-markdown-content hr {
margin: 16px 0;
border: none;
- border-top: 1px solid #ddd;
+ border-top: 1px solid var(--mdma-color-border);
}
.mdma-markdown-content a {
- color: #6c63ff;
+ color: var(--mdma-color-loading-accent);
text-decoration: none;
}
@@ -734,7 +1074,7 @@ body {
.mdma-markdown-content img {
max-width: 100%;
height: auto;
- border-radius: 6px;
+ border-radius: var(--mdma-radius-sm);
}
.mdma-markdown-content table.mdast-table {
@@ -747,17 +1087,17 @@ body {
.mdma-markdown-content table.mdast-table th,
.mdma-markdown-content table.mdast-table td {
padding: 8px 12px;
- border: 1px solid #e0e0e0;
+ border: 1px solid var(--mdma-color-border);
text-align: left;
}
.mdma-markdown-content table.mdast-table th {
- background: #f5f6fa;
+ background: var(--mdma-color-table-header-bg);
font-weight: 600;
}
.mdma-markdown-content table.mdast-table tbody tr:hover {
- background: #f9f9fc;
+ background: var(--mdma-color-row-hover);
}
.mdma-markdown-content strong {
@@ -770,14 +1110,14 @@ body {
.mdma-markdown-content del {
text-decoration: line-through;
- color: #888;
+ color: var(--mdma-color-text-muted);
}
/* ===== Form ===== */
.mdma-form {
- background: #fff;
- border: 1px solid #e0e0e0;
- border-radius: 10px;
+ background: var(--mdma-color-background);
+ border: 1px solid var(--mdma-color-border);
+ border-radius: var(--mdma-radius-md);
padding: 24px;
margin-bottom: 20px;
}
@@ -786,7 +1126,7 @@ body {
font-size: 18px;
font-weight: 600;
margin-bottom: 16px;
- color: #1a1a2e;
+ color: var(--mdma-color-heading);
}
.mdma-form-field {
@@ -797,7 +1137,7 @@ body {
display: block;
font-size: 13px;
font-weight: 600;
- color: #444;
+ color: var(--mdma-color-text-subtle);
margin-bottom: 4px;
}
@@ -812,10 +1152,10 @@ body {
width: 100%;
padding: 8px 12px;
font-size: 14px;
- border: 1px solid #d1d5db;
- border-radius: 6px;
- background: #fafafa;
- color: #1a1a2e;
+ border: 1px solid var(--mdma-color-border);
+ border-radius: var(--mdma-radius-sm);
+ background: var(--mdma-color-input-bg);
+ color: var(--mdma-color-heading);
transition: border-color 0.15s, box-shadow 0.15s;
outline: none;
}
@@ -823,8 +1163,8 @@ body {
.mdma-form-field input:focus,
.mdma-form-field select:focus,
.mdma-form-field textarea:focus {
- border-color: #6c5ce7;
- box-shadow: 0 0 0 3px rgba(108, 92, 231, 0.15);
+ border-color: var(--mdma-color-primary);
+ box-shadow: 0 0 0 3px var(--mdma-color-focus-ring);
}
/* Sensitive field styles */
@@ -834,14 +1174,14 @@ body {
.mdma-form-field--sensitive input,
.mdma-form-field--sensitive textarea {
- border-color: #f59e0b;
- background: #fffbeb;
+ border-color: var(--mdma-color-sensitive-border);
+ background: var(--mdma-color-sensitive-bg);
}
.mdma-form-field--sensitive input:focus,
.mdma-form-field--sensitive textarea:focus {
- border-color: #d97706;
- box-shadow: 0 0 0 3px rgba(245, 158, 11, 0.15);
+ border-color: var(--mdma-color-sensitive-border-focus);
+ box-shadow: 0 0 0 3px var(--mdma-color-sensitive-focus-ring);
}
.mdma-sensitive-badge {
@@ -873,7 +1213,7 @@ body {
font-size: 11px;
font-weight: 600;
letter-spacing: 0.04em;
- color: #6c5ce7;
+ color: var(--mdma-color-primary);
padding: 2px 4px;
opacity: 0.7;
transition: opacity 0.15s;
@@ -893,25 +1233,25 @@ body {
.mdma-input--file input[type="file"] {
font-size: 13px;
- color: #374151;
+ color: var(--mdma-color-file-text);
}
.mdma-input--file input[type="file"]::file-selector-button {
padding: 7px 14px;
font-size: 13px;
font-weight: 500;
- color: #6c5ce7;
- background: rgba(108, 92, 231, 0.08);
- border: 1px solid rgba(108, 92, 231, 0.3);
- border-radius: 6px;
+ color: var(--mdma-color-primary);
+ background: var(--mdma-color-file-btn-bg);
+ border: 1px solid var(--mdma-color-file-btn-border);
+ border-radius: var(--mdma-radius-sm);
cursor: pointer;
margin-right: 10px;
transition: background 0.15s, border-color 0.15s;
}
.mdma-input--file input[type="file"]:hover::file-selector-button {
- background: rgba(108, 92, 231, 0.14);
- border-color: rgba(108, 92, 231, 0.5);
+ background: var(--mdma-color-file-btn-bg-hover);
+ border-color: var(--mdma-color-file-btn-border-hover);
}
.mdma-file-list {
@@ -928,22 +1268,22 @@ body {
align-items: center;
gap: 6px;
font-size: 13px;
- color: #374151;
- background: #f3f4f6;
+ color: var(--mdma-color-file-text);
+ background: var(--mdma-color-file-bg);
border-radius: 5px;
padding: 4px 10px;
}
.mdma-file-size {
font-size: 11px;
- color: #9ca3af;
+ color: var(--mdma-color-file-size);
}
/* Table sensitive styles */
.mdma-table-cell--sensitive {
cursor: pointer;
- color: #000;
- background: #fef3c7;
+ color: var(--mdma-color-sensitive-cell-text);
+ background: var(--mdma-color-sensitive-cell-bg);
padding: 2px 6px;
border-radius: 3px;
font-family: monospace;
@@ -953,7 +1293,7 @@ body {
}
.mdma-table-cell--sensitive:hover {
- background: #fde68a;
+ background: var(--mdma-color-sensitive-cell-bg-hover);
}
/* Custom glass input sensitive styles */
@@ -1013,7 +1353,7 @@ body {
.mdma-form-field input[type="checkbox"] {
width: auto;
margin-right: 8px;
- accent-color: #6c5ce7;
+ accent-color: var(--mdma-color-primary);
}
.mdma-form-submit {
@@ -1021,16 +1361,16 @@ body {
padding: 8px 20px;
font-size: 14px;
font-weight: 600;
- color: #fff;
- background: #6c5ce7;
+ color: var(--mdma-color-on-primary);
+ background: var(--mdma-color-primary);
border: none;
- border-radius: 6px;
+ border-radius: var(--mdma-radius-sm);
cursor: pointer;
transition: background 0.15s;
}
.mdma-form-submit:hover {
- background: #5a4bd1;
+ background: var(--mdma-color-primary-hover);
}
/* ===== Button ===== */
@@ -1040,7 +1380,7 @@ body {
font-size: 14px;
font-weight: 600;
border: none;
- border-radius: 6px;
+ border-radius: var(--mdma-radius-sm);
cursor: pointer;
transition: background 0.15s, transform 0.1s;
margin-bottom: 20px;
@@ -1051,37 +1391,37 @@ body {
}
.mdma-button--primary {
- background: #6c5ce7;
- color: #fff;
+ background: var(--mdma-color-primary);
+ color: var(--mdma-color-on-primary);
}
.mdma-button--primary:hover {
- background: #5a4bd1;
+ background: var(--mdma-color-primary-hover);
}
.mdma-button--secondary {
- background: #dfe6e9;
- color: #2d3436;
+ background: var(--mdma-color-secondary);
+ color: var(--mdma-color-on-secondary);
}
.mdma-button--secondary:hover {
- background: #c8d6db;
+ background: var(--mdma-color-secondary-hover);
}
.mdma-button--danger {
- background: #e74c3c;
- color: #fff;
+ background: var(--mdma-color-danger);
+ color: var(--mdma-color-on-danger);
}
.mdma-button--danger:hover {
- background: #c0392b;
+ background: var(--mdma-color-danger-hover);
}
/* ===== Tasklist ===== */
.mdma-tasklist {
- background: #fff;
- border: 1px solid #e0e0e0;
- border-radius: 10px;
+ background: var(--mdma-color-background);
+ border: 1px solid var(--mdma-color-border);
+ border-radius: var(--mdma-radius-md);
padding: 24px;
margin-bottom: 20px;
}
@@ -1090,7 +1430,7 @@ body {
font-size: 18px;
font-weight: 600;
margin-bottom: 12px;
- color: #1a1a2e;
+ color: var(--mdma-color-heading);
}
.mdma-tasklist-items {
@@ -1099,7 +1439,7 @@ body {
.mdma-tasklist-item {
padding: 8px 0;
- border-bottom: 1px solid #f0f0f0;
+ border-bottom: 1px solid var(--mdma-color-border-subtle);
}
.mdma-tasklist-item:last-child {
@@ -1112,25 +1452,25 @@ body {
gap: 8px;
cursor: pointer;
font-size: 14px;
- color: #333;
+ color: var(--mdma-color-text);
}
.mdma-tasklist-item input[type="checkbox"] {
- accent-color: #6c5ce7;
+ accent-color: var(--mdma-color-primary);
width: 16px;
height: 16px;
}
.mdma-tasklist-item input[type="checkbox"]:checked + span {
text-decoration: line-through;
- color: #999;
+ color: var(--mdma-color-text-muted);
}
/* ===== Table ===== */
.mdma-table {
- background: #fff;
- border: 1px solid #e0e0e0;
- border-radius: 10px;
+ background: var(--mdma-color-background);
+ border: 1px solid var(--mdma-color-border);
+ border-radius: var(--mdma-radius-md);
padding: 24px;
margin-bottom: 20px;
overflow-x: auto;
@@ -1140,7 +1480,7 @@ body {
font-size: 18px;
font-weight: 600;
margin-bottom: 12px;
- color: #1a1a2e;
+ color: var(--mdma-color-heading);
}
.mdma-table table {
@@ -1155,32 +1495,32 @@ body {
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
- color: #666;
- background: #f8f9fa;
- border-bottom: 2px solid #e0e0e0;
+ color: var(--mdma-color-text-muted);
+ background: var(--mdma-color-table-header-bg);
+ border-bottom: 2px solid var(--mdma-color-border);
}
.mdma-table td {
padding: 10px 12px;
font-size: 14px;
- border-bottom: 1px solid #f0f0f0;
- color: #333;
+ border-bottom: 1px solid var(--mdma-color-border-subtle);
+ color: var(--mdma-color-text);
}
.mdma-table tr:hover td {
- background: #f8f9fa;
+ background: var(--mdma-color-row-hover);
}
.mdma-table-empty {
text-align: center;
padding: 20px;
- color: #999;
+ color: var(--mdma-color-text-muted);
font-style: italic;
}
/* ===== Callout ===== */
.mdma-callout {
- border-radius: 10px;
+ border-radius: var(--mdma-radius-md);
padding: 16px 20px;
margin-bottom: 20px;
position: relative;
@@ -1188,27 +1528,27 @@ body {
}
.mdma-callout--info {
- background: #ebf5fb;
- border-left-color: #3498db;
- color: #1a5276;
+ background: var(--mdma-color-info-bg);
+ border-left-color: var(--mdma-color-info);
+ color: var(--mdma-color-info-text);
}
.mdma-callout--warning {
- background: #fef9e7;
- border-left-color: #f39c12;
- color: #7d6608;
+ background: var(--mdma-color-warning-bg);
+ border-left-color: var(--mdma-color-warning);
+ color: var(--mdma-color-warning-text);
}
.mdma-callout--error {
- background: #fdedec;
- border-left-color: #e74c3c;
- color: #78281f;
+ background: var(--mdma-color-error-bg);
+ border-left-color: var(--mdma-color-error);
+ color: var(--mdma-color-error-text);
}
.mdma-callout--success {
- background: #eafaf1;
- border-left-color: #27ae60;
- color: #1e8449;
+ background: var(--mdma-color-success-bg);
+ border-left-color: var(--mdma-color-success);
+ color: var(--mdma-color-success-text);
}
.mdma-callout-title {
@@ -1241,43 +1581,43 @@ body {
/* ===== Approval Gate ===== */
.mdma-approval-gate {
- background: #fff;
- border: 2px solid #e0e0e0;
- border-radius: 10px;
+ background: var(--mdma-color-background);
+ border: 2px solid var(--mdma-color-border);
+ border-radius: var(--mdma-radius-md);
padding: 24px;
margin-bottom: 20px;
}
.mdma-approval-gate--pending {
- border-color: #f39c12;
+ border-color: var(--mdma-color-warning);
}
.mdma-approval-gate--approved {
- border-color: #27ae60;
- background: #f0faf4;
+ border-color: var(--mdma-color-success);
+ background: var(--mdma-color-success-bg);
}
.mdma-approval-gate--denied {
- border-color: #e74c3c;
- background: #fdf2f2;
+ border-color: var(--mdma-color-error);
+ background: var(--mdma-color-error-bg);
}
.mdma-approval-gate-title {
font-size: 16px;
font-weight: 600;
margin-bottom: 8px;
- color: #1a1a2e;
+ color: var(--mdma-color-heading);
}
.mdma-approval-gate-description {
font-size: 14px;
- color: #555;
+ color: var(--mdma-color-text-subtle);
margin-bottom: 12px;
}
.mdma-approval-gate-status {
font-size: 13px;
- color: #666;
+ color: var(--mdma-color-text-muted);
margin-bottom: 16px;
}
@@ -1287,13 +1627,13 @@ body {
}
.mdma-approval-gate--pending .mdma-approval-gate-status strong {
- color: #f39c12;
+ color: var(--mdma-color-warning);
}
.mdma-approval-gate--approved .mdma-approval-gate-status strong {
- color: #27ae60;
+ color: var(--mdma-color-success);
}
.mdma-approval-gate--denied .mdma-approval-gate-status strong {
- color: #e74c3c;
+ color: var(--mdma-color-error);
}
.mdma-approval-gate-actions {
@@ -1307,8 +1647,8 @@ body {
align-items: center;
gap: 10px;
padding: 12px 16px;
- background: #fff;
- border: 1px solid #e0e0e0;
+ background: var(--mdma-color-background);
+ border: 1px solid var(--mdma-color-border);
border-radius: 8px;
margin-bottom: 20px;
font-size: 13px;
@@ -1316,7 +1656,7 @@ body {
.mdma-webhook-label {
font-weight: 600;
- color: #333;
+ color: var(--mdma-color-text);
}
.mdma-webhook-status {
@@ -1327,8 +1667,8 @@ body {
}
.mdma-webhook-status--idle {
- background: #f0f0f0;
- color: #666;
+ background: var(--mdma-color-surface);
+ color: var(--mdma-color-text-muted);
}
.mdma-webhook-status--executing {
background: #ffeaa7;
@@ -1346,10 +1686,10 @@ body {
/* ===== Unknown Component ===== */
.mdma-unknown-component {
padding: 12px 16px;
- background: #fff3cd;
- border: 1px solid #ffc107;
+ background: var(--mdma-color-unknown-bg);
+ border: 1px solid var(--mdma-color-unknown-border);
border-radius: 8px;
- color: #856404;
+ color: var(--mdma-color-unknown-text);
font-size: 13px;
margin-bottom: 20px;
}
@@ -1714,6 +2054,24 @@ body {
line-height: 1.6;
}
+.chat-example-select {
+ margin-top: 8px;
+ min-width: 220px;
+ padding: 8px 12px;
+ font-size: 14px;
+ border: 1px solid #d1d5db;
+ border-radius: 6px;
+ background: #fff;
+ color: #374151;
+ cursor: pointer;
+}
+
+[data-theme="dark"] .chat-example-select {
+ border-color: #2a3140;
+ background: #1c2333;
+ color: #e5e7eb;
+}
+
/* ===== Chat Message ===== */
@keyframes chat-msg-enter {
from {
@@ -2214,44 +2572,44 @@ body {
.mdma-progress-label {
font-size: 13px;
font-weight: 600;
- color: #dfe6e9;
+ color: var(--mdma-color-widget-strong);
min-width: 80px;
}
.mdma-progress-track {
flex: 1;
height: 12px;
- background: rgba(255, 255, 255, 0.08);
- border-radius: 6px;
+ background: var(--mdma-color-widget-track);
+ border-radius: var(--mdma-radius-sm);
overflow: hidden;
}
.mdma-progress-fill {
height: 100%;
- border-radius: 6px;
+ border-radius: var(--mdma-radius-sm);
transition: width 0.4s ease;
}
.mdma-progress-fill--default {
- background: #6c5ce7;
+ background: var(--mdma-color-primary);
}
.mdma-progress-fill--success {
- background: #00b894;
+ background: var(--mdma-color-widget-positive);
}
.mdma-progress-fill--warning {
- background: #fdcb6e;
+ background: var(--mdma-color-widget-star);
}
.mdma-progress-fill--danger {
- background: #e74c3c;
+ background: var(--mdma-color-danger);
}
.mdma-progress-text {
font-size: 13px;
font-weight: 600;
- color: #b2bec3;
+ color: var(--mdma-color-widget-muted);
min-width: 36px;
text-align: right;
}
@@ -2268,7 +2626,7 @@ body {
.mdma-rating-label {
font-size: 13px;
font-weight: 600;
- color: #dfe6e9;
+ color: var(--mdma-color-widget-strong);
margin-right: 4px;
}
@@ -2283,7 +2641,7 @@ body {
font-size: 22px;
cursor: pointer;
padding: 0 2px;
- color: #636e72;
+ color: var(--mdma-color-widget-faint);
transition: color 0.12s, transform 0.12s;
line-height: 1;
}
@@ -2293,12 +2651,12 @@ body {
}
.mdma-rating-star--filled {
- color: #fdcb6e;
+ color: var(--mdma-color-widget-star);
}
.mdma-rating-value {
font-size: 13px;
- color: #b2bec3;
+ color: var(--mdma-color-widget-muted);
margin-left: 4px;
}
@@ -2309,9 +2667,9 @@ body {
flex-direction: column;
gap: 4px;
padding: 14px 20px;
- background: rgba(255, 255, 255, 0.04);
- border: 1px solid rgba(255, 255, 255, 0.08);
- border-radius: 10px;
+ background: var(--mdma-color-widget-surface);
+ border: 1px solid var(--mdma-color-widget-border);
+ border-radius: var(--mdma-radius-md);
min-width: 120px;
}
@@ -2320,7 +2678,7 @@ body {
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
- color: #b2bec3;
+ color: var(--mdma-color-widget-muted);
}
.mdma-metric-value-row {
@@ -2332,13 +2690,13 @@ body {
.mdma-metric-value {
font-size: 28px;
font-weight: 700;
- color: #dfe6e9;
+ color: var(--mdma-color-widget-strong);
line-height: 1.1;
}
.mdma-metric-unit {
font-size: 14px;
- color: #636e72;
+ color: var(--mdma-color-widget-faint);
}
.mdma-metric-trend {
@@ -2348,22 +2706,22 @@ body {
}
.mdma-metric-trend--up {
- color: #00b894;
+ color: var(--mdma-color-widget-positive);
}
.mdma-metric-trend--down {
- color: #e74c3c;
+ color: var(--mdma-color-danger);
}
.mdma-metric-trend--flat {
- color: #b2bec3;
+ color: var(--mdma-color-widget-muted);
}
/* ─── Chart Component ─────────────────────────────────────────────────────── */
.mdma-chart {
- background: rgba(255, 255, 255, 0.02);
- border: 1px solid rgba(108, 92, 231, 0.15);
+ background: var(--mdma-color-chart-bg);
+ border: 1px solid var(--mdma-color-chart-border);
border-radius: 14px;
padding: 18px;
margin-bottom: 20px;
@@ -2373,7 +2731,7 @@ body {
.mdma-chart-label {
font-size: 14px;
font-weight: 700;
- color: #a29bfe;
+ color: var(--mdma-color-chart-label);
margin-bottom: 12px;
}
@@ -2388,23 +2746,23 @@ body {
display: flex;
align-items: center;
justify-content: center;
- color: #636e72;
+ color: var(--mdma-color-chart-axis);
font-size: 13px;
font-style: italic;
}
/* recharts overrides for MDMA theme */
.mdma-chart .recharts-text {
- fill: #636e72;
+ fill: var(--mdma-color-chart-axis);
}
.mdma-chart .recharts-legend-item-text {
- color: #b2bec3 !important;
+ color: var(--mdma-color-chart-legend) !important;
font-size: 12px;
}
.mdma-chart .recharts-default-tooltip {
- background: #1a1a2e !important;
+ background: var(--mdma-color-chart-tooltip-bg) !important;
border: 1px solid rgba(162, 155, 254, 0.3) !important;
border-radius: 8px !important;
}
@@ -2937,9 +3295,9 @@ body {
/* ─── Thinking Component (basic) ─────────────────────────────────────────── */
.mdma-thinking {
- background: #fff;
- border: 1px solid #e0e0e0;
- border-radius: 10px;
+ background: var(--mdma-color-background);
+ border: 1px solid var(--mdma-color-border);
+ border-radius: var(--mdma-radius-md);
margin: 16px 0;
overflow: hidden;
}
@@ -2952,7 +3310,7 @@ body {
cursor: pointer;
font-size: 14px;
font-weight: 600;
- color: #555;
+ color: var(--mdma-color-text-subtle);
user-select: none;
list-style: none;
}
@@ -2966,7 +3324,7 @@ body {
font-size: 10px;
margin-left: auto;
transition: transform 0.2s;
- color: #999;
+ color: var(--mdma-color-text-muted);
}
.mdma-thinking[open] .mdma-thinking-summary::after {
@@ -2978,7 +3336,7 @@ body {
width: 8px;
height: 8px;
border-radius: 50%;
- background: #6c5ce7;
+ background: var(--mdma-color-primary);
animation: mdma-thinking-pulse 1.5s ease-in-out infinite;
}
@@ -3002,9 +3360,9 @@ body {
padding: 0 16px 16px;
font-size: 13px;
line-height: 1.6;
- color: #555;
+ color: var(--mdma-color-text-subtle);
white-space: pre-wrap;
- border-top: 1px solid #f0f0f0;
+ border-top: 1px solid var(--mdma-color-border-subtle);
padding-top: 12px;
}
@@ -5003,8 +5361,73 @@ body {
}
.docs-layout--with-preview .docs-content {
- flex: 0 1 860px;
+ flex: 1 1 0;
+ min-width: 0;
+}
+
+/* Mobile-only top bar: a hamburger toggle for the nav drawer + a breadcrumb
+ trail. Hidden on desktop; the mobile media query flips it to `display: flex`. */
+.docs-mobile-bar {
+ display: none;
+ align-items: center;
+ gap: 12px;
+ padding: 9px 14px;
+ border-bottom: 1px solid rgba(128, 128, 128, 0.25);
+ flex-shrink: 0;
+}
+.docs-menu-toggle {
+ flex-shrink: 0;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ width: 36px;
+ height: 36px;
+ font-size: 18px;
+ line-height: 1;
+ border: 1px solid rgba(128, 128, 128, 0.3);
+ border-radius: 8px;
+ background: transparent;
+ color: inherit;
+ cursor: pointer;
+}
+.docs-menu-toggle--open {
+ background: rgba(108, 92, 231, 0.12);
+ border-color: rgba(108, 92, 231, 0.4);
+ color: #6c5ce7;
+}
+.docs-breadcrumb {
+ display: flex;
+ align-items: center;
+ flex-wrap: wrap;
+ gap: 6px;
min-width: 0;
+ font-size: 13px;
+ color: #6b7280;
+}
+.docs-breadcrumb-sep {
+ color: #9ca3af;
+}
+.docs-breadcrumb-group {
+ font-weight: 600;
+ color: #374151;
+}
+.docs-breadcrumb-current {
+ font-weight: 600;
+ color: #6c5ce7;
+}
+[data-theme="dark"] .docs-breadcrumb {
+ color: #9ca3af;
+}
+[data-theme="dark"] .docs-breadcrumb-group {
+ color: #cbd5e1;
+}
+[data-theme="dark"] .docs-breadcrumb-current {
+ color: #a29bfe;
+}
+[data-theme="dark"] .docs-menu-toggle--open {
+ background: rgba(139, 127, 240, 0.18);
+ border-color: rgba(139, 127, 240, 0.5);
+ color: #a29bfe;
}
.docs-nav {
@@ -5028,6 +5451,10 @@ body {
padding: 4px 10px 12px;
}
+.docs-nav-title--group {
+ margin-top: 16px;
+}
+
.docs-nav-item {
display: block;
width: 100%;
@@ -5266,9 +5693,9 @@ body {
.docs-content {
flex: 1;
+ min-width: 0;
overflow-y: auto;
padding: 40px 48px;
- max-width: 860px;
}
.docs-content h2 {
@@ -5417,8 +5844,8 @@ body {
}
.docs-preview-panel {
- flex: 1;
- min-width: 260px;
+ flex: 0 0 500px;
+ min-width: 0;
min-height: 0;
align-self: stretch;
border-left: 1px solid #e5e7eb;
@@ -6164,10 +6591,10 @@ body {
/* Give the Snack more room than the component preview: narrower content column,
wider preview panel. */
.docs-layout--rn .docs-content {
- flex: 0 1 620px;
+ flex: 1 1 0;
}
.docs-layout--rn .docs-preview-panel {
- min-width: 440px;
+ flex: 0 0 520px;
}
.rn-preview-panel {
@@ -6212,3 +6639,1335 @@ body {
font-size: 13px;
color: #6b7280;
}
+
+/* ===== Dark mode (demo chrome) =====
+ Additive overrides applied when the app root renders
+ `.demo-layout[data-theme='dark']`. Each rule restates only color-ish
+ properties (background / color / border / box-shadow) with dark values;
+ `[data-theme='dark'] .foo` outspecifies the light `.foo`, so light mode is
+ untouched. MDMA component styles (`.mdma-*`) and chrome that is already dark
+ in light mode (header, nav dropdown, docs left nav, dark code blocks) are
+ intentionally left alone. */
+
+/* ── Reset & Base ── */
+[data-theme="dark"] body {
+ color: #e5e7eb;
+ background: #0e1119;
+}
+
+/* ── Home View ── */
+[data-theme="dark"] .home-hero-title {
+ color: #e5e7eb;
+}
+[data-theme="dark"] .home-card {
+ background: #151a26;
+ border-color: #2a3140;
+}
+[data-theme="dark"] .home-card-label {
+ color: #e5e7eb;
+}
+[data-theme="dark"] .home-card-desc {
+ color: #9ca3af;
+}
+
+/* ── Main content / error ── */
+[data-theme="dark"] .demo-error {
+ background: #2a1717;
+ color: #ef4444;
+ border-color: #7f1d1d;
+}
+
+/* ── Event Log Panel ── */
+[data-theme="dark"] .demo-event-panel {
+ background: #151a26;
+ border-left-color: #2a3140;
+}
+[data-theme="dark"] .demo-event-title {
+ color: #cbd5e1;
+ border-bottom-color: #2a3140;
+}
+[data-theme="dark"] .demo-event-empty {
+ color: #9ca3af;
+}
+[data-theme="dark"] .demo-event-entry {
+ border-bottom-color: #232a38;
+}
+[data-theme="dark"] .demo-event-time {
+ color: #9ca3af;
+}
+[data-theme="dark"] .demo-event-type {
+ background: #1c2333;
+ color: #cbd5e1;
+}
+[data-theme="dark"] .demo-event-detail {
+ color: #9ca3af;
+}
+
+/* ── Chat Action Log ── */
+[data-theme="dark"] .chat-action-log-toggle {
+ border-color: #2a3140;
+ background: #151a26;
+ color: #cbd5e1;
+}
+[data-theme="dark"] .chat-action-log-panel {
+ background: #151a26;
+ border-left-color: #2a3140;
+}
+[data-theme="dark"] .chat-action-log-title {
+ color: #cbd5e1;
+ border-bottom-color: #2a3140;
+}
+[data-theme="dark"] .chat-action-log-empty {
+ color: #9ca3af;
+}
+[data-theme="dark"] .chat-action-log-entry {
+ border-bottom-color: #232a38;
+}
+[data-theme="dark"] .chat-action-log-msg-id {
+ background: rgba(108, 92, 231, 0.14);
+ color: #a29bfe;
+}
+
+/* ── Chat Settings Bar ── */
+[data-theme="dark"] .chat-settings-bar {
+ background: rgba(108, 92, 231, 0.1);
+ border-bottom-color: #2a3140;
+}
+[data-theme="dark"] .chat-settings-toggle {
+ color: #a29bfe;
+}
+[data-theme="dark"] .chat-settings-toggle::after {
+ color: #9ca3af;
+}
+[data-theme="dark"] .chat-settings-toggle:hover {
+ color: #a29bfe;
+ background: rgba(108, 92, 231, 0.18);
+}
+[data-theme="dark"] .chat-settings-toggle[data-alert="true"] {
+ color: #ef4444;
+ background: #2a1717;
+}
+[data-theme="dark"] .chat-settings-toggle[data-alert="true"]:hover {
+ color: #f87171;
+ background: #3a1d1d;
+}
+[data-theme="dark"] .settings-missing-key {
+ color: #ef4444;
+ background: #2a1717;
+ border-color: #7f1d1d;
+}
+[data-theme="dark"] .ai-preset-btn {
+ border-color: #2a3140;
+ background: #1c2333;
+ color: #cbd5e1;
+}
+[data-theme="dark"] .ai-preset-btn:hover {
+ color: #a29bfe;
+ background: rgba(108, 92, 231, 0.14);
+}
+[data-theme="dark"] .ai-setting span {
+ color: #cbd5e1;
+}
+[data-theme="dark"] .ai-setting input,
+[data-theme="dark"] .ai-setting select {
+ border-color: #2a3140;
+ background: #1c2333;
+ color: #e5e7eb;
+}
+[data-theme="dark"] .ai-setting--toggle input[type="checkbox"] {
+ background: #2a3140;
+}
+
+/* ── Chat Messages ── */
+[data-theme="dark"] .chat-messages {
+ background: #0e1119;
+}
+[data-theme="dark"] .chat-empty-title {
+ color: #e5e7eb;
+}
+[data-theme="dark"] .chat-empty-hint {
+ color: #9ca3af;
+}
+[data-theme="dark"] .chat-msg-view-toggle {
+ border-color: #2a3140;
+ background: #151a26;
+ color: #cbd5e1;
+}
+[data-theme="dark"] .chat-msg--user .chat-msg-label {
+ color: #a29bfe;
+}
+[data-theme="dark"] .chat-msg--assistant .chat-msg-label {
+ color: #34d399;
+}
+[data-theme="dark"] .chat-msg--user .chat-msg-body {
+ background: rgba(108, 92, 231, 0.14);
+ border-color: rgba(108, 92, 231, 0.3);
+}
+[data-theme="dark"] .chat-msg--user .chat-msg-body p {
+ color: #e5e7eb;
+}
+[data-theme="dark"] .chat-msg--assistant .chat-msg-body {
+ background: #151a26;
+ border-color: #2a3140;
+}
+[data-theme="dark"] .chat-msg-raw {
+ color: #cbd5e1;
+}
+[data-theme="dark"] .agent-raw-toggle {
+ border-color: rgba(108, 92, 231, 0.3);
+}
+[data-theme="dark"] .agent-raw-toggle:hover {
+ background: rgba(108, 92, 231, 0.14);
+}
+[data-theme="dark"] .chat-msg-editor-reset {
+ border-color: #2a3140;
+ color: #9ca3af;
+}
+[data-theme="dark"] .chat-msg-typing {
+ color: #9ca3af;
+}
+[data-theme="dark"] .chat-msg-streaming {
+ border-top-color: #2a3140;
+}
+[data-theme="dark"] .chat-error {
+ background: #2a1717;
+ color: #ef4444;
+ border-color: #7f1d1d;
+}
+
+/* ── Chat Input Bar ── */
+[data-theme="dark"] .chat-input-bar {
+ background: #0e1119;
+ border-top-color: #2a3140;
+}
+[data-theme="dark"] .chat-input-container {
+ border-color: #2a3140;
+ background: #151a26;
+}
+[data-theme="dark"] .chat-input {
+ color: #e5e7eb;
+}
+[data-theme="dark"] .chat-input::placeholder {
+ color: #6b7280;
+}
+[data-theme="dark"] .chat-input-actions {
+ border-top-color: #232a38;
+}
+[data-theme="dark"] .chat-input-hint {
+ color: #6b7280;
+}
+[data-theme="dark"] .chat-clear-btn {
+ border-color: #2a3140;
+ color: #9ca3af;
+}
+[data-theme="dark"] .chat-clear-btn:hover {
+ background: #2a1717;
+}
+[data-theme="dark"] .chat-demo-btn {
+ border-color: rgba(108, 92, 231, 0.3);
+}
+[data-theme="dark"] .chat-demo-btn:hover {
+ background: rgba(108, 92, 231, 0.14);
+}
+[data-theme="dark"] .chat-send-btn:disabled {
+ background: #2a3140;
+}
+
+/* ── Playground info (text only; green tint bg reads fine on dark) ── */
+[data-theme="dark"] .playground-info {
+ color: #cbd5e1;
+}
+
+/* ── Creator custom glass inputs (.ce-*) ── */
+[data-theme="dark"] .ce-glass-input,
+[data-theme="dark"] .ce-glass-select,
+[data-theme="dark"] .ce-glass-textarea {
+ color: #e5e7eb;
+}
+[data-theme="dark"] .ce-glass-input::placeholder,
+[data-theme="dark"] .ce-glass-textarea::placeholder {
+ color: rgba(229, 231, 235, 0.4);
+}
+/* form-scoped variant (ties the existing `.ce-editable-field .ce-glass-*`
+ specificity and wins by source order — no `.mdma-*` selector needed) */
+[data-theme="dark"] .ce-editable-field .ce-glass-input,
+[data-theme="dark"] .ce-editable-field .ce-glass-select,
+[data-theme="dark"] .ce-editable-field .ce-glass-textarea {
+ color: #e5e7eb;
+}
+[data-theme="dark"] .ce-glass-input--sensitive {
+ background: #2a2412 !important;
+ border-color: #f5b041 !important;
+}
+[data-theme="dark"] .ce-pii-badge {
+ color: #f5b041;
+ background: #2a2412;
+ border-color: #5c4a1a;
+}
+[data-theme="dark"] .custom-table-cell--sensitive {
+ color: #f5b041;
+ background: #2a2412;
+}
+[data-theme="dark"] .custom-table-cell--sensitive:hover {
+ background: #3a3018;
+}
+
+/* ── Custom table override ── */
+[data-theme="dark"] .custom-table td {
+ color: #e5e7eb;
+}
+[data-theme="dark"] .custom-table th {
+ color: #9ca3af;
+}
+
+/* ── Validator ── */
+[data-theme="dark"] .validator-info {
+ color: #cbd5e1;
+}
+[data-theme="dark"] .validator-variant-selector {
+ border-bottom-color: #2a3140;
+ background: #1c2333;
+}
+[data-theme="dark"] .validator-variant-btn {
+ border-color: #2a3140;
+ background: #151a26;
+ color: #cbd5e1;
+}
+[data-theme="dark"] .validator-variant-btn:hover {
+ background: #222c3d;
+ border-color: #3a4150;
+}
+[data-theme="dark"] .validator-side-panel {
+ background: #151a26;
+ border-left-color: #2a3140;
+}
+[data-theme="dark"] .flow-progress-panel {
+ border-bottom-color: #2a3140;
+ background: #1c2333;
+}
+[data-theme="dark"] .flow-progress-panel h3 {
+ color: #cbd5e1;
+}
+[data-theme="dark"] .flow-step {
+ background: #1c2333;
+ border-color: #2a3140;
+}
+[data-theme="dark"] .flow-step--done {
+ background: #12251a;
+ border-color: #22c55e;
+}
+[data-theme="dark"] .flow-step--error {
+ background: #2a1717;
+ border-color: #ef4444;
+}
+[data-theme="dark"] .flow-step-num {
+ background: #222c3d;
+ color: #9ca3af;
+}
+[data-theme="dark"] .flow-step-label {
+ color: #cbd5e1;
+}
+[data-theme="dark"] .flow-step-badge--done {
+ background: #12251a;
+ color: #22c55e;
+}
+[data-theme="dark"] .flow-step-badge--error {
+ background: #2a1717;
+ color: #f87171;
+}
+[data-theme="dark"] .flow-complete-modal {
+ background: #151a26;
+}
+[data-theme="dark"] .flow-complete-modal h2 {
+ color: #e5e7eb;
+}
+[data-theme="dark"] .manual-validator {
+ border-bottom-color: #2a3140;
+ background: #1c2333;
+}
+[data-theme="dark"] .manual-validator-header {
+ color: #cbd5e1;
+}
+[data-theme="dark"] .manual-validator-badge {
+ color: #a29bfe;
+}
+[data-theme="dark"] .manual-validator-textarea {
+ border-top-color: #2a3140;
+ border-bottom-color: #2a3140;
+ background: #1c2333;
+ color: #e5e7eb;
+}
+[data-theme="dark"] .manual-validator-textarea:focus {
+ background: #222c3d;
+}
+[data-theme="dark"] .manual-validator-textarea::placeholder {
+ color: #6b7280;
+}
+[data-theme="dark"] .manual-validator-clear-btn {
+ border-color: #2a3140;
+ background: #151a26;
+ color: #cbd5e1;
+}
+[data-theme="dark"] .manual-validator-clear-btn:hover {
+ background: #222c3d;
+}
+[data-theme="dark"] .ref-card {
+ background: #1c2333;
+ border-color: #2a3140;
+}
+[data-theme="dark"] .ref-card-label {
+ color: #cbd5e1;
+}
+[data-theme="dark"] .ref-card-remove {
+ color: #9ca3af;
+}
+[data-theme="dark"] .ref-parse-error {
+ color: #ef4444;
+}
+[data-theme="dark"] .ref-load-btn {
+ background: rgba(108, 92, 231, 0.12);
+ border-color: rgba(108, 92, 231, 0.3);
+ color: #a29bfe;
+}
+[data-theme="dark"] .ref-load-btn:hover {
+ background: rgba(108, 92, 231, 0.2);
+}
+[data-theme="dark"] .validator-msg-result {
+ border-bottom-color: #2a3140;
+}
+[data-theme="dark"] .validator-summary {
+ border-bottom-color: #2a3140;
+}
+[data-theme="dark"] .validator-summary--ok {
+ background: #12251a;
+}
+[data-theme="dark"] .validator-summary--fail {
+ background: #2a1717;
+}
+[data-theme="dark"] .validator-summary--ok .validator-summary-status {
+ color: #22c55e;
+}
+[data-theme="dark"] .validator-summary--fail .validator-summary-status {
+ color: #f87171;
+}
+[data-theme="dark"] .validator-severity--error {
+ background: #2a1717;
+ color: #f87171;
+}
+[data-theme="dark"] .validator-severity--warning {
+ background: #2a2412;
+ color: #f5b041;
+}
+[data-theme="dark"] .validator-severity--info {
+ background: #14202a;
+ color: #60a5fa;
+}
+[data-theme="dark"] .validator-fix-count {
+ background: #12251a;
+ color: #22c55e;
+}
+[data-theme="dark"] .validator-issues h3 {
+ color: #cbd5e1;
+}
+[data-theme="dark"] .validator-issue {
+ background: #1c2333;
+ border-color: #2a3140;
+}
+[data-theme="dark"] .validator-issue--fixed {
+ background: #12251a;
+ border-color: #1e3a2a;
+}
+[data-theme="dark"] .validator-issue-rule {
+ color: #a29bfe;
+}
+[data-theme="dark"] .validator-issue-msg {
+ color: #e5e7eb;
+}
+[data-theme="dark"] .fixer-settings-panel {
+ border-bottom-color: #2a3140;
+ background: #151a26;
+}
+[data-theme="dark"] .fixer-settings-panel h3 {
+ color: #cbd5e1;
+}
+[data-theme="dark"] .fixer-settings-checkbox span {
+ color: #cbd5e1;
+}
+[data-theme="dark"] .fixer-settings-field select,
+[data-theme="dark"] .fixer-settings-field input {
+ border-color: #2a3140;
+ background: #1c2333;
+ color: #e5e7eb;
+}
+[data-theme="dark"] .validator-fixing-overlay {
+ background: rgba(14, 17, 25, 0.85);
+}
+[data-theme="dark"] .validator-fixing-spinner {
+ border-color: #2a3140;
+ border-top-color: #6c5ce7;
+}
+
+/* ── Stepper ── */
+[data-theme="dark"] .stepper-info {
+ color: #cbd5e1;
+}
+[data-theme="dark"] .stepper-step {
+ background: rgba(255, 255, 255, 0.04);
+}
+[data-theme="dark"] .stepper-step-num {
+ background: rgba(255, 255, 255, 0.08);
+ color: #9ca3af;
+}
+
+/* ── Form Creator ── */
+[data-theme="dark"] .creator-layout--split .creator-chat {
+ border-right-color: #2a3140;
+}
+[data-theme="dark"] .creator-chat {
+ background: #151a26;
+}
+[data-theme="dark"] .creator-chat-empty {
+ color: #9ca3af;
+}
+[data-theme="dark"] .creator-chat-error {
+ background: #2a1717;
+ color: #f87171;
+}
+[data-theme="dark"] .creator-msg--assistant .creator-msg-label {
+ color: #cbd5e1;
+}
+[data-theme="dark"] .creator-msg-body {
+ color: #cbd5e1;
+}
+[data-theme="dark"] .creator-msg--user .creator-msg-body {
+ background: rgba(108, 92, 231, 0.14);
+ color: #e5e7eb;
+}
+[data-theme="dark"] .creator-msg--has-panel:hover {
+ border-color: #2a3140;
+ background: #1c2333;
+}
+[data-theme="dark"] .creator-msg--active-panel {
+ background: rgba(108, 92, 231, 0.12);
+}
+[data-theme="dark"] .creator-panel {
+ background: #151a26;
+}
+[data-theme="dark"] .creator-panel-header {
+ border-bottom-color: #2a3140;
+ background: #1c2333;
+}
+[data-theme="dark"] .creator-panel-title {
+ color: #e5e7eb;
+}
+[data-theme="dark"] .creator-panel-raw-toggle {
+ border-color: #2a3140;
+ background: #1c2333;
+ color: #9ca3af;
+}
+[data-theme="dark"] .creator-panel-raw-toggle:hover {
+ background: #222c3d;
+ color: #cbd5e1;
+}
+[data-theme="dark"] .creator-panel-raw-toggle--active {
+ background: rgba(108, 92, 231, 0.14);
+ color: #a29bfe;
+}
+[data-theme="dark"] .creator-panel-source {
+ color: #cbd5e1;
+}
+[data-theme="dark"] .creator-panel-empty-title {
+ color: #9ca3af;
+}
+[data-theme="dark"] .creator-panel-actions {
+ border-top-color: #2a3140;
+ background: #1c2333;
+}
+[data-theme="dark"] .creator-reject-btn {
+ border-color: #2a3140;
+ background: #1c2333;
+ color: #9ca3af;
+}
+[data-theme="dark"] .creator-reject-btn:hover {
+ background: #2a1717;
+ color: #f87171;
+ border-color: #7f1d1d;
+}
+[data-theme="dark"] .creator-approved {
+ border-top-color: #2a3140;
+ background: #151a26;
+}
+[data-theme="dark"] .creator-approved-header {
+ color: #9ca3af;
+}
+[data-theme="dark"] .creator-approved-header:hover {
+ background: #1c2333;
+}
+[data-theme="dark"] .creator-approved-card {
+ background: #1c2333;
+ border-color: #2a3140;
+}
+[data-theme="dark"] .creator-approved-label {
+ color: #cbd5e1;
+}
+[data-theme="dark"] .creator-prompt-generator {
+ background: #151a26;
+}
+[data-theme="dark"] .creator-prompt-generator-header {
+ border-bottom-color: #2a3140;
+ background: #1c2333;
+}
+[data-theme="dark"] .creator-prompt-generator-title {
+ color: #e5e7eb;
+}
+[data-theme="dark"] .creator-prompt-back-btn:hover {
+ background: rgba(108, 92, 231, 0.14);
+}
+[data-theme="dark"] .creator-prompt-label {
+ color: #cbd5e1;
+}
+[data-theme="dark"] .creator-prompt-textarea {
+ border-color: #2a3140;
+ background: #1c2333;
+ color: #e5e7eb;
+}
+[data-theme="dark"] .creator-prompt-generate-btn:disabled {
+ background: #2a3140;
+}
+[data-theme="dark"] .creator-prompt-error {
+ background: #2a1717;
+ color: #f87171;
+}
+[data-theme="dark"] .creator-prompt-output-section {
+ border-color: #2a3140;
+}
+[data-theme="dark"] .creator-prompt-output-header {
+ background: #1c2333;
+ border-bottom-color: #2a3140;
+ color: #cbd5e1;
+}
+[data-theme="dark"] .creator-prompt-output {
+ background: #1c2333;
+ color: #e5e7eb;
+}
+[data-theme="dark"] .creator-prompt-test-bar {
+ border-top-color: #2a3140;
+ background: #1c2333;
+}
+
+/* ── Agent Chat ── */
+[data-theme="dark"] .agent-thinking {
+ border-color: rgba(108, 92, 231, 0.25);
+ background: rgba(108, 92, 231, 0.06);
+}
+[data-theme="dark"] .agent-thinking-summary {
+ color: #a29bfe;
+ background: rgba(108, 92, 231, 0.1);
+}
+[data-theme="dark"] .agent-thinking-content {
+ border-top-color: rgba(108, 92, 231, 0.2);
+}
+[data-theme="dark"] .agent-thinking-content pre {
+ color: #cbd5e1;
+}
+[data-theme="dark"] .agent-text-content p {
+ color: #e5e7eb;
+}
+[data-theme="dark"] .agent-text-h1,
+[data-theme="dark"] .agent-text-h2,
+[data-theme="dark"] .agent-text-h3 {
+ color: #e5e7eb;
+}
+[data-theme="dark"] .agent-text-hr {
+ border-top-color: #2a3140;
+}
+[data-theme="dark"] .agent-text-table th,
+[data-theme="dark"] .agent-text-table td {
+ border-color: #2a3140;
+ color: #e5e7eb;
+}
+[data-theme="dark"] .agent-text-table thead th {
+ background: #1c2333;
+}
+[data-theme="dark"] .agent-text-table tbody tr:nth-child(even) td {
+ background: #171c28;
+}
+[data-theme="dark"] .agent-text-list li {
+ color: #e5e7eb;
+}
+[data-theme="dark"] .agent-inline-code {
+ background: #1c2333;
+ border-color: #2a3140;
+ color: #a29bfe;
+}
+[data-theme="dark"] .agent-tool-call {
+ border-color: rgba(34, 197, 94, 0.3);
+ background: rgba(34, 197, 94, 0.06);
+}
+[data-theme="dark"] .agent-tool-call-header {
+ color: #34d399;
+ border-bottom-color: rgba(34, 197, 94, 0.25);
+ background: rgba(34, 197, 94, 0.14);
+}
+[data-theme="dark"] .agent-settings-note--storage {
+ color: #9ca3af;
+ background: rgba(34, 197, 94, 0.06);
+ border-color: rgba(34, 197, 94, 0.25);
+}
+
+/* ── Docs content (right reading pane; left nav stays dark) ── */
+[data-theme="dark"] .docs-package-tagline {
+ color: #cbd5e1;
+}
+[data-theme="dark"] .docs-inprogress-item {
+ background: #1c2333;
+ border-color: #2a3140;
+}
+[data-theme="dark"] .docs-inprogress-name {
+ color: #cbd5e1;
+}
+[data-theme="dark"] .docs-inprogress-badge {
+ background: #2a2412;
+ color: #f5b041;
+}
+[data-theme="dark"] .docs-intro-footnote,
+[data-theme="dark"] .docs-intro-footnote a {
+ color: #9ca3af;
+}
+[data-theme="dark"] .docs-runtime-demo {
+ border-color: #2a3140;
+ background: #151a26;
+}
+[data-theme="dark"] .docs-runtime-demo-doc {
+ border-right-color: #2a3140;
+}
+[data-theme="dark"] .docs-runtime-demo-log-header {
+ border-bottom-color: #2a3140;
+}
+[data-theme="dark"] .docs-runtime-demo-clear:hover {
+ color: #cbd5e1;
+}
+[data-theme="dark"] .docs-package-see-also {
+ background: rgba(108, 92, 231, 0.1);
+ border-color: rgba(108, 92, 231, 0.25);
+ color: #cbd5e1;
+}
+[data-theme="dark"] .docs-content h2 {
+ color: #e5e7eb;
+}
+[data-theme="dark"] .docs-content p {
+ color: #cbd5e1;
+}
+[data-theme="dark"] .docs-content code {
+ background: #1c2333;
+ border-color: #2a3140;
+ color: #a29bfe;
+}
+[data-theme="dark"] .docs-table th {
+ background: #1c2333;
+ border-color: #2a3140;
+ color: #cbd5e1;
+}
+[data-theme="dark"] .docs-table td {
+ border-color: #2a3140;
+ color: #cbd5e1;
+}
+[data-theme="dark"] .docs-table tr:hover td {
+ background: #1c2333;
+}
+[data-theme="dark"] .docs-table-row--active td,
+[data-theme="dark"] .docs-table-row--active td code {
+ background: #14202a;
+ color: #60a5fa;
+}
+[data-theme="dark"] .docs-table-row-clickable:hover td {
+ background: #1c2333;
+}
+[data-theme="dark"] .docs-preview-panel {
+ border-left-color: #2a3140;
+ background: #151a26;
+}
+[data-theme="dark"] .docs-example-toggle {
+ color: #a5b4fc;
+ background: rgba(99, 102, 241, 0.15);
+ border-color: rgba(99, 102, 241, 0.3);
+}
+[data-theme="dark"] .docs-example-toggle:hover {
+ background: rgba(99, 102, 241, 0.25);
+}
+[data-theme="dark"] .docs-convo-avatar--user {
+ background: #222c3d;
+ color: #cbd5e1;
+}
+[data-theme="dark"] .docs-convo-bubble {
+ background: #151a26;
+ border-color: #2a3140;
+ color: #e5e7eb;
+}
+[data-theme="dark"] .docs-convo-msg--user .docs-convo-bubble {
+ background: rgba(99, 102, 241, 0.12);
+ border-color: rgba(99, 102, 241, 0.3);
+}
+[data-theme="dark"] .docs-preview-panel-header {
+ background: #1c2333;
+ border-bottom-color: #2a3140;
+}
+[data-theme="dark"] .docs-preview-panel-label {
+ color: #e5e7eb;
+}
+[data-theme="dark"] .docs-preview-panel-type {
+ background: rgba(99, 102, 241, 0.15);
+ color: #a5b4fc;
+}
+[data-theme="dark"] .docs-preview-panel-render {
+ background: #151a26;
+ border-color: #2a3140;
+}
+[data-theme="dark"] .docs-list li {
+ color: #cbd5e1;
+}
+[data-theme="dark"] .docs-note {
+ color: #9ca3af !important;
+ background: #1c2333;
+}
+[data-theme="dark"] .docs-do {
+ background: #12251a;
+ border-color: rgba(34, 197, 94, 0.3);
+}
+[data-theme="dark"] .docs-dont {
+ background: #2a1717;
+ border-color: rgba(239, 68, 68, 0.3);
+}
+[data-theme="dark"] .docs-do h4 {
+ color: #22c55e;
+}
+[data-theme="dark"] .docs-dont h4 {
+ color: #ef4444;
+}
+
+/* ── Preview Layout ── */
+[data-theme="dark"] .preview-layout .preview-chat {
+ border-right-color: #2a3140;
+}
+[data-theme="dark"] .preview-layout .preview-pane {
+ background: #151a26;
+}
+[data-theme="dark"] .preview-layout .preview-pane-header {
+ border-bottom-color: #2a3140;
+ background: #1c2333;
+}
+[data-theme="dark"] .preview-layout .preview-pane-title {
+ color: #e5e7eb;
+}
+[data-theme="dark"] .preview-layout .preview-pane-status--idle {
+ background: #1c2333;
+ color: #9ca3af;
+}
+[data-theme="dark"] .preview-layout .preview-pane-status--validating,
+[data-theme="dark"] .preview-layout .preview-pane-status--fixing {
+ background: #2a2412;
+ color: #f5b041;
+}
+[data-theme="dark"] .preview-layout .preview-pane-status--ready {
+ background: #12251a;
+ color: #22c55e;
+}
+[data-theme="dark"] .preview-layout .preview-pane-status--invalid {
+ background: #2a1717;
+ color: #f87171;
+}
+[data-theme="dark"] .preview-layout .preview-pane-empty-title {
+ color: #cbd5e1;
+}
+[data-theme="dark"] .preview-layout .preview-callout-content {
+ color: #cbd5e1;
+}
+[data-theme="dark"] .preview-layout .preview-callout-dismiss:hover {
+ color: #e5e7eb;
+}
+[data-theme="dark"] .preview-layout .preview-callout--info {
+ background: #14202a;
+ border-color: rgba(37, 99, 235, 0.3);
+ border-left-color: #3b82f6;
+}
+[data-theme="dark"] .preview-layout .preview-callout--info .preview-callout-title {
+ color: #93c5fd;
+}
+[data-theme="dark"] .preview-layout .preview-callout--success {
+ background: #12251a;
+ border-color: rgba(34, 197, 94, 0.3);
+ border-left-color: #22c55e;
+}
+[data-theme="dark"] .preview-layout .preview-callout--success .preview-callout-title {
+ color: #34d399;
+}
+[data-theme="dark"] .preview-layout .preview-callout--warning {
+ background: #2a2412;
+ border-color: rgba(245, 176, 65, 0.3);
+ border-left-color: #f5b041;
+}
+[data-theme="dark"] .preview-layout .preview-callout--warning .preview-callout-title {
+ color: #f5b041;
+}
+[data-theme="dark"] .preview-layout .preview-callout--error {
+ background: #2a1717;
+ border-color: rgba(239, 68, 68, 0.3);
+ border-left-color: #ef4444;
+}
+[data-theme="dark"] .preview-layout .preview-callout--error .preview-callout-title {
+ color: #f87171;
+}
+[data-theme="dark"] .preview-layout .preview-pane-status--submitted {
+ background: rgba(99, 102, 241, 0.15);
+ color: #a5b4fc;
+}
+[data-theme="dark"] .preview-layout .preview-pane-note--submitted {
+ background: rgba(99, 102, 241, 0.12);
+ color: #a5b4fc;
+ border-color: rgba(99, 102, 241, 0.3);
+}
+[data-theme="dark"] .preview-layout .agent-tool-call--clickable:hover {
+ background: rgba(108, 92, 231, 0.1);
+}
+[data-theme="dark"] .preview-layout .agent-tool-call--active {
+ background: rgba(108, 92, 231, 0.16);
+}
+[data-theme="dark"] .preview-layout .preview-pane-note--fixed {
+ background: #2a2412;
+ color: #f5b041;
+ border-color: rgba(245, 176, 65, 0.3);
+}
+[data-theme="dark"] .preview-layout .preview-pane-note--invalid {
+ background: #2a1717;
+ color: #f87171;
+ border-color: rgba(239, 68, 68, 0.3);
+}
+[data-theme="dark"] .preview-layout .preview-pane-note--invalid code {
+ background: rgba(255, 255, 255, 0.08);
+}
+[data-theme="dark"] .preview-layout .preview-log-toggle {
+ border-color: #2a3140;
+ background: #151a26;
+ color: #cbd5e1;
+}
+[data-theme="dark"] .preview-layout .preview-log-drawer {
+ background: #151a26;
+ border-left-color: #2a3140;
+}
+[data-theme="dark"] .preview-layout .preview-log-drawer-header {
+ border-bottom-color: #2a3140;
+ background: #1c2333;
+}
+[data-theme="dark"] .preview-layout .preview-log-drawer-title {
+ color: #e5e7eb;
+}
+[data-theme="dark"] .preview-layout .preview-log-drawer-count {
+ background: rgba(99, 102, 241, 0.15);
+ color: #a5b4fc;
+}
+[data-theme="dark"] .preview-layout .preview-log-clear {
+ border-color: #2a3140;
+ background: #1c2333;
+ color: #cbd5e1;
+}
+[data-theme="dark"] .preview-layout .preview-log-clear:hover {
+ background: #222c3d;
+}
+[data-theme="dark"] .preview-layout .preview-log-drawer-close:hover {
+ color: #e5e7eb;
+}
+[data-theme="dark"] .preview-layout .preview-log-item {
+ background: #1c2333;
+ border-color: #2a3140;
+}
+[data-theme="dark"] .preview-layout .preview-log-item-method {
+ color: #cbd5e1;
+}
+[data-theme="dark"] .preview-layout .preview-log-item-status {
+ background: #12251a;
+ color: #22c55e;
+}
+[data-theme="dark"] .preview-layout .preview-log-item-claim {
+ background: rgba(99, 102, 241, 0.15);
+ color: #a5b4fc;
+}
+[data-theme="dark"] .preview-layout .preview-log-item-summary {
+ color: #cbd5e1;
+}
+
+/* ── React Native docs preview ── */
+[data-theme="dark"] .rn-open-snack {
+ color: #a5b4fc;
+ background: rgba(99, 102, 241, 0.15);
+ border-color: rgba(99, 102, 241, 0.3);
+}
+[data-theme="dark"] .rn-open-snack:hover {
+ background: rgba(99, 102, 241, 0.25);
+}
+
+/* ===== Light mode (nav bar + side nav) =====
+ * The top nav bar and docs side nav are dark-navy by default (that's the
+ * dark-mode look). In light mode they flip to a light surface so the whole app
+ * tracks the theme toggle. `data-theme` is always set on by the app. */
+
+/* --- Top nav bar --- */
+[data-theme="light"] .demo-header {
+ background: #ffffff;
+ color: #1a1a2e;
+}
+[data-theme="light"] .demo-subtitle {
+ color: #6b7280;
+ opacity: 1;
+}
+[data-theme="light"] .demo-nav-trigger {
+ background: rgba(108, 92, 231, 0.1);
+ border-color: rgba(108, 92, 231, 0.3);
+ color: #4c3fb0;
+}
+[data-theme="light"] .demo-nav-trigger:hover {
+ background: rgba(108, 92, 231, 0.18);
+ border-color: rgba(108, 92, 231, 0.5);
+}
+[data-theme="light"] .demo-nav-dropdown {
+ background: #ffffff;
+ border-color: #e5e7eb;
+ box-shadow: 0 12px 32px rgba(24, 24, 46, 0.14), 0 0 0 1px rgba(108, 92, 231, 0.06);
+}
+[data-theme="light"] .demo-nav-group + .demo-nav-group {
+ border-top-color: rgba(0, 0, 0, 0.07);
+}
+[data-theme="light"] .demo-nav-group-label {
+ color: #9ca3af;
+}
+[data-theme="light"] .demo-nav-item {
+ color: #374151;
+}
+[data-theme="light"] .demo-nav-item:hover {
+ background: rgba(108, 92, 231, 0.1);
+ color: #1a1a2e;
+}
+[data-theme="light"] .demo-nav-item--active {
+ background: rgba(108, 92, 231, 0.12);
+ color: #6c5ce7;
+}
+[data-theme="light"] .demo-nav-item--active:hover {
+ background: rgba(108, 92, 231, 0.18);
+ color: #4c3fb0;
+}
+[data-theme="light"] .demo-doc-selector {
+ background: #f8f9fa;
+ border-color: #e5e7eb;
+ color: #1a1a2e;
+}
+[data-theme="light"] .demo-doc-selector:hover {
+ background: #f0f0f5;
+}
+[data-theme="light"] .demo-doc-selector option {
+ background: #ffffff;
+ color: #1a1a2e;
+}
+
+/* --- Docs side nav --- */
+[data-theme="light"] .docs-nav {
+ background: #ffffff;
+ color: #1a1a2e;
+ border-right: 1px solid #e5e7eb;
+}
+[data-theme="light"] .docs-nav-title {
+ color: #9ca3af;
+}
+[data-theme="light"] .docs-nav-item {
+ color: #374151;
+}
+[data-theme="light"] .docs-nav-item:hover {
+ background: rgba(108, 92, 231, 0.1);
+ color: #1a1a2e;
+}
+[data-theme="light"] .docs-nav-item--active {
+ background: rgba(108, 92, 231, 0.12);
+ color: #6c5ce7;
+}
+[data-theme="light"] .docs-nav-sub-item {
+ border-left-color: rgba(0, 0, 0, 0.1);
+ color: #6b7280;
+}
+[data-theme="light"] .docs-nav-sub-item:hover {
+ background: rgba(108, 92, 231, 0.08);
+ color: #374151;
+ border-left-color: rgba(108, 92, 231, 0.5);
+}
+[data-theme="light"] .docs-nav-sub-item--active {
+ background: rgba(108, 92, 231, 0.12);
+ color: #6c5ce7;
+ border-left-color: #6c5ce7;
+}
+
+/* ===== Theming playground (docs) ===== */
+.theming-pickers {
+ display: flex;
+ align-items: flex-end;
+ flex-wrap: wrap;
+ gap: 16px;
+ margin: 8px 0 24px;
+}
+.theming-picker {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ gap: 5px;
+ font-size: 12px;
+}
+.theming-picker input[type="color"] {
+ width: 46px;
+ height: 46px;
+ padding: 0;
+ border: 1px solid #d1d5db;
+ border-radius: 10px;
+ background: none;
+ cursor: pointer;
+}
+.theming-picker input[type="color"]::-webkit-color-swatch-wrapper {
+ padding: 3px;
+}
+.theming-picker input[type="color"]::-webkit-color-swatch {
+ border: none;
+ border-radius: 7px;
+}
+.theming-picker-label {
+ font-weight: 600;
+ color: #374151;
+}
+.theming-picker-hex {
+ font-size: 11px;
+ color: #9ca3af;
+ font-variant-numeric: tabular-nums;
+}
+.theming-swatch-reset {
+ align-self: center;
+ padding: 6px 14px;
+ font-size: 13px;
+ font-weight: 600;
+ border: 1px solid #d1d5db;
+ border-radius: 999px;
+ background: #ffffff;
+ color: #374151;
+ cursor: pointer;
+ transition: background 0.1s, border-color 0.1s, color 0.1s;
+}
+.theming-swatch-reset:hover {
+ background: #f3f4f6;
+}
+[data-theme="dark"] .theming-picker input[type="color"] {
+ border-color: #2a3140;
+}
+[data-theme="dark"] .theming-picker-label {
+ color: #cbd5e1;
+}
+[data-theme="dark"] .theming-swatch-reset {
+ background: #1c2333;
+ border-color: #2a3140;
+ color: #cbd5e1;
+}
+[data-theme="dark"] .theming-swatch-reset:hover {
+ background: #222c3d;
+}
+
+/* ===== Mobile responsive ===== */
+
+/* Neutral borders below use rgba(128,128,128,.25) so they read acceptably in
+ both light and dark themes (never a pure-white line on a dark surface). */
+
+@media (max-width: 860px) {
+ /* Guard against any fixed-width child forcing horizontal page scroll. */
+ html,
+ body {
+ max-width: 100%;
+ overflow-x: hidden;
+ }
+
+ /* ── Header — keep it on one row ─────────────────────────────── */
+ .demo-header {
+ padding: 10px 12px;
+ }
+ .demo-header-left {
+ gap: 8px;
+ min-width: 0;
+ }
+ .demo-header-right {
+ gap: 8px;
+ min-width: 0;
+ }
+ .demo-logo {
+ height: 60px;
+ }
+ .demo-subtitle {
+ display: none;
+ }
+ .demo-ph-badge {
+ display: none;
+ }
+ /* Keep the nav dropdown inside the viewport. */
+ .demo-nav-dropdown {
+ right: 0;
+ max-width: calc(100vw - 24px);
+ }
+
+ /* ── Docs layout → stack vertically ──────────────────────────── */
+ .docs-layout {
+ flex-direction: column;
+ overflow: auto;
+ }
+ /* The side nav collapses into a hamburger-toggled drawer; the mobile bar
+ (hamburger + breadcrumb) sits at the top. */
+ .docs-mobile-bar {
+ display: flex;
+ }
+ .docs-nav {
+ display: none;
+ }
+ .docs-nav--open {
+ display: flex;
+ width: 100%;
+ max-height: 65vh;
+ overflow-y: auto;
+ border-right: none;
+ border-bottom: 1px solid rgba(128, 128, 128, 0.25);
+ }
+ /* Flow naturally in the stacked column instead of being an internal
+ scroll box (the base rule's flex:1 + overflow:auto would clip content). */
+ .docs-content {
+ padding: 24px 18px;
+ flex: none;
+ overflow: visible;
+ }
+ /* Preview panel drops below the content, full width. */
+ .docs-layout--with-preview {
+ overflow: auto;
+ }
+ .docs-preview-panel,
+ .docs-layout--rn .docs-preview-panel {
+ flex: none;
+ width: 100%;
+ border-left: none;
+ border-top: 1px solid rgba(128, 128, 128, 0.25);
+ min-height: 360px;
+ overflow: visible;
+ }
+ .docs-preview-panel-render {
+ max-width: 100%;
+ }
+ /* Runtime demo (doc + log) stacks too. */
+ .docs-runtime-demo {
+ flex-direction: column;
+ }
+ .docs-runtime-demo-doc {
+ border-right: none;
+ border-bottom: 1px solid rgba(128, 128, 128, 0.25);
+ }
+ .docs-runtime-demo-log {
+ width: 100%;
+ }
+
+ /* ── Validator → stack ───────────────────────────────────────── */
+ .validator-content {
+ flex-direction: column;
+ }
+ .validator-side-panel {
+ width: 100%;
+ flex-shrink: 0;
+ border-left: none;
+ border-top: 1px solid rgba(128, 128, 128, 0.25);
+ }
+ /* Variant selector already scrolls; let it wrap if it prefers. */
+ .validator-variant-selector {
+ flex-wrap: wrap;
+ }
+
+ /* ── Chat / Author + Custom settings ─────────────────────────── */
+ .chat-settings {
+ padding: 0 12px 12px;
+ }
+ .chat-settings-fields {
+ grid-template-columns: 1fr;
+ }
+ .ai-settings-presets {
+ flex-wrap: wrap;
+ }
+ .ai-setting-model-group {
+ flex-wrap: wrap;
+ }
+ .chat-input-bar {
+ padding: 12px 12px 16px;
+ }
+ /* Action-log side panel stacks under the chat instead of sitting beside it. */
+ .chat-layout--with-log {
+ flex-direction: column;
+ }
+ .chat-action-log-panel {
+ width: 100%;
+ border-left: none;
+ border-top: 1px solid rgba(128, 128, 128, 0.25);
+ }
+
+ /* ── Fixed-width side panels / settings — clamp to viewport ───── */
+ .demo-event-panel {
+ width: 100%;
+ max-width: 100%;
+ }
+ .fixer-settings-panel {
+ max-width: 100%;
+ }
+
+ /* ── General padding reductions ──────────────────────────────── */
+ .home-view {
+ padding: 28px 16px;
+ gap: 32px;
+ }
+ .home-hero-title {
+ font-size: 26px;
+ }
+ .demo-document-panel {
+ padding: 20px 16px;
+ }
+}
+
+@media (max-width: 520px) {
+ /* Tighten the header further and drop the star count on tiny screens. */
+ .demo-header {
+ padding: 8px 10px;
+ }
+ .demo-header-right {
+ gap: 6px;
+ }
+ .demo-logo {
+ height: 52px;
+ }
+ .demo-star-count {
+ display: none;
+ }
+ .demo-star-btn {
+ padding: 6px 10px;
+ }
+ .demo-nav-trigger {
+ padding: 7px 10px;
+ }
+
+ .docs-content {
+ padding: 18px 14px;
+ }
+ .home-view {
+ padding: 20px 12px;
+ }
+ /* Preview/agent chat + pane split already stacks at 900px; give the pane a
+ minimum so it stays usable on phones. */
+ .preview-layout .preview-pane {
+ min-height: 320px;
+ }
+}
+
+@media (max-width: 640px) {
+ /* Free room in the chat input footer so the Send button never clips. */
+ .chat-input-hint {
+ display: none;
+ }
+}
diff --git a/demo/src/theme-context.ts b/demo/src/theme-context.ts
new file mode 100644
index 0000000..9bb40f1
--- /dev/null
+++ b/demo/src/theme-context.ts
@@ -0,0 +1,15 @@
+import { createContext, useContext } from 'react';
+
+/** Theme applied to the MDMA examples shown throughout the demo. */
+export type ThemeMode = 'light' | 'dark' | 'auto';
+
+/**
+ * The web examples inherit the theme through `MdmaThemeProvider`, but the React
+ * Native preview renders via the separate RN renderer (its own context), so it
+ * reads the current mode from here instead.
+ */
+export const DemoThemeContext = createContext('light');
+
+export function useDemoThemeMode(): ThemeMode {
+ return useContext(DemoThemeContext);
+}
diff --git a/docs/guides/theming.md b/docs/guides/theming.md
new file mode 100644
index 0000000..0798ce4
--- /dev/null
+++ b/docs/guides/theming.md
@@ -0,0 +1,135 @@
+# Theming
+
+MDMA renderers ship with a polished default look and a first-class theming layer on top of it. Theming is **opt-in**: render a document with no `theme` and you get the built-in light palette, unchanged. When you want more, you can switch to a dark palette, follow the operating-system preference, or hand over a fully custom set of design tokens.
+
+Both the web renderer (`@mobile-reality/mdma-renderer-react`) and the React Native renderer (`@mobile-reality/mdma-renderer-react-native`) expose the **same `MdmaTheme` token shape**, so a custom theme object is portable between the two.
+
+## The three modes
+
+Everything is driven by the `theme` prop on `MdmaDocument`:
+
+| `theme` value | Result |
+| -------------------- | ------------------------------------------------------------- |
+| *(omitted)* | Default light palette. No behavior change — fully backward compatible. |
+| `"light"` / `"dark"` | The built-in light or dark palette. |
+| `"auto"` | Follows the OS preference (`prefers-color-scheme` on web, `useColorScheme()` on native). |
+| `MdmaTheme` object | Your custom tokens. |
+
+```tsx
+ // not themed (default light)
+ // built-in dark
+ // follow the OS
+ // custom
+```
+
+## The `MdmaTheme` token shape
+
+A theme is a plain object. This is the whole contract:
+
+```ts
+interface MdmaTheme {
+ colors: {
+ background: string;
+ surface: string;
+ text: string;
+ textMuted: string;
+ border: string;
+ primary: string;
+ onPrimary: string;
+ secondary: string;
+ onSecondary: string;
+ danger: string;
+ onDanger: string;
+ // Callout accents + soft backgrounds
+ info: string;
+ infoBg: string;
+ warning: string;
+ warningBg: string;
+ error: string;
+ errorBg: string;
+ success: string;
+ successBg: string;
+ };
+ spacing: { xs: number; sm: number; md: number; lg: number };
+ radius: { sm: number; md: number };
+ fontSize: { small: number; body: number; label: number; title: number };
+}
+```
+
+The `lightTheme` and `darkTheme` presets are exported from both renderer packages, so the easiest way to build a custom theme is to spread a preset and override just what you need:
+
+```ts
+import { lightTheme, type MdmaTheme } from '@mobile-reality/mdma-renderer-react';
+
+const brandTheme: MdmaTheme = {
+ ...lightTheme,
+ colors: { ...lightTheme.colors, primary: '#e91e63', onPrimary: '#ffffff' },
+ radius: { sm: 10, md: 16 },
+};
+```
+
+## Web renderer
+
+Import the stylesheet once, then pass a `theme`:
+
+```tsx
+import { MdmaDocument, lightTheme } from '@mobile-reality/mdma-renderer-react';
+import '@mobile-reality/mdma-renderer-react/styles.css';
+
+ ;
+```
+
+### How it works — CSS variables
+
+The stylesheet expresses every color, radius, and size as a `--mdma-*` CSS custom property scoped to `.mdma-document`. The defaults are the light palette, so importing the CSS with no theme renders exactly as before.
+
+- `theme="light" | "dark" | "auto"` sets a `data-theme` attribute on the document root; the stylesheet swaps the palette (and `"auto"` is gated behind a `prefers-color-scheme` media query).
+- A custom `MdmaTheme` object is written as inline CSS variables on the root, overriding the defaults.
+
+Because it's all CSS variables, you can also theme **without touching the `theme` prop** — just set the variables in your own CSS:
+
+```css
+.mdma-document {
+ --mdma-color-primary: #e91e63;
+ --mdma-radius-md: 16px;
+}
+```
+
+The public color tokens map to `--mdma-color-` (e.g. `onPrimary` → `--mdma-color-on-primary`, `infoBg` → `--mdma-color-info-bg`); numeric scales become `--mdma-radius-*`, `--mdma-spacing-*`, and `--mdma-font-size-*` in pixels.
+
+### Rendering blocks without `MdmaDocument`
+
+If you render a lone `` outside of `MdmaDocument`, wrap it in `MdmaThemeProvider` so the variables cascade:
+
+```tsx
+import { MdmaThemeProvider, MdmaBlock } from '@mobile-reality/mdma-renderer-react';
+
+
+
+ ;
+```
+
+## React Native renderer
+
+```tsx
+import { MdmaDocument } from '@mobile-reality/mdma-renderer-react-native';
+
+ ;
+```
+
+`MdmaDocument` wraps its children in an `MdmaThemeProvider`. There is no stylesheet — each renderer builds its `StyleSheet` from the resolved tokens, which any custom component can read with `useMdmaTheme()`:
+
+```tsx
+import { useMdmaTheme } from '@mobile-reality/mdma-renderer-react-native';
+
+function Badge() {
+ const theme = useMdmaTheme();
+ return ;
+}
+```
+
+## Notes
+
+- **Portability.** The `colors`/`spacing`/`radius`/`fontSize` shape is identical across renderers, so a shared theme object can drive both your web and native apps. The built-in `lightTheme`/`darkTheme` *values* differ slightly per platform to match each renderer's native look.
+- **Backward compatible.** Existing code that never sets `theme` is unaffected — the default light palette is the previous look.
+- **Presentation stays in the renderer.** Themes live entirely in the renderers; the MDMA spec and prompts never describe appearance, so the same document renders under any theme.
diff --git a/packages/agui/package.json b/packages/agui/package.json
index 0f45a1a..ac3caf4 100644
--- a/packages/agui/package.json
+++ b/packages/agui/package.json
@@ -67,9 +67,7 @@
"typescript": "^5.7.0",
"vitest": "^3.2.0"
},
- "files": [
- "dist"
- ],
+ "files": ["dist"],
"publishConfig": {
"access": "public"
},
diff --git a/packages/attachables-core/package.json b/packages/attachables-core/package.json
index 7ab4072..6d9d88f 100644
--- a/packages/attachables-core/package.json
+++ b/packages/attachables-core/package.json
@@ -31,9 +31,7 @@
"typescript": "^5.7.0",
"vitest": "^3.2.0"
},
- "files": [
- "dist"
- ],
+ "files": ["dist"],
"publishConfig": {
"access": "public"
},
diff --git a/packages/cli/package.json b/packages/cli/package.json
index b30befd..7a76914 100644
--- a/packages/cli/package.json
+++ b/packages/cli/package.json
@@ -1,15 +1,7 @@
{
"name": "@mobile-reality/mdma-cli",
"version": "0.2.9",
- "keywords": [
- "mdma",
- "markdown",
- "ai",
- "llm",
- "interactive-document",
- "cli",
- "prompt-builder"
- ],
+ "keywords": ["mdma", "markdown", "ai", "llm", "interactive-document", "cli", "prompt-builder"],
"type": "module",
"bin": {
"mdma": "./dist/bin/mdma.js"
@@ -68,10 +60,7 @@
"vite": "^6.0.0",
"vitest": "^3.2.0"
},
- "files": [
- "dist",
- "app-dist"
- ],
+ "files": ["dist", "app-dist"],
"publishConfig": {
"access": "public"
},
diff --git a/packages/mcp/package.json b/packages/mcp/package.json
index 35b689f..e21ae8a 100644
--- a/packages/mcp/package.json
+++ b/packages/mcp/package.json
@@ -40,9 +40,7 @@
"vitest": "^3.2.0",
"zod": "^3.24.0"
},
- "files": [
- "dist"
- ],
+ "files": ["dist"],
"publishConfig": {
"access": "public"
},
diff --git a/packages/parser/package.json b/packages/parser/package.json
index 004c87e..b07dae8 100644
--- a/packages/parser/package.json
+++ b/packages/parser/package.json
@@ -41,9 +41,7 @@
"vitest": "^3.2.0",
"zod": "^3.24.0"
},
- "files": [
- "dist"
- ],
+ "files": ["dist"],
"publishConfig": {
"access": "public"
},
diff --git a/packages/prompt-pack/package.json b/packages/prompt-pack/package.json
index c4070a5..d3351f6 100644
--- a/packages/prompt-pack/package.json
+++ b/packages/prompt-pack/package.json
@@ -37,9 +37,7 @@
"typescript": "^5.7.0",
"vitest": "^3.2.0"
},
- "files": [
- "dist"
- ],
+ "files": ["dist"],
"publishConfig": {
"access": "public"
},
diff --git a/packages/renderer-react-native/README.md b/packages/renderer-react-native/README.md
index 704735c..95fee43 100644
--- a/packages/renderer-react-native/README.md
+++ b/packages/renderer-react-native/README.md
@@ -43,8 +43,13 @@ function Screen({ ast, store }) {
### Theming
-`MdmaDocument` wraps its children in an `MdmaThemeProvider`. Pass `theme="light" | "dark"` or a
-full `MdmaTheme` token object. Renderers read tokens via `useMdmaTheme()`.
+`MdmaDocument` wraps its children in an `MdmaThemeProvider`. Pass `theme="light" | "dark" | "auto"`
+(`"auto"` follows the OS color scheme via `useColorScheme()`) or a full `MdmaTheme` token object.
+Renderers read tokens via `useMdmaTheme()`. Omit `theme` entirely for the default light palette.
+
+The `MdmaTheme` token shape is shared with the web renderer
+(`@mobile-reality/mdma-renderer-react`), so a custom theme object is portable between the two.
+See the repo [Theming guide](../../docs/guides/theming.md) for the full token reference.
### Customizations
diff --git a/packages/renderer-react-native/package.json b/packages/renderer-react-native/package.json
index 100b6cf..44e70de 100644
--- a/packages/renderer-react-native/package.json
+++ b/packages/renderer-react-native/package.json
@@ -46,9 +46,7 @@
"typescript": "^5.7.0",
"vitest": "^3.2.0"
},
- "files": [
- "dist"
- ],
+ "files": ["dist"],
"publishConfig": {
"access": "public"
},
diff --git a/packages/renderer-react-native/src/components/ChartRenderer.tsx b/packages/renderer-react-native/src/components/ChartRenderer.tsx
index 43de1b7..dfaafe3 100644
--- a/packages/renderer-react-native/src/components/ChartRenderer.tsx
+++ b/packages/renderer-react-native/src/components/ChartRenderer.tsx
@@ -70,9 +70,14 @@ export const ChartRenderer = memo(function ChartRenderer({
) : (
-
+
{data.headers.map((h) => (
;
}
-function isComponentConfig(
- entry: ComponentEntry,
-): entry is {
+function isComponentConfig(entry: ComponentEntry): entry is {
renderer?: ComponentType;
elements?: Record;
} {
diff --git a/packages/renderer-react-native/src/components/TableRenderer.tsx b/packages/renderer-react-native/src/components/TableRenderer.tsx
index 05a03ab..837e930 100644
--- a/packages/renderer-react-native/src/components/TableRenderer.tsx
+++ b/packages/renderer-react-native/src/components/TableRenderer.tsx
@@ -41,9 +41,14 @@ export const TableRenderer = memo(function TableRenderer({
-
+
{/* Header */}
{component.columns.map((col) => (
diff --git a/packages/renderer-react-native/src/theme/MdmaThemeProvider.tsx b/packages/renderer-react-native/src/theme/MdmaThemeProvider.tsx
index 1da9dca..0372055 100644
--- a/packages/renderer-react-native/src/theme/MdmaThemeProvider.tsx
+++ b/packages/renderer-react-native/src/theme/MdmaThemeProvider.tsx
@@ -1,92 +1,8 @@
import { createContext, useContext, useMemo, type ReactNode } from 'react';
+import { useColorScheme } from 'react-native';
+import { type MdmaTheme, lightTheme, darkTheme } from './tokens.js';
-/**
- * Design tokens for the RN renderer. This is the React Native answer to
- * `renderer-react`'s `styles.css`: instead of `.mdma-*` classes, every
- * renderer reads these tokens and builds `StyleSheet` values from them.
- * The default light/dark palettes are chosen to visually track the web look.
- */
-export interface MdmaTheme {
- colors: {
- background: string;
- surface: string;
- text: string;
- textMuted: string;
- border: string;
- primary: string;
- onPrimary: string;
- secondary: string;
- onSecondary: string;
- danger: string;
- onDanger: string;
- /** Callout variant accents + soft backgrounds. */
- info: string;
- infoBg: string;
- warning: string;
- warningBg: string;
- error: string;
- errorBg: string;
- success: string;
- successBg: string;
- };
- spacing: { xs: number; sm: number; md: number; lg: number };
- radius: { sm: number; md: number };
- fontSize: { small: number; body: number; label: number; title: number };
-}
-
-export const lightTheme: MdmaTheme = {
- colors: {
- background: '#ffffff',
- surface: '#f7f7f8',
- text: '#1a1a1a',
- textMuted: '#6b7280',
- border: '#e5e7eb',
- primary: '#2563eb',
- onPrimary: '#ffffff',
- secondary: '#e5e7eb',
- onSecondary: '#1a1a1a',
- danger: '#dc2626',
- onDanger: '#ffffff',
- info: '#2563eb',
- infoBg: '#eff6ff',
- warning: '#d97706',
- warningBg: '#fffbeb',
- error: '#dc2626',
- errorBg: '#fef2f2',
- success: '#16a34a',
- successBg: '#f0fdf4',
- },
- spacing: { xs: 4, sm: 8, md: 12, lg: 16 },
- radius: { sm: 6, md: 10 },
- fontSize: { small: 12, body: 14, label: 15, title: 17 },
-};
-
-export const darkTheme: MdmaTheme = {
- colors: {
- background: '#0b0f19',
- surface: '#151a26',
- text: '#f3f4f6',
- textMuted: '#9ca3af',
- border: '#2a3140',
- primary: '#3b82f6',
- onPrimary: '#ffffff',
- secondary: '#2a3140',
- onSecondary: '#f3f4f6',
- danger: '#ef4444',
- onDanger: '#ffffff',
- info: '#3b82f6',
- infoBg: '#172033',
- warning: '#f59e0b',
- warningBg: '#2a2312',
- error: '#ef4444',
- errorBg: '#2a1717',
- success: '#22c55e',
- successBg: '#12251a',
- },
- spacing: { xs: 4, sm: 8, md: 12, lg: 16 },
- radius: { sm: 6, md: 10 },
- fontSize: { small: 12, body: 14, label: 15, title: 17 },
-};
+export { type MdmaTheme, lightTheme, darkTheme } from './tokens.js';
const MdmaThemeContext = createContext(lightTheme);
@@ -95,17 +11,23 @@ export function useMdmaTheme(): MdmaTheme {
}
export interface MdmaThemeProviderProps {
- /** A full theme, or `'light'` / `'dark'` for the built-in palettes. */
- theme?: MdmaTheme | 'light' | 'dark';
+ /**
+ * A full theme, or one of the built-in palettes:
+ * - `'light'` / `'dark'` — the fixed built-in palettes.
+ * - `'auto'` — follows the OS color scheme via `useColorScheme()`.
+ */
+ theme?: MdmaTheme | 'light' | 'dark' | 'auto';
children: ReactNode;
}
export function MdmaThemeProvider({ theme = 'light', children }: MdmaThemeProviderProps) {
+ const colorScheme = useColorScheme();
const resolved = useMemo(() => {
+ if (theme === 'auto') return colorScheme === 'dark' ? darkTheme : lightTheme;
if (theme === 'light') return lightTheme;
if (theme === 'dark') return darkTheme;
return theme;
- }, [theme]);
+ }, [theme, colorScheme]);
return {children} ;
}
diff --git a/packages/renderer-react-native/src/theme/tokens.ts b/packages/renderer-react-native/src/theme/tokens.ts
new file mode 100644
index 0000000..ea3c7ef
--- /dev/null
+++ b/packages/renderer-react-native/src/theme/tokens.ts
@@ -0,0 +1,93 @@
+/**
+ * Design tokens for the RN renderer. This is the React Native answer to
+ * `renderer-react`'s `styles.css`: instead of `.mdma-*` classes, every
+ * renderer reads these tokens and builds `StyleSheet` values from them.
+ *
+ * The light/dark palettes are kept **identical** to the web renderer's built-in
+ * themes (`renderer-react`'s `lightTheme`/`darkTheme`) so MDMA content looks the
+ * same across web and native. Keep the two in sync when either changes.
+ *
+ * This module is intentionally free of any `react-native` import so the pure
+ * token values stay usable (and unit-testable) without an RN runtime.
+ */
+export interface MdmaTheme {
+ colors: {
+ background: string;
+ surface: string;
+ text: string;
+ textMuted: string;
+ border: string;
+ primary: string;
+ onPrimary: string;
+ secondary: string;
+ onSecondary: string;
+ danger: string;
+ onDanger: string;
+ /** Callout variant accents + soft backgrounds. */
+ info: string;
+ infoBg: string;
+ warning: string;
+ warningBg: string;
+ error: string;
+ errorBg: string;
+ success: string;
+ successBg: string;
+ };
+ spacing: { xs: number; sm: number; md: number; lg: number };
+ radius: { sm: number; md: number };
+ fontSize: { small: number; body: number; label: number; title: number };
+}
+
+export const lightTheme: MdmaTheme = {
+ colors: {
+ background: '#ffffff',
+ surface: '#f8f9fa',
+ text: '#333333',
+ textMuted: '#666666',
+ border: '#e0e0e0',
+ primary: '#6c5ce7',
+ onPrimary: '#ffffff',
+ secondary: '#dfe6e9',
+ onSecondary: '#2d3436',
+ danger: '#e74c3c',
+ onDanger: '#ffffff',
+ info: '#3498db',
+ infoBg: '#ebf5fb',
+ warning: '#f39c12',
+ warningBg: '#fef9e7',
+ error: '#e74c3c',
+ errorBg: '#fdedec',
+ success: '#27ae60',
+ successBg: '#eafaf1',
+ },
+ spacing: { xs: 4, sm: 8, md: 12, lg: 16 },
+ radius: { sm: 6, md: 10 },
+ fontSize: { small: 13, body: 14, label: 15, title: 18 },
+};
+
+export const darkTheme: MdmaTheme = {
+ colors: {
+ background: '#151a26',
+ surface: '#1c2333',
+ text: '#e5e7eb',
+ textMuted: '#9ca3af',
+ border: '#2a3140',
+ primary: '#8b7ff0',
+ onPrimary: '#ffffff',
+ secondary: '#2a3140',
+ onSecondary: '#e5e7eb',
+ danger: '#ef4444',
+ onDanger: '#ffffff',
+ info: '#5dade2',
+ infoBg: '#16283a',
+ warning: '#f5b041',
+ warningBg: '#2a2412',
+ error: '#ef4444',
+ errorBg: '#2a1717',
+ success: '#2ecc71',
+ successBg: '#12251a',
+ },
+ spacing: { xs: 4, sm: 8, md: 12, lg: 16 },
+ radius: { sm: 6, md: 10 },
+ fontSize: { small: 13, body: 14, label: 15, title: 18 },
+};
diff --git a/packages/renderer-react-native/tests/theme.test.ts b/packages/renderer-react-native/tests/theme.test.ts
index a750e6b..e744771 100644
--- a/packages/renderer-react-native/tests/theme.test.ts
+++ b/packages/renderer-react-native/tests/theme.test.ts
@@ -1,5 +1,5 @@
import { describe, it, expect } from 'vitest';
-import { lightTheme, darkTheme, type MdmaTheme } from '../src/theme/MdmaThemeProvider.js';
+import { lightTheme, darkTheme, type MdmaTheme } from '../src/theme/tokens.js';
// Structural parity guard: the light and dark palettes must expose exactly the
// same token shape, so a renderer styled against one never hits an undefined
diff --git a/packages/renderer-react/package.json b/packages/renderer-react/package.json
index b6787aa..ce80ef0 100644
--- a/packages/renderer-react/package.json
+++ b/packages/renderer-react/package.json
@@ -1,16 +1,7 @@
{
"name": "@mobile-reality/mdma-renderer-react",
"version": "0.4.1",
- "keywords": [
- "mdma",
- "markdown",
- "ai",
- "llm",
- "interactive-document",
- "react",
- "forms",
- "ui"
- ],
+ "keywords": ["mdma", "markdown", "ai", "llm", "interactive-document", "react", "forms", "ui"],
"type": "module",
"exports": {
".": {
@@ -38,10 +29,7 @@
"typescript": "^5.7.0",
"vitest": "^3.2.0"
},
- "files": [
- "dist",
- "styles.css"
- ],
+ "files": ["dist", "styles.css"],
"publishConfig": {
"access": "public"
},
diff --git a/packages/renderer-react/src/components/MdmaDocument.tsx b/packages/renderer-react/src/components/MdmaDocument.tsx
index be22d46..514fff5 100644
--- a/packages/renderer-react/src/components/MdmaDocument.tsx
+++ b/packages/renderer-react/src/components/MdmaDocument.tsx
@@ -1,12 +1,17 @@
import type { MdmaRoot, MdmaBlock as MdmaBlockType } from '@mobile-reality/mdma-spec';
import { MDMA_LANG_TAG } from '@mobile-reality/mdma-spec';
import type { DocumentStore } from '@mobile-reality/mdma-runtime';
-import { useRef, useMemo, type ComponentType } from 'react';
+import { useRef, useMemo, useContext, type ComponentType } from 'react';
import { MdmaProvider } from '../context/MdmaProvider.js';
import {
ElementOverridesProvider,
type ElementOverrides,
} from '../context/ElementOverridesContext.js';
+import {
+ MdmaThemeContext,
+ resolveThemeProps,
+ type MdmaThemeInput,
+} from '../theme/MdmaThemeProvider.js';
import { MdmaBlock } from './MdmaBlock.js';
import { MdmaBlockLoading } from './MdmaBlockLoading.js';
import { MdastRenderer } from './MdastRenderer.js';
@@ -48,6 +53,12 @@ export interface MdmaDocumentProps {
store: DocumentStore;
/** Rendering customizations (component renderers + element overrides). */
customizations?: MdmaRenderCustomizations;
+ /**
+ * Theme applied to the document root: `'light'` | `'dark'` | `'auto'`
+ * (follows the OS preference), or a full `MdmaTheme` for custom tokens.
+ * Omit to use the stylesheet's default (light) look.
+ */
+ theme?: MdmaThemeInput;
className?: string;
}
@@ -148,12 +159,20 @@ function buildPartialThinkingBlock(yaml: string): MdmaBlockType | null {
} as MdmaBlockType;
}
-export function MdmaDocument({ ast, store, customizations, className }: MdmaDocumentProps) {
+export function MdmaDocument({ ast, store, customizations, theme, className }: MdmaDocumentProps) {
const { renderers, elementOverrides } = useMemo(
() => splitComponents(customizations?.components),
[customizations?.components],
);
+ // Resolve our own `theme` prop, or inherit an ancestor `MdmaThemeProvider`'s
+ // theme when none is given (so a single provider can theme a whole app).
+ const inheritedTheme = useContext(MdmaThemeContext);
+ const themeProps = useMemo(
+ () => (theme !== undefined ? resolveThemeProps(theme) : inheritedTheme),
+ [theme, inheritedTheme],
+ );
+
// Cache of successfully parsed blocks by component ID. Prevents flickering
// during streaming when a block alternates between parsed and pending states
// as incomplete YAML temporarily fails to validate.
@@ -162,58 +181,64 @@ export function MdmaDocument({ ast, store, customizations, className }: MdmaDocu
return (
-
- {ast.children.map((child, index) => {
- if (isMdmaBlock(child)) {
- // Cache this successfully parsed block
- renderedBlocksRef.current.set(child.component.id, child);
- return
;
- }
- // Incomplete MDMA blocks (still streaming or failed validation)
- if (isPendingMdmaBlock(child)) {
- const pendingYaml = (child as { value?: string }).value;
- const pendingId = extractIdFromYaml(pendingYaml);
-
- // If this block was previously rendered, keep showing the rendered
- // version instead of flickering back to the loading skeleton.
- const cachedBlock = pendingId ? renderedBlocksRef.current.get(pendingId) : null;
- if (cachedBlock) {
- return (
-
- );
+
+
+ {ast.children.map((child, index) => {
+ if (isMdmaBlock(child)) {
+ // Cache this successfully parsed block
+ renderedBlocksRef.current.set(child.component.id, child);
+ return ;
}
+ // Incomplete MDMA blocks (still streaming or failed validation)
+ if (isPendingMdmaBlock(child)) {
+ const pendingYaml = (child as { value?: string }).value;
+ const pendingId = extractIdFromYaml(pendingYaml);
- // Thinking blocks stream their content live instead of showing
- // a loading skeleton — build a synthetic block from partial YAML.
- const pendingType = extractTypeFromYaml(pendingYaml);
- if (pendingType === 'thinking' && pendingYaml) {
- const partialBlock = buildPartialThinkingBlock(pendingYaml);
- if (partialBlock) {
+ // If this block was previously rendered, keep showing the rendered
+ // version instead of flickering back to the loading skeleton.
+ const cachedBlock = pendingId ? renderedBlocksRef.current.get(pendingId) : null;
+ if (cachedBlock) {
return (
);
}
- }
- return ;
- }
- // Render standard Markdown nodes (headings, paragraphs, lists, etc.)
- return (
- [0]['node']}
- />
- );
- })}
-
+ // Thinking blocks stream their content live instead of showing
+ // a loading skeleton — build a synthetic block from partial YAML.
+ const pendingType = extractTypeFromYaml(pendingYaml);
+ if (pendingType === 'thinking' && pendingYaml) {
+ const partialBlock = buildPartialThinkingBlock(pendingYaml);
+ if (partialBlock) {
+ return (
+
+ );
+ }
+ }
+
+ return ;
+ }
+ // Render standard Markdown nodes (headings, paragraphs, lists, etc.)
+ return (
+ [0]['node']}
+ />
+ );
+ })}
+
+
);
diff --git a/packages/renderer-react/src/index.ts b/packages/renderer-react/src/index.ts
index 66bfab3..591d64a 100644
--- a/packages/renderer-react/src/index.ts
+++ b/packages/renderer-react/src/index.ts
@@ -13,6 +13,18 @@ export {
type MdmaProviderProps,
type DataSources,
} from './context/MdmaProvider.js';
+export {
+ MdmaThemeProvider,
+ useMdmaTheme,
+ resolveThemeProps,
+ themeToCssVars,
+ lightTheme,
+ darkTheme,
+ type MdmaTheme,
+ type MdmaThemeInput,
+ type MdmaThemeProviderProps,
+ type ResolvedThemeProps,
+} from './theme/MdmaThemeProvider.js';
export {
ElementOverridesProvider,
useElementOverride,
diff --git a/packages/renderer-react/src/theme/MdmaThemeProvider.tsx b/packages/renderer-react/src/theme/MdmaThemeProvider.tsx
new file mode 100644
index 0000000..99bd98d
--- /dev/null
+++ b/packages/renderer-react/src/theme/MdmaThemeProvider.tsx
@@ -0,0 +1,217 @@
+import { createContext, useContext, useMemo, type CSSProperties, type ReactNode } from 'react';
+
+/**
+ * Design tokens for the web renderer. This is the typed mirror of the CSS
+ * custom properties in `styles.css`: the same token shape the React Native
+ * renderer exposes (`renderer-react-native`'s `MdmaTheme`), so a theme object
+ * is portable between the two renderers.
+ *
+ * You rarely need to read these tokens directly — the components are styled via
+ * `.mdma-*` classes that read the matching `--mdma-*` CSS variables. A custom
+ * theme just rewrites those variables (see {@link MdmaThemeProvider}).
+ */
+export interface MdmaTheme {
+ colors: {
+ background: string;
+ surface: string;
+ text: string;
+ textMuted: string;
+ border: string;
+ primary: string;
+ onPrimary: string;
+ secondary: string;
+ onSecondary: string;
+ danger: string;
+ onDanger: string;
+ /** Callout variant accents + soft backgrounds. */
+ info: string;
+ infoBg: string;
+ warning: string;
+ warningBg: string;
+ error: string;
+ errorBg: string;
+ success: string;
+ successBg: string;
+ };
+ spacing: { xs: number; sm: number; md: number; lg: number };
+ radius: { sm: number; md: number };
+ fontSize: { small: number; body: number; label: number; title: number };
+}
+
+/** Built-in light palette — matches the default `--mdma-*` values in `styles.css`. */
+export const lightTheme: MdmaTheme = {
+ colors: {
+ background: '#ffffff',
+ surface: '#f8f9fa',
+ text: '#333333',
+ textMuted: '#666666',
+ border: '#e0e0e0',
+ primary: '#6c5ce7',
+ onPrimary: '#ffffff',
+ secondary: '#dfe6e9',
+ onSecondary: '#2d3436',
+ danger: '#e74c3c',
+ onDanger: '#ffffff',
+ info: '#3498db',
+ infoBg: '#ebf5fb',
+ warning: '#f39c12',
+ warningBg: '#fef9e7',
+ error: '#e74c3c',
+ errorBg: '#fdedec',
+ success: '#27ae60',
+ successBg: '#eafaf1',
+ },
+ spacing: { xs: 4, sm: 8, md: 12, lg: 16 },
+ radius: { sm: 6, md: 10 },
+ fontSize: { small: 13, body: 14, label: 15, title: 18 },
+};
+
+/** Built-in dark palette — matches the `[data-theme='dark']` block in `styles.css`. */
+export const darkTheme: MdmaTheme = {
+ colors: {
+ background: '#151a26',
+ surface: '#1c2333',
+ text: '#e5e7eb',
+ textMuted: '#9ca3af',
+ border: '#2a3140',
+ primary: '#8b7ff0',
+ onPrimary: '#ffffff',
+ secondary: '#2a3140',
+ onSecondary: '#e5e7eb',
+ danger: '#ef4444',
+ onDanger: '#ffffff',
+ info: '#5dade2',
+ infoBg: '#16283a',
+ warning: '#f5b041',
+ warningBg: '#2a2412',
+ error: '#ef4444',
+ errorBg: '#2a1717',
+ success: '#2ecc71',
+ successBg: '#12251a',
+ },
+ spacing: { xs: 4, sm: 8, md: 12, lg: 16 },
+ radius: { sm: 6, md: 10 },
+ fontSize: { small: 13, body: 14, label: 15, title: 18 },
+};
+
+/**
+ * What can be passed as a `theme`:
+ * - `'light'` / `'dark'` — the built-in palettes (applied via `data-theme`).
+ * - `'auto'` — follows the OS `prefers-color-scheme`.
+ * - a full {@link MdmaTheme} — custom tokens, written inline as CSS variables.
+ * - `undefined` — no theme; the stylesheet's default (light) tokens apply.
+ */
+export type MdmaThemeInput = MdmaTheme | 'light' | 'dark' | 'auto';
+
+/**
+ * Nearest resolved theme. Holds the full {@link ResolvedThemeProps} (not just
+ * the tokens) so a descendant `MdmaDocument` with no `theme` prop of its own can
+ * inherit the ancestor's `data-theme` / inline variables and re-apply them to
+ * its own root. The default is the unthemed light look.
+ */
+const MdmaThemeContext = createContext({ theme: lightTheme });
+
+/** The resolved token object for the nearest theme (defaults to {@link lightTheme}). */
+export function useMdmaTheme(): MdmaTheme {
+ return useContext(MdmaThemeContext).theme;
+}
+
+export { MdmaThemeContext };
+
+function camelToKebab(key: string): string {
+ return key.replace(/[A-Z]/g, (m) => `-${m.toLowerCase()}`);
+}
+
+/**
+ * Convert an {@link MdmaTheme} into the inline CSS-variable style object the
+ * `.mdma-*` classes read. Numeric scales are emitted as `px`.
+ */
+export function themeToCssVars(theme: MdmaTheme): CSSProperties {
+ const vars: Record = {};
+ for (const [key, value] of Object.entries(theme.colors)) {
+ vars[`--mdma-color-${camelToKebab(key)}`] = value;
+ }
+ for (const [key, value] of Object.entries(theme.spacing)) {
+ vars[`--mdma-spacing-${key}`] = `${value}px`;
+ }
+ for (const [key, value] of Object.entries(theme.radius)) {
+ vars[`--mdma-radius-${key}`] = `${value}px`;
+ }
+ for (const [key, value] of Object.entries(theme.fontSize)) {
+ vars[`--mdma-font-size-${camelToKebab(key)}`] = `${value}px`;
+ }
+ return vars as CSSProperties;
+}
+
+export interface ResolvedThemeProps {
+ /** `data-theme` attribute value, when a built-in palette is selected. */
+ dataTheme?: 'light' | 'dark' | 'auto';
+ /** Inline CSS variables, when a custom theme object is provided. */
+ style?: CSSProperties;
+ /** The resolved token object exposed via {@link useMdmaTheme}. */
+ theme: MdmaTheme;
+}
+
+/**
+ * Resolve a {@link MdmaThemeInput} into the DOM props that apply it: a
+ * `data-theme` attribute for built-in palettes, or inline CSS variables for a
+ * custom theme. Shared by {@link MdmaThemeProvider} and `MdmaDocument`.
+ */
+export function resolveThemeProps(theme: MdmaThemeInput | undefined): ResolvedThemeProps {
+ if (theme === undefined) return { theme: lightTheme };
+ if (theme === 'light') return { dataTheme: 'light', theme: lightTheme };
+ if (theme === 'dark') return { dataTheme: 'dark', theme: darkTheme };
+ // `auto` follows the OS preference at the CSS layer; the resolved token
+ // object is a best-effort light default for any JS consumer of useMdmaTheme.
+ if (theme === 'auto') return { dataTheme: 'auto', theme: lightTheme };
+ // A custom theme sets the public tokens inline, but the stylesheet also has
+ // internal derived vars (heading text, code backgrounds, …) that aren't on
+ // the public type. Pick the light/dark base by the theme's own background so
+ // those derived vars match — otherwise a custom *dark* theme would render,
+ // e.g., near-black heading text on a dark surface.
+ return {
+ dataTheme: isDarkTheme(theme) ? 'dark' : 'light',
+ style: themeToCssVars(theme),
+ theme,
+ };
+}
+
+/** Heuristic: is this theme's background dark? (relative luminance < 50%). */
+function isDarkTheme(theme: MdmaTheme): boolean {
+ const hex = theme.colors.background.replace('#', '');
+ if (hex.length < 6) return false;
+ const r = Number.parseInt(hex.slice(0, 2), 16);
+ const g = Number.parseInt(hex.slice(2, 4), 16);
+ const b = Number.parseInt(hex.slice(4, 6), 16);
+ if (Number.isNaN(r + g + b)) return false;
+ return 0.299 * r + 0.587 * g + 0.114 * b < 128;
+}
+
+export interface MdmaThemeProviderProps {
+ /** `'light'` | `'dark'` | `'auto'`, or a full {@link MdmaTheme}. */
+ theme?: MdmaThemeInput;
+ /** Extra class names for the wrapper element. */
+ className?: string;
+ /** Extra inline styles, merged after the theme's CSS variables. */
+ style?: CSSProperties;
+ children: ReactNode;
+}
+
+/**
+ * Standalone theme wrapper for rendering MDMA components *without* `MdmaDocument`
+ * (e.g. a lone ``). Renders a `.mdma-theme-root` element carrying the
+ * theme so the `--mdma-*` variables cascade to everything inside. When you use
+ * `MdmaDocument`, pass `theme` to it directly instead — it themes its own root.
+ */
+export function MdmaThemeProvider({ theme, className, style, children }: MdmaThemeProviderProps) {
+ const resolved = useMemo(() => resolveThemeProps(theme), [theme]);
+ return (
+
+ {children}
+
+ );
+}
diff --git a/packages/renderer-react/styles.css b/packages/renderer-react/styles.css
index 685e198..8b2ad89 100644
--- a/packages/renderer-react/styles.css
+++ b/packages/renderer-react/styles.css
@@ -1,7 +1,213 @@
+/* ===== MDMA Theme Tokens =====
+ *
+ * Every color/radius/size below is driven by a CSS custom property so the look
+ * can be themed without touching component markup. The default values encode
+ * the built-in *light* theme, so importing this stylesheet with no theme set
+ * renders exactly as before — theming is opt-in.
+ *
+ * How the knobs are set:
+ * - No theme → these defaults apply (light).
+ * - theme="light|dark" → `data-theme` on the root swaps the palette (below).
+ * - theme="auto" → follows `prefers-color-scheme`.
+ * - a custom MdmaTheme → `MdmaThemeProvider` writes the public `--mdma-*`
+ * tokens inline on the root, overriding these.
+ *
+ * `--mdma-color-*` names ending without a `-text`/`-bg` suffix that also appear
+ * on the `MdmaTheme` TS type are the *public* tokens a custom theme can set.
+ * The rest are internal derivations that follow the light/dark palette.
+ */
+.mdma-document,
+.mdma-theme-root {
+ /* Public palette (mirrors the MdmaTheme `colors` shape) */
+ --mdma-color-background: #ffffff;
+ --mdma-color-surface: #f8f9fa;
+ --mdma-color-text: #333333;
+ --mdma-color-text-muted: #666666;
+ --mdma-color-border: #e0e0e0;
+ --mdma-color-primary: #6c5ce7;
+ --mdma-color-on-primary: #ffffff;
+ --mdma-color-secondary: #dfe6e9;
+ --mdma-color-on-secondary: #2d3436;
+ --mdma-color-danger: #e74c3c;
+ --mdma-color-on-danger: #ffffff;
+ --mdma-color-info: #3498db;
+ --mdma-color-info-bg: #ebf5fb;
+ --mdma-color-warning: #f39c12;
+ --mdma-color-warning-bg: #fef9e7;
+ --mdma-color-error: #e74c3c;
+ --mdma-color-error-bg: #fdedec;
+ --mdma-color-success: #27ae60;
+ --mdma-color-success-bg: #eafaf1;
+
+ /* Internal derivations (follow the palette; not on the public TS type) */
+ --mdma-color-heading: #1a1a2e;
+ --mdma-color-text-subtle: #555555;
+ --mdma-color-border-subtle: #f0f0f0;
+ --mdma-color-primary-hover: color-mix(in srgb, var(--mdma-color-primary) 85%, #000000);
+ --mdma-color-secondary-hover: color-mix(in srgb, var(--mdma-color-secondary) 88%, #000000);
+ --mdma-color-danger-hover: color-mix(in srgb, var(--mdma-color-danger) 84%, #000000);
+ --mdma-color-input-bg: #fafafa;
+ --mdma-color-focus-ring: color-mix(in srgb, var(--mdma-color-primary) 22%, transparent);
+ --mdma-color-info-text: #1a5276;
+ --mdma-color-warning-text: #7d6608;
+ --mdma-color-error-text: #78281f;
+ --mdma-color-success-text: #1e8449;
+ --mdma-color-code-bg: #1e1e2e;
+ --mdma-color-code-text: #e0e0e0;
+ --mdma-color-inline-code-bg: #f0f0f5;
+ --mdma-color-inline-code-text: #c0392b;
+ --mdma-color-loading-bg: #ffffff;
+ --mdma-color-loading-border: #c4bfff;
+ --mdma-color-loading-accent: #6c63ff;
+ --mdma-color-loading-text: #8b82d4;
+ --mdma-color-table-header-bg: #f8f9fa;
+ --mdma-color-row-hover: #f8f9fa;
+ --mdma-color-blockquote-bg: #f8f8ff;
+ --mdma-color-chart-bg: rgba(255, 255, 255, 0.02);
+ --mdma-color-chart-border: rgba(108, 92, 231, 0.15);
+ --mdma-color-chart-label: #a29bfe;
+ --mdma-color-chart-axis: #636e72;
+ --mdma-color-chart-legend: #b2bec3;
+ --mdma-color-chart-tooltip-bg: #1a1a2e;
+ --mdma-color-unknown-bg: #fff3cd;
+ --mdma-color-unknown-border: #ffc107;
+ --mdma-color-unknown-text: #856404;
+
+ /* Radius / spacing / font-size scales (mirror the MdmaTheme numeric tokens) */
+ --mdma-radius-sm: 6px;
+ --mdma-radius-md: 10px;
+ --mdma-spacing-xs: 4px;
+ --mdma-spacing-sm: 8px;
+ --mdma-spacing-md: 12px;
+ --mdma-spacing-lg: 16px;
+ --mdma-font-size-small: 13px;
+ --mdma-font-size-body: 14px;
+ --mdma-font-size-label: 15px;
+ --mdma-font-size-title: 18px;
+}
+
+/* ===== Built-in dark palette ===== */
+.mdma-document[data-theme="dark"],
+.mdma-theme-root[data-theme="dark"] {
+ --mdma-color-background: #151a26;
+ --mdma-color-surface: #1c2333;
+ --mdma-color-text: #e5e7eb;
+ --mdma-color-text-muted: #9ca3af;
+ --mdma-color-border: #2a3140;
+ --mdma-color-primary: #8b7ff0;
+ --mdma-color-on-primary: #ffffff;
+ --mdma-color-secondary: #2a3140;
+ --mdma-color-on-secondary: #e5e7eb;
+ --mdma-color-danger: #ef4444;
+ --mdma-color-on-danger: #ffffff;
+ --mdma-color-info: #5dade2;
+ --mdma-color-info-bg: #16283a;
+ --mdma-color-warning: #f5b041;
+ --mdma-color-warning-bg: #2a2412;
+ --mdma-color-error: #ef4444;
+ --mdma-color-error-bg: #2a1717;
+ --mdma-color-success: #2ecc71;
+ --mdma-color-success-bg: #12251a;
+
+ --mdma-color-heading: #f3f4f6;
+ --mdma-color-text-subtle: #cbd0d8;
+ --mdma-color-border-subtle: #232a38;
+ --mdma-color-primary-hover: color-mix(in srgb, var(--mdma-color-primary) 80%, #ffffff);
+ --mdma-color-secondary-hover: color-mix(in srgb, var(--mdma-color-secondary) 78%, #ffffff);
+ --mdma-color-danger-hover: color-mix(in srgb, var(--mdma-color-danger) 80%, #ffffff);
+ --mdma-color-input-bg: #10141d;
+ --mdma-color-focus-ring: color-mix(in srgb, var(--mdma-color-primary) 35%, transparent);
+ --mdma-color-info-text: #aed6f1;
+ --mdma-color-warning-text: #f8c471;
+ --mdma-color-error-text: #f5b7b1;
+ --mdma-color-success-text: #a9dfbf;
+ --mdma-color-code-bg: #0d1017;
+ --mdma-color-code-text: #e0e0e0;
+ --mdma-color-inline-code-bg: #232a38;
+ --mdma-color-inline-code-text: #f19999;
+ --mdma-color-loading-bg: #1c2333;
+ --mdma-color-loading-border: #3a3466;
+ --mdma-color-loading-accent: #8b7ff0;
+ --mdma-color-loading-text: #a29bfe;
+ --mdma-color-table-header-bg: #1c2333;
+ --mdma-color-row-hover: #1c2333;
+ --mdma-color-blockquote-bg: #1c2333;
+ --mdma-color-chart-bg: rgba(255, 255, 255, 0.02);
+ --mdma-color-chart-border: rgba(162, 155, 254, 0.2);
+ --mdma-color-chart-label: #a29bfe;
+ --mdma-color-chart-axis: #9ca3af;
+ --mdma-color-chart-legend: #b2bec3;
+ --mdma-color-chart-tooltip-bg: #0d1017;
+ --mdma-color-unknown-bg: #2a2412;
+ --mdma-color-unknown-border: #b7950b;
+ --mdma-color-unknown-text: #f8c471;
+}
+
+/* theme="auto" — same dark palette, gated on the OS preference. */
+@media (prefers-color-scheme: dark) {
+ .mdma-document[data-theme="auto"],
+ .mdma-theme-root[data-theme="auto"] {
+ --mdma-color-background: #151a26;
+ --mdma-color-surface: #1c2333;
+ --mdma-color-text: #e5e7eb;
+ --mdma-color-text-muted: #9ca3af;
+ --mdma-color-border: #2a3140;
+ --mdma-color-primary: #8b7ff0;
+ --mdma-color-on-primary: #ffffff;
+ --mdma-color-secondary: #2a3140;
+ --mdma-color-on-secondary: #e5e7eb;
+ --mdma-color-danger: #ef4444;
+ --mdma-color-on-danger: #ffffff;
+ --mdma-color-info: #5dade2;
+ --mdma-color-info-bg: #16283a;
+ --mdma-color-warning: #f5b041;
+ --mdma-color-warning-bg: #2a2412;
+ --mdma-color-error: #ef4444;
+ --mdma-color-error-bg: #2a1717;
+ --mdma-color-success: #2ecc71;
+ --mdma-color-success-bg: #12251a;
+
+ --mdma-color-heading: #f3f4f6;
+ --mdma-color-text-subtle: #cbd0d8;
+ --mdma-color-border-subtle: #232a38;
+ --mdma-color-primary-hover: color-mix(in srgb, var(--mdma-color-primary) 80%, #ffffff);
+ --mdma-color-secondary-hover: color-mix(in srgb, var(--mdma-color-secondary) 78%, #ffffff);
+ --mdma-color-danger-hover: color-mix(in srgb, var(--mdma-color-danger) 80%, #ffffff);
+ --mdma-color-input-bg: #10141d;
+ --mdma-color-focus-ring: color-mix(in srgb, var(--mdma-color-primary) 35%, transparent);
+ --mdma-color-info-text: #aed6f1;
+ --mdma-color-warning-text: #f8c471;
+ --mdma-color-error-text: #f5b7b1;
+ --mdma-color-success-text: #a9dfbf;
+ --mdma-color-code-bg: #0d1017;
+ --mdma-color-code-text: #e0e0e0;
+ --mdma-color-inline-code-bg: #232a38;
+ --mdma-color-inline-code-text: #f19999;
+ --mdma-color-loading-bg: #1c2333;
+ --mdma-color-loading-border: #3a3466;
+ --mdma-color-loading-accent: #8b7ff0;
+ --mdma-color-loading-text: #a29bfe;
+ --mdma-color-table-header-bg: #1c2333;
+ --mdma-color-row-hover: #1c2333;
+ --mdma-color-blockquote-bg: #1c2333;
+ --mdma-color-chart-bg: rgba(255, 255, 255, 0.02);
+ --mdma-color-chart-border: rgba(162, 155, 254, 0.2);
+ --mdma-color-chart-label: #a29bfe;
+ --mdma-color-chart-axis: #9ca3af;
+ --mdma-color-chart-legend: #b2bec3;
+ --mdma-color-chart-tooltip-bg: #0d1017;
+ --mdma-color-unknown-bg: #2a2412;
+ --mdma-color-unknown-border: #b7950b;
+ --mdma-color-unknown-text: #f8c471;
+ }
+}
+
/* ===== MDMA Document ===== */
.mdma-document {
max-width: 720px;
margin: 0 auto;
+ color: var(--mdma-color-text);
+ font-size: var(--mdma-font-size-body);
}
/* ===== Entrance animation for all MDMA children ===== */
@@ -35,9 +241,9 @@
.mdma-block-loading {
position: relative;
overflow: hidden;
- background: #fff;
- border: 1px dashed #c4bfff;
- border-radius: 10px;
+ background: var(--mdma-color-loading-bg);
+ border: 1px dashed var(--mdma-color-loading-border);
+ border-radius: var(--mdma-radius-md);
padding: 28px 24px;
margin-bottom: 20px;
min-height: 80px;
@@ -75,7 +281,7 @@
display: flex;
align-items: center;
gap: 10px;
- color: #8b82d4;
+ color: var(--mdma-color-loading-text);
font-size: 13px;
}
@@ -83,7 +289,7 @@
width: 18px;
height: 18px;
border: 2px solid rgba(108, 99, 255, 0.2);
- border-top-color: #6c63ff;
+ border-top-color: var(--mdma-color-loading-accent);
border-radius: 50%;
animation: mdma-spin 0.8s linear infinite;
}
@@ -136,7 +342,7 @@
margin: 16px 0 8px;
font-weight: 600;
line-height: 1.3;
- color: #1a1a2e;
+ color: var(--mdma-color-heading);
}
.mdma-markdown-content h1 {
@@ -178,9 +384,9 @@
.mdma-markdown-content blockquote {
margin: 0 0 10px;
padding: 8px 16px;
- border-left: 3px solid #6c63ff;
- background: #f8f8ff;
- color: #444;
+ border-left: 3px solid var(--mdma-color-primary);
+ background: var(--mdma-color-blockquote-bg);
+ color: var(--mdma-color-text-subtle);
}
.mdma-markdown-content blockquote p:last-child {
@@ -190,9 +396,9 @@
.mdma-markdown-content pre.mdast-code-block {
margin: 0 0 10px;
padding: 12px 16px;
- background: #1e1e2e;
- color: #e0e0e0;
- border-radius: 6px;
+ background: var(--mdma-color-code-bg);
+ color: var(--mdma-color-code-text);
+ border-radius: var(--mdma-radius-sm);
overflow-x: auto;
font-size: 13px;
line-height: 1.5;
@@ -206,21 +412,21 @@
}
.mdma-markdown-content code.mdast-inline-code {
- background: #f0f0f5;
+ background: var(--mdma-color-inline-code-bg);
padding: 2px 6px;
border-radius: 3px;
font-size: 0.9em;
- color: #c0392b;
+ color: var(--mdma-color-inline-code-text);
}
.mdma-markdown-content hr {
margin: 16px 0;
border: none;
- border-top: 1px solid #ddd;
+ border-top: 1px solid var(--mdma-color-border);
}
.mdma-markdown-content a {
- color: #6c63ff;
+ color: var(--mdma-color-primary);
text-decoration: none;
}
@@ -231,7 +437,7 @@
.mdma-markdown-content img {
max-width: 100%;
height: auto;
- border-radius: 6px;
+ border-radius: var(--mdma-radius-sm);
}
.mdma-markdown-content table.mdast-table {
@@ -244,17 +450,17 @@
.mdma-markdown-content table.mdast-table th,
.mdma-markdown-content table.mdast-table td {
padding: 8px 12px;
- border: 1px solid #e0e0e0;
+ border: 1px solid var(--mdma-color-border);
text-align: left;
}
.mdma-markdown-content table.mdast-table th {
- background: #f5f6fa;
+ background: var(--mdma-color-table-header-bg);
font-weight: 600;
}
.mdma-markdown-content table.mdast-table tbody tr:hover {
- background: #f9f9fc;
+ background: var(--mdma-color-row-hover);
}
.mdma-markdown-content strong {
@@ -267,35 +473,35 @@
.mdma-markdown-content del {
text-decoration: line-through;
- color: #888;
+ color: var(--mdma-color-text-muted);
}
/* ===== Form ===== */
.mdma-form {
- background: #fff;
- border: 1px solid #e0e0e0;
- border-radius: 10px;
+ background: var(--mdma-color-background);
+ border: 1px solid var(--mdma-color-border);
+ border-radius: var(--mdma-radius-md);
padding: 24px;
margin-bottom: 20px;
}
.mdma-form-label {
- font-size: 18px;
+ font-size: var(--mdma-font-size-title);
font-weight: 600;
margin-bottom: 16px;
- color: #1a1a2e;
+ color: var(--mdma-color-heading);
}
.mdma-form-field {
- margin-bottom: 16px;
+ margin-bottom: var(--mdma-spacing-lg);
}
.mdma-form-field label {
display: block;
font-size: 13px;
font-weight: 600;
- color: #444;
- margin-bottom: 4px;
+ color: var(--mdma-color-text-subtle);
+ margin-bottom: var(--mdma-spacing-xs);
}
.mdma-form-field input[type="text"],
@@ -308,10 +514,10 @@
width: 100%;
padding: 8px 12px;
font-size: 14px;
- border: 1px solid #d1d5db;
- border-radius: 6px;
- background: #fafafa;
- color: #1a1a2e;
+ border: 1px solid var(--mdma-color-border);
+ border-radius: var(--mdma-radius-sm);
+ background: var(--mdma-color-input-bg);
+ color: var(--mdma-color-text);
transition: border-color 0.15s, box-shadow 0.15s;
outline: none;
}
@@ -319,8 +525,8 @@
.mdma-form-field input:focus,
.mdma-form-field select:focus,
.mdma-form-field textarea:focus {
- border-color: #6c5ce7;
- box-shadow: 0 0 0 3px rgba(108, 92, 231, 0.15);
+ border-color: var(--mdma-color-primary);
+ box-shadow: 0 0 0 3px var(--mdma-color-focus-ring);
}
.mdma-form-field textarea {
@@ -331,7 +537,7 @@
.mdma-form-field input[type="checkbox"] {
width: auto;
margin-right: 8px;
- accent-color: #6c5ce7;
+ accent-color: var(--mdma-color-primary);
}
.mdma-form-submit {
@@ -339,16 +545,16 @@
padding: 8px 20px;
font-size: 14px;
font-weight: 600;
- color: #fff;
- background: #6c5ce7;
+ color: var(--mdma-color-on-primary);
+ background: var(--mdma-color-primary);
border: none;
- border-radius: 6px;
+ border-radius: var(--mdma-radius-sm);
cursor: pointer;
transition: background 0.15s;
}
.mdma-form-submit:hover {
- background: #5a4bd1;
+ background: var(--mdma-color-primary-hover);
}
/* ===== Button ===== */
@@ -358,7 +564,7 @@
font-size: 14px;
font-weight: 600;
border: none;
- border-radius: 6px;
+ border-radius: var(--mdma-radius-sm);
cursor: pointer;
transition: background 0.15s, transform 0.1s;
margin-bottom: 20px;
@@ -369,46 +575,46 @@
}
.mdma-button--primary {
- background: #6c5ce7;
- color: #fff;
+ background: var(--mdma-color-primary);
+ color: var(--mdma-color-on-primary);
}
.mdma-button--primary:hover {
- background: #5a4bd1;
+ background: var(--mdma-color-primary-hover);
}
.mdma-button--secondary {
- background: #dfe6e9;
- color: #2d3436;
+ background: var(--mdma-color-secondary);
+ color: var(--mdma-color-on-secondary);
}
.mdma-button--secondary:hover {
- background: #c8d6db;
+ background: var(--mdma-color-secondary-hover);
}
.mdma-button--danger {
- background: #e74c3c;
- color: #fff;
+ background: var(--mdma-color-danger);
+ color: var(--mdma-color-on-danger);
}
.mdma-button--danger:hover {
- background: #c0392b;
+ background: var(--mdma-color-danger-hover);
}
/* ===== Tasklist ===== */
.mdma-tasklist {
- background: #fff;
- border: 1px solid #e0e0e0;
- border-radius: 10px;
+ background: var(--mdma-color-background);
+ border: 1px solid var(--mdma-color-border);
+ border-radius: var(--mdma-radius-md);
padding: 24px;
margin-bottom: 20px;
}
.mdma-tasklist-label {
- font-size: 18px;
+ font-size: var(--mdma-font-size-title);
font-weight: 600;
margin-bottom: 12px;
- color: #1a1a2e;
+ color: var(--mdma-color-heading);
}
.mdma-tasklist-items {
@@ -417,7 +623,7 @@
.mdma-tasklist-item {
padding: 8px 0;
- border-bottom: 1px solid #f0f0f0;
+ border-bottom: 1px solid var(--mdma-color-border-subtle);
}
.mdma-tasklist-item:last-child {
@@ -430,35 +636,35 @@
gap: 8px;
cursor: pointer;
font-size: 14px;
- color: #333;
+ color: var(--mdma-color-text);
}
.mdma-tasklist-item input[type="checkbox"] {
- accent-color: #6c5ce7;
+ accent-color: var(--mdma-color-primary);
width: 16px;
height: 16px;
}
.mdma-tasklist-item input[type="checkbox"]:checked + span {
text-decoration: line-through;
- color: #999;
+ color: var(--mdma-color-text-muted);
}
/* ===== Table ===== */
.mdma-table {
- background: #fff;
- border: 1px solid #e0e0e0;
- border-radius: 10px;
+ background: var(--mdma-color-background);
+ border: 1px solid var(--mdma-color-border);
+ border-radius: var(--mdma-radius-md);
padding: 24px;
margin-bottom: 20px;
overflow-x: auto;
}
.mdma-table-label {
- font-size: 18px;
+ font-size: var(--mdma-font-size-title);
font-weight: 600;
margin-bottom: 12px;
- color: #1a1a2e;
+ color: var(--mdma-color-heading);
}
.mdma-table table {
@@ -473,32 +679,32 @@
font-weight: 600;
text-transform: uppercase;
letter-spacing: 0.5px;
- color: #666;
- background: #f8f9fa;
- border-bottom: 2px solid #e0e0e0;
+ color: var(--mdma-color-text-muted);
+ background: var(--mdma-color-table-header-bg);
+ border-bottom: 2px solid var(--mdma-color-border);
}
.mdma-table td {
padding: 10px 12px;
font-size: 14px;
- border-bottom: 1px solid #f0f0f0;
- color: #333;
+ border-bottom: 1px solid var(--mdma-color-border-subtle);
+ color: var(--mdma-color-text);
}
.mdma-table tr:hover td {
- background: #f8f9fa;
+ background: var(--mdma-color-row-hover);
}
.mdma-table-empty {
text-align: center;
padding: 20px;
- color: #999;
+ color: var(--mdma-color-text-muted);
font-style: italic;
}
/* ===== Callout ===== */
.mdma-callout {
- border-radius: 10px;
+ border-radius: var(--mdma-radius-md);
padding: 16px 20px;
margin-bottom: 20px;
position: relative;
@@ -506,27 +712,27 @@
}
.mdma-callout--info {
- background: #ebf5fb;
- border-left-color: #3498db;
- color: #1a5276;
+ background: var(--mdma-color-info-bg);
+ border-left-color: var(--mdma-color-info);
+ color: var(--mdma-color-info-text);
}
.mdma-callout--warning {
- background: #fef9e7;
- border-left-color: #f39c12;
- color: #7d6608;
+ background: var(--mdma-color-warning-bg);
+ border-left-color: var(--mdma-color-warning);
+ color: var(--mdma-color-warning-text);
}
.mdma-callout--error {
- background: #fdedec;
- border-left-color: #e74c3c;
- color: #78281f;
+ background: var(--mdma-color-error-bg);
+ border-left-color: var(--mdma-color-error);
+ color: var(--mdma-color-error-text);
}
.mdma-callout--success {
- background: #eafaf1;
- border-left-color: #27ae60;
- color: #1e8449;
+ background: var(--mdma-color-success-bg);
+ border-left-color: var(--mdma-color-success);
+ color: var(--mdma-color-success-text);
}
.mdma-callout-title {
@@ -559,43 +765,43 @@
/* ===== Approval Gate ===== */
.mdma-approval-gate {
- background: #fff;
- border: 2px solid #e0e0e0;
- border-radius: 10px;
+ background: var(--mdma-color-background);
+ border: 2px solid var(--mdma-color-border);
+ border-radius: var(--mdma-radius-md);
padding: 24px;
margin-bottom: 20px;
}
.mdma-approval-gate--pending {
- border-color: #f39c12;
+ border-color: var(--mdma-color-warning);
}
.mdma-approval-gate--approved {
- border-color: #27ae60;
- background: #f0faf4;
+ border-color: var(--mdma-color-success);
+ background: var(--mdma-color-success-bg);
}
.mdma-approval-gate--denied {
- border-color: #e74c3c;
- background: #fdf2f2;
+ border-color: var(--mdma-color-error);
+ background: var(--mdma-color-error-bg);
}
.mdma-approval-gate-title {
font-size: 16px;
font-weight: 600;
margin-bottom: 8px;
- color: #1a1a2e;
+ color: var(--mdma-color-heading);
}
.mdma-approval-gate-description {
font-size: 14px;
- color: #555;
+ color: var(--mdma-color-text-subtle);
margin-bottom: 12px;
}
.mdma-approval-gate-status {
font-size: 13px;
- color: #666;
+ color: var(--mdma-color-text-muted);
margin-bottom: 16px;
}
@@ -605,13 +811,13 @@
}
.mdma-approval-gate--pending .mdma-approval-gate-status strong {
- color: #f39c12;
+ color: var(--mdma-color-warning);
}
.mdma-approval-gate--approved .mdma-approval-gate-status strong {
- color: #27ae60;
+ color: var(--mdma-color-success);
}
.mdma-approval-gate--denied .mdma-approval-gate-status strong {
- color: #e74c3c;
+ color: var(--mdma-color-error);
}
.mdma-approval-gate-actions {
@@ -625,8 +831,8 @@
align-items: center;
gap: 10px;
padding: 12px 16px;
- background: #fff;
- border: 1px solid #e0e0e0;
+ background: var(--mdma-color-background);
+ border: 1px solid var(--mdma-color-border);
border-radius: 8px;
margin-bottom: 20px;
font-size: 13px;
@@ -634,7 +840,7 @@
.mdma-webhook-label {
font-weight: 600;
- color: #333;
+ color: var(--mdma-color-text);
}
.mdma-webhook-status {
@@ -645,8 +851,8 @@
}
.mdma-webhook-status--idle {
- background: #f0f0f0;
- color: #666;
+ background: var(--mdma-color-surface);
+ color: var(--mdma-color-text-muted);
}
.mdma-webhook-status--executing {
background: #ffeaa7;
@@ -664,17 +870,17 @@
/* ===== Unknown Component ===== */
.mdma-unknown-component {
padding: 12px 16px;
- background: #fff3cd;
- border: 1px solid #ffc107;
+ background: var(--mdma-color-unknown-bg);
+ border: 1px solid var(--mdma-color-unknown-border);
border-radius: 8px;
- color: #856404;
+ color: var(--mdma-color-unknown-text);
font-size: 13px;
margin-bottom: 20px;
}
.mdma-chart {
- background: rgba(255, 255, 255, 0.02);
- border: 1px solid rgba(108, 92, 231, 0.15);
+ background: var(--mdma-color-chart-bg);
+ border: 1px solid var(--mdma-color-chart-border);
border-radius: 14px;
padding: 18px;
margin-bottom: 20px;
@@ -684,7 +890,7 @@
.mdma-chart-label {
font-size: 14px;
font-weight: 700;
- color: #a29bfe;
+ color: var(--mdma-color-chart-label);
margin-bottom: 12px;
}
@@ -699,32 +905,32 @@
display: flex;
align-items: center;
justify-content: center;
- color: #636e72;
+ color: var(--mdma-color-text-muted);
font-size: 13px;
font-style: italic;
}
/* recharts overrides for MDMA theme */
.mdma-chart .recharts-text {
- fill: #636e72;
+ fill: var(--mdma-color-chart-axis);
}
.mdma-chart .recharts-legend-item-text {
- color: #b2bec3 !important;
+ color: var(--mdma-color-chart-legend) !important;
font-size: 12px;
}
.mdma-chart .recharts-default-tooltip {
- background: #1a1a2e !important;
+ background: var(--mdma-color-chart-tooltip-bg) !important;
border: 1px solid rgba(162, 155, 254, 0.3) !important;
border-radius: 8px !important;
}
/* ─── Thinking Component (basic) ─────────────────────────────────────────── */
.mdma-thinking {
- background: #fff;
- border: 1px solid #e0e0e0;
- border-radius: 10px;
+ background: var(--mdma-color-background);
+ border: 1px solid var(--mdma-color-border);
+ border-radius: var(--mdma-radius-md);
margin: 16px 0;
overflow: hidden;
}
@@ -737,7 +943,7 @@
cursor: pointer;
font-size: 14px;
font-weight: 600;
- color: #555;
+ color: var(--mdma-color-text-subtle);
user-select: none;
list-style: none;
}
@@ -751,7 +957,7 @@
font-size: 10px;
margin-left: auto;
transition: transform 0.2s;
- color: #999;
+ color: var(--mdma-color-text-muted);
}
.mdma-thinking[open] .mdma-thinking-summary::after {
@@ -763,7 +969,7 @@
width: 8px;
height: 8px;
border-radius: 50%;
- background: #6c5ce7;
+ background: var(--mdma-color-primary);
animation: mdma-thinking-pulse 1.5s ease-in-out infinite;
}
@@ -787,8 +993,8 @@
padding: 0 16px 16px;
font-size: 13px;
line-height: 1.6;
- color: #555;
+ color: var(--mdma-color-text-subtle);
white-space: pre-wrap;
- border-top: 1px solid #f0f0f0;
+ border-top: 1px solid var(--mdma-color-border-subtle);
padding-top: 12px;
}
diff --git a/packages/renderer-react/tests/theme.test.ts b/packages/renderer-react/tests/theme.test.ts
new file mode 100644
index 0000000..67ab0e8
--- /dev/null
+++ b/packages/renderer-react/tests/theme.test.ts
@@ -0,0 +1,84 @@
+import { describe, it, expect } from 'vitest';
+import {
+ lightTheme,
+ darkTheme,
+ themeToCssVars,
+ resolveThemeProps,
+ type MdmaTheme,
+} from '../src/theme/MdmaThemeProvider.js';
+
+// Structural parity guard: the light and dark palettes must expose exactly the
+// same token shape so a class styled against one never reads an undefined
+// variable under the other.
+function keyShape(theme: MdmaTheme) {
+ return {
+ colors: Object.keys(theme.colors).sort(),
+ spacing: Object.keys(theme.spacing).sort(),
+ radius: Object.keys(theme.radius).sort(),
+ fontSize: Object.keys(theme.fontSize).sort(),
+ };
+}
+
+describe('MdmaTheme tokens', () => {
+ it('light and dark themes share the same token shape', () => {
+ expect(keyShape(lightTheme)).toEqual(keyShape(darkTheme));
+ });
+
+ it('every color token is a defined string in both themes', () => {
+ for (const theme of [lightTheme, darkTheme]) {
+ for (const value of Object.values(theme.colors)) {
+ expect(typeof value).toBe('string');
+ expect(value.length).toBeGreaterThan(0);
+ }
+ }
+ });
+});
+
+describe('themeToCssVars', () => {
+ it('maps camelCase tokens to kebab-case --mdma-* variables', () => {
+ const vars = themeToCssVars(lightTheme) as Record;
+ expect(vars['--mdma-color-primary']).toBe(lightTheme.colors.primary);
+ expect(vars['--mdma-color-on-primary']).toBe(lightTheme.colors.onPrimary);
+ expect(vars['--mdma-color-text-muted']).toBe(lightTheme.colors.textMuted);
+ expect(vars['--mdma-color-info-bg']).toBe(lightTheme.colors.infoBg);
+ });
+
+ it('emits numeric scales with a px unit', () => {
+ const vars = themeToCssVars(lightTheme) as Record;
+ expect(vars['--mdma-radius-md']).toBe(`${lightTheme.radius.md}px`);
+ expect(vars['--mdma-spacing-lg']).toBe(`${lightTheme.spacing.lg}px`);
+ expect(vars['--mdma-font-size-title']).toBe(`${lightTheme.fontSize.title}px`);
+ });
+});
+
+describe('resolveThemeProps', () => {
+ it('returns the light defaults with no data-theme when unset', () => {
+ const r = resolveThemeProps(undefined);
+ expect(r.dataTheme).toBeUndefined();
+ expect(r.style).toBeUndefined();
+ expect(r.theme).toBe(lightTheme);
+ });
+
+ it('applies built-in palettes via data-theme', () => {
+ expect(resolveThemeProps('light')).toMatchObject({ dataTheme: 'light', theme: lightTheme });
+ expect(resolveThemeProps('dark')).toMatchObject({ dataTheme: 'dark', theme: darkTheme });
+ expect(resolveThemeProps('auto').dataTheme).toBe('auto');
+ });
+
+ it('writes a custom theme as inline CSS variables', () => {
+ const custom: MdmaTheme = {
+ ...lightTheme,
+ colors: { ...lightTheme.colors, primary: '#ff0000' },
+ };
+ const r = resolveThemeProps(custom);
+ expect((r.style as Record)['--mdma-color-primary']).toBe('#ff0000');
+ expect(r.theme).toBe(custom);
+ });
+
+ it('picks the light/dark base for a custom theme by its background, so internal vars match', () => {
+ const lightCustom: MdmaTheme = { ...lightTheme };
+ const darkCustom: MdmaTheme = { ...darkTheme };
+ expect(resolveThemeProps(lightCustom).dataTheme).toBe('light');
+ expect(resolveThemeProps(darkCustom).dataTheme).toBe('dark');
+ });
+});
diff --git a/packages/runtime/package.json b/packages/runtime/package.json
index a34d785..4792e4c 100644
--- a/packages/runtime/package.json
+++ b/packages/runtime/package.json
@@ -36,9 +36,7 @@
"typescript": "^5.7.0",
"vitest": "^3.2.0"
},
- "files": [
- "dist"
- ],
+ "files": ["dist"],
"publishConfig": {
"access": "public"
},
diff --git a/packages/spec/package.json b/packages/spec/package.json
index f72233a..89822ea 100644
--- a/packages/spec/package.json
+++ b/packages/spec/package.json
@@ -1,16 +1,7 @@
{
"name": "@mobile-reality/mdma-spec",
"version": "0.3.1",
- "keywords": [
- "mdma",
- "markdown",
- "ai",
- "llm",
- "interactive-document",
- "schema",
- "zod",
- "types"
- ],
+ "keywords": ["mdma", "markdown", "ai", "llm", "interactive-document", "schema", "zod", "types"],
"type": "module",
"main": "./dist/index.js",
"module": "./dist/index.js",
@@ -47,9 +38,7 @@
"typescript": "^5.7.0",
"vitest": "^3.2.0"
},
- "files": [
- "dist"
- ],
+ "files": ["dist"],
"publishConfig": {
"access": "public"
},
diff --git a/packages/validator/package.json b/packages/validator/package.json
index 6703fd4..7eb9bcc 100644
--- a/packages/validator/package.json
+++ b/packages/validator/package.json
@@ -35,9 +35,7 @@
"vitest": "^3.2.0",
"zod": "^3.24.0"
},
- "files": [
- "dist"
- ],
+ "files": ["dist"],
"publishConfig": {
"access": "public"
},