Merge pull request 'development' (#184) from development into main

Reviewed-on: #184
This commit was merged in pull request #184.
This commit is contained in:
2026-01-26 18:59:23 +00:00
40 changed files with 7224 additions and 1 deletions

View File

@@ -165,6 +165,22 @@
"category": "data",
"tags": ["pandas", "postgresql", "postgis", "dbt", "data-engineering", "etl"],
"license": "MIT"
},
{
"name": "viz-platform",
"version": "1.0.0",
"description": "Visualization tools with Dash Mantine Components validation, Plotly charts, and theming",
"source": "./plugins/viz-platform",
"author": {
"name": "Leo Miranda",
"email": "leobmiranda@gmail.com"
},
"homepage": "https://gitea.hotserv.cloud/personal-projects/leo-claude-mktplace/src/branch/main/plugins/viz-platform/README.md",
"repository": "https://gitea.hotserv.cloud/personal-projects/leo-claude-mktplace.git",
"mcpServers": ["./.mcp.json"],
"category": "visualization",
"tags": ["dash", "plotly", "mantine", "charts", "dashboards", "theming", "dmc"],
"license": "MIT"
}
]
}

View File

@@ -6,6 +6,39 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
## [Unreleased]
### Added
#### Sprint 1: viz-platform Plugin ✅ Completed
- **viz-platform** v1.0.0 - Visualization tools with Dash Mantine Components validation and theming
- **DMC Tools** (3 tools): `list_components`, `get_component_props`, `validate_component`
- Version-locked component registry prevents Claude from hallucinating invalid props
- Static JSON registry approach for fast, deterministic validation
- **Chart Tools** (2 tools): `chart_create`, `chart_configure_interaction`
- Plotly-based visualization with theme token support
- **Layout Tools** (3 tools): `layout_create`, `layout_add_filter`, `layout_set_grid`
- Dashboard composition with responsive grid system
- **Theme Tools** (4 tools): `theme_create`, `theme_extend`, `theme_validate`, `theme_export_css`
- Design token-based theming system
- Dual storage: user-level (`~/.config/claude/themes/`) and project-level
- **Page Tools** (3 tools): `page_create`, `page_add_navbar`, `page_set_auth`
- Multi-page Dash app structure generation
- **Commands**: `/chart`, `/dashboard`, `/theme`, `/theme-new`, `/theme-css`, `/component`, `/initial-setup`
- **Agents**: `theme-setup`, `layout-builder`, `component-check`
- **SessionStart Hook**: DMC version check (non-blocking)
- **Tests**: 94 tests passing
- config.py: 82% coverage
- component_registry.py: 92% coverage
- dmc_tools.py: 88% coverage
- chart_tools.py: 68% coverage
- theme_tools.py: 99% coverage
**Sprint Completed:**
- Milestone: Sprint 1 - viz-platform Plugin (closed 2026-01-26)
- Issues: #170-#182 (13/13 closed)
- Wiki: [Sprint-1-viz-platform-Implementation-Plan](https://gitea.hotserv.cloud/personal-projects/leo-claude-mktplace/wiki/Sprint-1-viz-platform-Implementation-Plan)
- Lessons: [sprint-1---viz-platform-plugin-implementation](https://gitea.hotserv.cloud/personal-projects/leo-claude-mktplace/wiki/lessons/sprints/sprint-1---viz-platform-plugin-implementation)
- Reference: `docs/changes/CHANGE_V04_0_0_PROPOSAL_ORIGINAL.md` (Phases 4-5)
---
## [4.1.0] - 2026-01-26

View File

@@ -66,6 +66,7 @@ A plugin marketplace for Claude Code containing:
| `claude-config-maintainer` | CLAUDE.md optimization and maintenance | 1.0.0 |
| `cmdb-assistant` | NetBox CMDB integration for infrastructure management | 1.0.0 |
| `data-platform` | pandas, PostgreSQL, and dbt integration for data engineering | 1.0.0 |
| `viz-platform` | DMC validation, Plotly charts, and theming for dashboards | 1.0.0 |
| `project-hygiene` | Post-task cleanup automation via hooks | 0.1.0 |
## Quick Start
@@ -91,6 +92,7 @@ A plugin marketplace for Claude Code containing:
| **Security** | `/security-scan`, `/refactor`, `/refactor-dry` |
| **Config** | `/config-analyze`, `/config-optimize` |
| **Data** | `/ingest`, `/profile`, `/schema`, `/explain`, `/lineage`, `/run` |
| **Visualization** | `/component`, `/chart`, `/dashboard`, `/theme`, `/theme-new`, `/theme-css` |
| **Debug** | `/debug-report`, `/debug-review` |
## Repository Structure
@@ -101,7 +103,8 @@ leo-claude-mktplace/
│ └── marketplace.json # Marketplace manifest
├── mcp-servers/ # SHARED MCP servers (v3.0.0+)
│ ├── gitea/ # Gitea MCP (issues, PRs, wiki)
── netbox/ # NetBox MCP (CMDB)
── netbox/ # NetBox MCP (CMDB)
│ └── viz-platform/ # DMC validation, charts, themes
├── plugins/
│ ├── projman/ # Sprint management
│ │ ├── .claude-plugin/plugin.json
@@ -133,6 +136,13 @@ leo-claude-mktplace/
│ │ ├── commands/ # 7 commands
│ │ ├── hooks/ # SessionStart PostgreSQL check
│ │ └── agents/ # 2 agents
│ ├── viz-platform/ # Visualization (NEW v4.0.0)
│ │ ├── .claude-plugin/plugin.json
│ │ ├── .mcp.json
│ │ ├── mcp-servers/ # viz-platform MCP
│ │ ├── commands/ # 7 commands
│ │ ├── hooks/ # SessionStart DMC check
│ │ └── agents/ # 3 agents
│ ├── doc-guardian/ # Documentation drift detection
│ ├── code-sentinel/ # Security scanning & refactoring
│ ├── claude-config-maintainer/

View File

@@ -0,0 +1,7 @@
"""
viz-platform MCP Server package.
Provides Dash Mantine Components validation and visualization tools to Claude Code.
"""
__version__ = "1.0.0"

View File

@@ -0,0 +1,397 @@
"""
Chart creation tools using Plotly.
Provides tools for creating data visualizations with automatic theme integration.
"""
import logging
from typing import Dict, List, Optional, Any, Union
logger = logging.getLogger(__name__)
# Default color palette based on Mantine theme
DEFAULT_COLORS = [
"#228be6", # blue
"#40c057", # green
"#fa5252", # red
"#fab005", # yellow
"#7950f2", # violet
"#fd7e14", # orange
"#20c997", # teal
"#f783ac", # pink
"#868e96", # gray
"#15aabf", # cyan
]
class ChartTools:
"""
Plotly-based chart creation tools.
Creates charts that integrate with DMC theming system.
"""
def __init__(self, theme_store=None):
"""
Initialize chart tools.
Args:
theme_store: Optional ThemeStore for theme token resolution
"""
self.theme_store = theme_store
self._active_theme = None
def set_theme(self, theme: Dict[str, Any]) -> None:
"""Set the active theme for chart styling."""
self._active_theme = theme
def _get_color_palette(self) -> List[str]:
"""Get color palette from theme or defaults."""
if self._active_theme and 'colors' in self._active_theme:
colors = self._active_theme['colors']
# Extract primary colors from theme
palette = []
for key in ['primary', 'secondary', 'success', 'warning', 'error']:
if key in colors:
palette.append(colors[key])
if palette:
return palette + DEFAULT_COLORS[len(palette):]
return DEFAULT_COLORS
def _resolve_color(self, color: Optional[str]) -> str:
"""Resolve a color token to actual color value."""
if not color:
return self._get_color_palette()[0]
# Check if it's a theme token
if self._active_theme and 'colors' in self._active_theme:
colors = self._active_theme['colors']
if color in colors:
return colors[color]
# Check if it's already a valid color
if color.startswith('#') or color.startswith('rgb'):
return color
# Map common color names to palette
color_map = {
'blue': DEFAULT_COLORS[0],
'green': DEFAULT_COLORS[1],
'red': DEFAULT_COLORS[2],
'yellow': DEFAULT_COLORS[3],
'violet': DEFAULT_COLORS[4],
'orange': DEFAULT_COLORS[5],
'teal': DEFAULT_COLORS[6],
'pink': DEFAULT_COLORS[7],
'gray': DEFAULT_COLORS[8],
'cyan': DEFAULT_COLORS[9],
}
return color_map.get(color, color)
async def chart_create(
self,
chart_type: str,
data: Dict[str, Any],
options: Optional[Dict[str, Any]] = None
) -> Dict[str, Any]:
"""
Create a Plotly chart.
Args:
chart_type: Type of chart (line, bar, scatter, pie, heatmap, histogram, area)
data: Data specification with x, y values or labels/values for pie
options: Optional chart options (title, color, layout settings)
Returns:
Dict with:
- figure: Plotly figure JSON
- chart_type: Type of chart created
- error: Error message if creation failed
"""
options = options or {}
# Validate chart type
valid_types = ['line', 'bar', 'scatter', 'pie', 'heatmap', 'histogram', 'area']
if chart_type not in valid_types:
return {
"error": f"Invalid chart_type '{chart_type}'. Must be one of: {valid_types}",
"chart_type": chart_type,
"figure": None
}
try:
# Build trace based on chart type
trace = self._build_trace(chart_type, data, options)
if 'error' in trace:
return trace
# Build layout
layout = self._build_layout(options)
# Create figure structure
figure = {
"data": [trace],
"layout": layout
}
return {
"figure": figure,
"chart_type": chart_type,
"trace_count": 1
}
except Exception as e:
logger.error(f"Chart creation failed: {e}")
return {
"error": str(e),
"chart_type": chart_type,
"figure": None
}
def _build_trace(
self,
chart_type: str,
data: Dict[str, Any],
options: Dict[str, Any]
) -> Dict[str, Any]:
"""Build Plotly trace for the chart type."""
color = self._resolve_color(options.get('color'))
palette = self._get_color_palette()
# Common trace properties
trace: Dict[str, Any] = {}
if chart_type == 'line':
trace = {
"type": "scatter",
"mode": "lines+markers",
"x": data.get('x', []),
"y": data.get('y', []),
"line": {"color": color},
"marker": {"color": color}
}
if 'name' in data:
trace['name'] = data['name']
elif chart_type == 'bar':
trace = {
"type": "bar",
"x": data.get('x', []),
"y": data.get('y', []),
"marker": {"color": color}
}
if options.get('horizontal'):
trace['orientation'] = 'h'
trace['x'], trace['y'] = trace['y'], trace['x']
if 'name' in data:
trace['name'] = data['name']
elif chart_type == 'scatter':
trace = {
"type": "scatter",
"mode": "markers",
"x": data.get('x', []),
"y": data.get('y', []),
"marker": {
"color": color,
"size": options.get('marker_size', 10)
}
}
if 'size' in data:
trace['marker']['size'] = data['size']
if 'name' in data:
trace['name'] = data['name']
elif chart_type == 'pie':
labels = data.get('labels', data.get('x', []))
values = data.get('values', data.get('y', []))
trace = {
"type": "pie",
"labels": labels,
"values": values,
"marker": {"colors": palette[:len(labels)]}
}
if options.get('donut'):
trace['hole'] = options.get('hole', 0.4)
elif chart_type == 'heatmap':
trace = {
"type": "heatmap",
"z": data.get('z', data.get('values', [])),
"x": data.get('x', []),
"y": data.get('y', []),
"colorscale": options.get('colorscale', 'Blues')
}
elif chart_type == 'histogram':
trace = {
"type": "histogram",
"x": data.get('x', data.get('values', [])),
"marker": {"color": color}
}
if 'nbins' in options:
trace['nbinsx'] = options['nbins']
elif chart_type == 'area':
trace = {
"type": "scatter",
"mode": "lines",
"x": data.get('x', []),
"y": data.get('y', []),
"fill": "tozeroy",
"line": {"color": color},
"fillcolor": color.replace(')', ', 0.3)').replace('rgb', 'rgba') if color.startswith('rgb') else color + '4D'
}
if 'name' in data:
trace['name'] = data['name']
else:
return {"error": f"Unsupported chart type: {chart_type}"}
return trace
def _build_layout(self, options: Dict[str, Any]) -> Dict[str, Any]:
"""Build Plotly layout from options."""
layout: Dict[str, Any] = {
"autosize": True,
"margin": {"l": 50, "r": 30, "t": 50, "b": 50}
}
# Title
if 'title' in options:
layout['title'] = {
"text": options['title'],
"x": 0.5,
"xanchor": "center"
}
# Axis labels
if 'x_label' in options:
layout['xaxis'] = layout.get('xaxis', {})
layout['xaxis']['title'] = options['x_label']
if 'y_label' in options:
layout['yaxis'] = layout.get('yaxis', {})
layout['yaxis']['title'] = options['y_label']
# Theme-based styling
if self._active_theme:
colors = self._active_theme.get('colors', {})
bg = colors.get('background', {})
if isinstance(bg, dict):
layout['paper_bgcolor'] = bg.get('base', '#ffffff')
layout['plot_bgcolor'] = bg.get('subtle', '#f8f9fa')
elif isinstance(bg, str):
layout['paper_bgcolor'] = bg
layout['plot_bgcolor'] = bg
text_color = colors.get('text', {})
if isinstance(text_color, dict):
layout['font'] = {'color': text_color.get('primary', '#212529')}
elif isinstance(text_color, str):
layout['font'] = {'color': text_color}
# Additional layout options
if 'showlegend' in options:
layout['showlegend'] = options['showlegend']
if 'height' in options:
layout['height'] = options['height']
if 'width' in options:
layout['width'] = options['width']
return layout
async def chart_configure_interaction(
self,
figure: Dict[str, Any],
interactions: Dict[str, Any]
) -> Dict[str, Any]:
"""
Configure interactions for a chart.
Args:
figure: Plotly figure JSON to modify
interactions: Interaction configuration:
- hover_template: Custom hover text template
- click_data: Enable click data capture
- selection: Enable selection (box, lasso)
- zoom: Enable/disable zoom
Returns:
Dict with:
- figure: Updated figure JSON
- interactions_added: List of interactions configured
- error: Error message if configuration failed
"""
if not figure or 'data' not in figure:
return {
"error": "Invalid figure: must contain 'data' key",
"figure": figure,
"interactions_added": []
}
try:
interactions_added = []
# Process each trace
for i, trace in enumerate(figure['data']):
# Hover template
if 'hover_template' in interactions:
trace['hovertemplate'] = interactions['hover_template']
if i == 0:
interactions_added.append('hover_template')
# Custom hover info
if 'hover_info' in interactions:
trace['hoverinfo'] = interactions['hover_info']
if i == 0:
interactions_added.append('hover_info')
# Layout-level interactions
layout = figure.get('layout', {})
# Click data (Dash callback integration)
if interactions.get('click_data', False):
layout['clickmode'] = 'event+select'
interactions_added.append('click_data')
# Selection mode
if 'selection' in interactions:
sel_mode = interactions['selection']
if sel_mode in ['box', 'lasso', 'box+lasso']:
layout['dragmode'] = 'select' if sel_mode == 'box' else sel_mode
interactions_added.append(f'selection:{sel_mode}')
# Zoom configuration
if 'zoom' in interactions:
if not interactions['zoom']:
layout['xaxis'] = layout.get('xaxis', {})
layout['yaxis'] = layout.get('yaxis', {})
layout['xaxis']['fixedrange'] = True
layout['yaxis']['fixedrange'] = True
interactions_added.append('zoom:disabled')
else:
interactions_added.append('zoom:enabled')
# Modebar configuration
if 'modebar' in interactions:
layout['modebar'] = interactions['modebar']
interactions_added.append('modebar')
figure['layout'] = layout
return {
"figure": figure,
"interactions_added": interactions_added
}
except Exception as e:
logger.error(f"Interaction configuration failed: {e}")
return {
"error": str(e),
"figure": figure,
"interactions_added": []
}

View File

@@ -0,0 +1,301 @@
"""
DMC Component Registry for viz-platform.
Provides version-locked component definitions to prevent Claude from
hallucinating invalid props. Uses static JSON registries pre-generated
from DMC source.
"""
import json
import logging
from pathlib import Path
from typing import Dict, List, Optional, Any
logger = logging.getLogger(__name__)
class ComponentRegistry:
"""
Version-locked registry of Dash Mantine Components.
Loads component definitions from static JSON files and provides
lookup methods for validation tools.
"""
def __init__(self, dmc_version: Optional[str] = None):
"""
Initialize the component registry.
Args:
dmc_version: Installed DMC version (e.g., "0.14.7").
If None, will try to detect or use fallback.
"""
self.dmc_version = dmc_version
self.registry_dir = Path(__file__).parent.parent / 'registry'
self.components: Dict[str, Dict[str, Any]] = {}
self.categories: Dict[str, List[str]] = {}
self.loaded_version: Optional[str] = None
def load(self) -> bool:
"""
Load the component registry for the configured DMC version.
Returns:
True if registry loaded successfully, False otherwise
"""
registry_file = self._find_registry_file()
if not registry_file:
logger.warning(
f"No registry found for DMC {self.dmc_version}. "
"Component validation will be limited."
)
return False
try:
with open(registry_file, 'r') as f:
data = json.load(f)
self.loaded_version = data.get('version')
self.components = data.get('components', {})
self.categories = data.get('categories', {})
logger.info(
f"Loaded component registry v{self.loaded_version} "
f"with {len(self.components)} components"
)
return True
except Exception as e:
logger.error(f"Failed to load registry: {e}")
return False
def _find_registry_file(self) -> Optional[Path]:
"""
Find the best matching registry file for the DMC version.
Strategy:
1. Exact major.minor match (e.g., dmc_0_14.json for 0.14.7)
2. Fallback to latest available registry
Returns:
Path to registry file, or None if not found
"""
if not self.registry_dir.exists():
logger.warning(f"Registry directory not found: {self.registry_dir}")
return None
# Try exact major.minor match
if self.dmc_version:
parts = self.dmc_version.split('.')
if len(parts) >= 2:
major_minor = f"{parts[0]}_{parts[1]}"
exact_match = self.registry_dir / f"dmc_{major_minor}.json"
if exact_match.exists():
return exact_match
# Fallback: find latest registry
registry_files = list(self.registry_dir.glob("dmc_*.json"))
if registry_files:
# Sort by version and return latest
registry_files.sort(reverse=True)
fallback = registry_files[0]
if self.dmc_version:
logger.warning(
f"No exact match for DMC {self.dmc_version}, "
f"using fallback: {fallback.name}"
)
return fallback
return None
def get_component(self, name: str) -> Optional[Dict[str, Any]]:
"""
Get component definition by name.
Args:
name: Component name (e.g., "Button", "TextInput")
Returns:
Component definition dict, or None if not found
"""
return self.components.get(name)
def get_component_props(self, name: str) -> Optional[Dict[str, Any]]:
"""
Get props schema for a component.
Args:
name: Component name
Returns:
Props dict with type info, or None if component not found
"""
component = self.get_component(name)
if component:
return component.get('props', {})
return None
def list_components(self, category: Optional[str] = None) -> Dict[str, List[str]]:
"""
List available components, optionally filtered by category.
Args:
category: Optional category filter (e.g., "inputs", "buttons")
Returns:
Dict of category -> component names
"""
if category:
if category in self.categories:
return {category: self.categories[category]}
return {}
return self.categories
def get_categories(self) -> List[str]:
"""
Get list of available component categories.
Returns:
List of category names
"""
return list(self.categories.keys())
def validate_prop(
self,
component: str,
prop_name: str,
prop_value: Any
) -> Dict[str, Any]:
"""
Validate a single prop value against the registry.
Args:
component: Component name
prop_name: Prop name
prop_value: Value to validate
Returns:
Dict with valid: bool, error: Optional[str]
"""
props = self.get_component_props(component)
if props is None:
return {
'valid': False,
'error': f"Unknown component: {component}"
}
if prop_name not in props:
# Check for similar prop names (typo detection)
similar = self._find_similar_props(prop_name, props.keys())
if similar:
return {
'valid': False,
'error': f"Unknown prop '{prop_name}' for {component}. Did you mean '{similar}'?"
}
return {
'valid': False,
'error': f"Unknown prop '{prop_name}' for {component}"
}
prop_schema = props[prop_name]
return self._validate_value(prop_value, prop_schema, prop_name)
def _validate_value(
self,
value: Any,
schema: Dict[str, Any],
prop_name: str
) -> Dict[str, Any]:
"""
Validate a value against a prop schema.
Args:
value: Value to validate
schema: Prop schema from registry
prop_name: Prop name (for error messages)
Returns:
Dict with valid: bool, error: Optional[str]
"""
prop_type = schema.get('type', 'any')
# Any type always valid
if prop_type == 'any':
return {'valid': True}
# Check enum values
if 'enum' in schema:
if value not in schema['enum']:
return {
'valid': False,
'error': f"Prop '{prop_name}' expects one of {schema['enum']}, got '{value}'"
}
return {'valid': True}
# Type checking
type_checks = {
'string': lambda v: isinstance(v, str),
'number': lambda v: isinstance(v, (int, float)),
'integer': lambda v: isinstance(v, int),
'boolean': lambda v: isinstance(v, bool),
'array': lambda v: isinstance(v, list),
'object': lambda v: isinstance(v, dict),
}
checker = type_checks.get(prop_type)
if checker and not checker(value):
return {
'valid': False,
'error': f"Prop '{prop_name}' expects type '{prop_type}', got '{type(value).__name__}'"
}
return {'valid': True}
def _find_similar_props(
self,
prop_name: str,
available_props: List[str]
) -> Optional[str]:
"""
Find a similar prop name for typo suggestions.
Uses simple edit distance heuristic.
Args:
prop_name: The (possibly misspelled) prop name
available_props: List of valid prop names
Returns:
Most similar prop name, or None if no close match
"""
prop_lower = prop_name.lower()
for prop in available_props:
# Exact match after lowercase
if prop.lower() == prop_lower:
return prop
# Common typos: extra/missing letter
if abs(len(prop) - len(prop_name)) == 1:
if prop_lower.startswith(prop.lower()[:3]):
return prop
return None
def is_loaded(self) -> bool:
"""Check if registry is loaded."""
return len(self.components) > 0
def load_registry(dmc_version: Optional[str] = None) -> ComponentRegistry:
"""
Convenience function to load and return a component registry.
Args:
dmc_version: Optional DMC version string
Returns:
Loaded ComponentRegistry instance
"""
registry = ComponentRegistry(dmc_version)
registry.load()
return registry

View File

@@ -0,0 +1,172 @@
"""
Configuration loader for viz-platform MCP Server.
Implements hybrid configuration system:
- System-level: ~/.config/claude/viz-platform.env (theme preferences)
- Project-level: .env (DMC version overrides)
- Auto-detection: DMC package version from installed package
"""
from pathlib import Path
from dotenv import load_dotenv
import os
import logging
from typing import Dict, Optional
logging.basicConfig(level=logging.INFO)
logger = logging.getLogger(__name__)
class VizPlatformConfig:
"""Hybrid configuration loader for viz-platform tools"""
def __init__(self):
self.dmc_version: Optional[str] = None
self.theme_dir_user: Path = Path.home() / '.config' / 'claude' / 'themes'
self.theme_dir_project: Optional[Path] = None
self.default_theme: Optional[str] = None
def load(self) -> Dict[str, any]:
"""
Load configuration from system and project levels.
Returns:
Dict containing dmc_version, theme directories, and availability flags
"""
# Load system config
system_config = Path.home() / '.config' / 'claude' / 'viz-platform.env'
if system_config.exists():
load_dotenv(system_config)
logger.info(f"Loaded system configuration from {system_config}")
# Find project directory
project_dir = self._find_project_directory()
# Load project config (overrides system)
if project_dir:
project_config = project_dir / '.env'
if project_config.exists():
load_dotenv(project_config, override=True)
logger.info(f"Loaded project configuration from {project_config}")
# Set project theme directory
self.theme_dir_project = project_dir / '.viz-platform' / 'themes'
# Get DMC version (from env or auto-detect)
self.dmc_version = os.getenv('DMC_VERSION') or self._detect_dmc_version()
self.default_theme = os.getenv('VIZ_DEFAULT_THEME')
# Ensure user theme directory exists
self.theme_dir_user.mkdir(parents=True, exist_ok=True)
return {
'dmc_version': self.dmc_version,
'dmc_available': self.dmc_version is not None,
'theme_dir_user': str(self.theme_dir_user),
'theme_dir_project': str(self.theme_dir_project) if self.theme_dir_project else None,
'default_theme': self.default_theme,
'project_dir': str(project_dir) if project_dir else None
}
def _detect_dmc_version(self) -> Optional[str]:
"""
Auto-detect installed Dash Mantine Components version.
Returns:
Version string (e.g., "0.14.7") or None if not installed
"""
try:
from importlib.metadata import version
dmc_version = version('dash-mantine-components')
logger.info(f"Detected DMC version: {dmc_version}")
return dmc_version
except ImportError:
logger.warning("dash-mantine-components not installed - using registry fallback")
return None
except Exception as e:
logger.warning(f"Could not detect DMC version: {e}")
return None
def _find_project_directory(self) -> Optional[Path]:
"""
Find the user's project directory.
Returns:
Path to project directory, or None if not found
"""
# Strategy 1: Check CLAUDE_PROJECT_DIR environment variable
project_dir = os.getenv('CLAUDE_PROJECT_DIR')
if project_dir:
path = Path(project_dir)
if path.exists():
logger.info(f"Found project directory from CLAUDE_PROJECT_DIR: {path}")
return path
# Strategy 2: Check PWD
pwd = os.getenv('PWD')
if pwd:
path = Path(pwd)
if path.exists() and (
(path / '.git').exists() or
(path / '.env').exists() or
(path / '.viz-platform').exists()
):
logger.info(f"Found project directory from PWD: {path}")
return path
# Strategy 3: Check current working directory
cwd = Path.cwd()
if (cwd / '.git').exists() or (cwd / '.env').exists() or (cwd / '.viz-platform').exists():
logger.info(f"Found project directory from cwd: {cwd}")
return cwd
logger.debug("Could not determine project directory")
return None
def load_config() -> Dict[str, any]:
"""
Convenience function to load configuration.
Returns:
Configuration dictionary
"""
config = VizPlatformConfig()
return config.load()
def check_dmc_version() -> Dict[str, any]:
"""
Check DMC installation status for SessionStart hook.
Returns:
Dict with installation status and version info
"""
config = load_config()
if not config.get('dmc_available'):
return {
'installed': False,
'message': 'dash-mantine-components not installed. Run: pip install dash-mantine-components'
}
version = config.get('dmc_version', 'unknown')
# Check for registry compatibility
registry_path = Path(__file__).parent.parent / 'registry'
major_minor = '.'.join(version.split('.')[:2]) if version else None
registry_file = registry_path / f'dmc_{major_minor.replace(".", "_")}.json' if major_minor else None
if registry_file and registry_file.exists():
return {
'installed': True,
'version': version,
'registry_available': True,
'message': f'DMC {version} ready with component registry'
}
else:
return {
'installed': True,
'version': version,
'registry_available': False,
'message': f'DMC {version} installed but no matching registry. Validation may be limited.'
}

View File

@@ -0,0 +1,306 @@
"""
DMC (Dash Mantine Components) validation tools.
Provides component constraint layer to prevent Claude from hallucinating invalid props.
"""
import logging
from typing import Dict, List, Optional, Any
from .component_registry import ComponentRegistry
logger = logging.getLogger(__name__)
class DMCTools:
"""
DMC component validation tools.
These tools provide the "constraint layer" that validates component usage
against a version-locked registry of DMC components.
"""
def __init__(self, registry: Optional[ComponentRegistry] = None):
"""
Initialize DMC tools with component registry.
Args:
registry: ComponentRegistry instance. If None, creates one.
"""
self.registry = registry
self._initialized = False
def initialize(self, dmc_version: Optional[str] = None) -> bool:
"""
Initialize the registry if not already provided.
Args:
dmc_version: DMC version to load registry for
Returns:
True if initialized successfully
"""
if self.registry is None:
self.registry = ComponentRegistry(dmc_version)
if not self.registry.is_loaded():
self.registry.load()
self._initialized = self.registry.is_loaded()
return self._initialized
async def list_components(
self,
category: Optional[str] = None
) -> Dict[str, Any]:
"""
List available DMC components, optionally filtered by category.
Args:
category: Optional category filter (e.g., "inputs", "buttons", "navigation")
Returns:
Dict with:
- components: Dict[category -> [component names]]
- categories: List of available categories
- version: Loaded DMC registry version
- total_count: Total number of components
"""
if not self._initialized:
return {
"error": "Registry not initialized",
"components": {},
"categories": [],
"version": None,
"total_count": 0
}
components = self.registry.list_components(category)
all_categories = self.registry.get_categories()
# Count total components
total = sum(len(comps) for comps in components.values())
return {
"components": components,
"categories": all_categories if not category else [category],
"version": self.registry.loaded_version,
"total_count": total
}
async def get_component_props(self, component: str) -> Dict[str, Any]:
"""
Get props schema for a specific component.
Args:
component: Component name (e.g., "Button", "TextInput")
Returns:
Dict with:
- component: Component name
- description: Component description
- props: Dict of prop name -> {type, default, enum, description}
- prop_count: Number of props
- required: List of required prop names
Or error dict if component not found
"""
if not self._initialized:
return {
"error": "Registry not initialized",
"component": component,
"props": {},
"prop_count": 0
}
comp_def = self.registry.get_component(component)
if not comp_def:
# Try to suggest similar component name
similar = self._find_similar_component(component)
error_msg = f"Component '{component}' not found in registry"
if similar:
error_msg += f". Did you mean '{similar}'?"
return {
"error": error_msg,
"component": component,
"props": {},
"prop_count": 0
}
props = comp_def.get('props', {})
# Extract required props
required = [
name for name, schema in props.items()
if schema.get('required', False)
]
return {
"component": component,
"description": comp_def.get('description', ''),
"props": props,
"prop_count": len(props),
"required": required,
"version": self.registry.loaded_version
}
async def validate_component(
self,
component: str,
props: Dict[str, Any]
) -> Dict[str, Any]:
"""
Validate component props against registry.
Args:
component: Component name
props: Props dict to validate
Returns:
Dict with:
- valid: bool - True if all props are valid
- errors: List of error messages
- warnings: List of warning messages
- validated_props: Number of props validated
- component: Component name for reference
"""
if not self._initialized:
return {
"valid": False,
"errors": ["Registry not initialized"],
"warnings": [],
"validated_props": 0,
"component": component
}
errors: List[str] = []
warnings: List[str] = []
# Check if component exists
comp_def = self.registry.get_component(component)
if not comp_def:
similar = self._find_similar_component(component)
error_msg = f"Unknown component: {component}"
if similar:
error_msg += f". Did you mean '{similar}'?"
errors.append(error_msg)
return {
"valid": False,
"errors": errors,
"warnings": warnings,
"validated_props": 0,
"component": component
}
comp_props = comp_def.get('props', {})
# Check for required props
for prop_name, prop_schema in comp_props.items():
if prop_schema.get('required', False) and prop_name not in props:
errors.append(f"Missing required prop: '{prop_name}'")
# Validate each provided prop
for prop_name, prop_value in props.items():
# Skip special props that are always allowed
if prop_name in ('id', 'children', 'className', 'style', 'key'):
continue
result = self.registry.validate_prop(component, prop_name, prop_value)
if not result.get('valid', True):
error = result.get('error', f"Invalid prop: {prop_name}")
# Distinguish between typos/unknown props and type errors
if "Unknown prop" in error:
errors.append(f"{error}")
elif "expects one of" in error:
errors.append(f"{error}")
elif "expects type" in error:
warnings.append(f"⚠️ {error}")
else:
errors.append(f"{error}")
# Check for props that exist but might have common mistakes
self._check_common_mistakes(component, props, warnings)
return {
"valid": len(errors) == 0,
"errors": errors,
"warnings": warnings,
"validated_props": len(props),
"component": component,
"version": self.registry.loaded_version
}
def _find_similar_component(self, component: str) -> Optional[str]:
"""
Find a similar component name for suggestions.
Args:
component: The (possibly misspelled) component name
Returns:
Similar component name, or None if no close match
"""
if not self.registry:
return None
comp_lower = component.lower()
all_components = []
for comps in self.registry.categories.values():
all_components.extend(comps)
for comp in all_components:
# Exact match after lowercase
if comp.lower() == comp_lower:
return comp
# Check if it's a prefix match
if comp.lower().startswith(comp_lower) or comp_lower.startswith(comp.lower()):
return comp
# Check for common typos
if abs(len(comp) - len(component)) <= 2:
if comp_lower[:4] == comp.lower()[:4]:
return comp
return None
def _check_common_mistakes(
self,
component: str,
props: Dict[str, Any],
warnings: List[str]
) -> None:
"""
Check for common prop usage mistakes and add warnings.
Args:
component: Component name
props: Props being used
warnings: List to append warnings to
"""
# Common mistake: using 'onclick' instead of callback pattern
if 'onclick' in [p.lower() for p in props.keys()]:
warnings.append(
"⚠️ Dash uses callback patterns, not inline event handlers. "
"Use 'n_clicks' prop with a callback instead."
)
# Common mistake: using 'class' instead of 'className'
if 'class' in props:
warnings.append(
"⚠️ Use 'className' instead of 'class' for CSS classes."
)
# Button-specific checks
if component == 'Button':
if 'href' in props and 'component' not in props:
warnings.append(
"⚠️ Button with 'href' should also set 'component=\"a\"' for proper anchor behavior."
)
# Input-specific checks
if 'Input' in component:
if 'value' in props and 'onChange' in [p for p in props.keys()]:
warnings.append(
"⚠️ Dash uses 'value' prop with callbacks, not 'onChange'. "
"The value updates automatically through Dash callbacks."
)

View File

@@ -0,0 +1,367 @@
"""
Layout composition tools for dashboard building.
Provides tools for creating structured layouts with grids, filters, and sections.
"""
import logging
from typing import Dict, List, Optional, Any
from uuid import uuid4
logger = logging.getLogger(__name__)
# Layout templates
TEMPLATES = {
"dashboard": {
"sections": ["header", "filters", "main", "footer"],
"default_grid": {"cols": 12, "spacing": "md"},
"description": "Standard dashboard with header, filters, main content, and footer"
},
"report": {
"sections": ["title", "summary", "content", "appendix"],
"default_grid": {"cols": 1, "spacing": "lg"},
"description": "Report layout with title, summary, and content sections"
},
"form": {
"sections": ["header", "fields", "actions"],
"default_grid": {"cols": 2, "spacing": "md"},
"description": "Form layout with header, fields, and action buttons"
},
"blank": {
"sections": ["main"],
"default_grid": {"cols": 12, "spacing": "md"},
"description": "Blank canvas for custom layouts"
}
}
# Filter type definitions
FILTER_TYPES = {
"dropdown": {
"component": "Select",
"props": ["label", "data", "placeholder", "clearable", "searchable", "value"]
},
"multi_select": {
"component": "MultiSelect",
"props": ["label", "data", "placeholder", "clearable", "searchable", "value"]
},
"date_range": {
"component": "DateRangePicker",
"props": ["label", "placeholder", "value", "minDate", "maxDate"]
},
"date": {
"component": "DatePicker",
"props": ["label", "placeholder", "value", "minDate", "maxDate"]
},
"search": {
"component": "TextInput",
"props": ["label", "placeholder", "value", "icon"]
},
"checkbox_group": {
"component": "CheckboxGroup",
"props": ["label", "children", "value"]
},
"radio_group": {
"component": "RadioGroup",
"props": ["label", "children", "value"]
},
"slider": {
"component": "Slider",
"props": ["label", "min", "max", "step", "value", "marks"]
},
"range_slider": {
"component": "RangeSlider",
"props": ["label", "min", "max", "step", "value", "marks"]
}
}
class LayoutTools:
"""
Dashboard layout composition tools.
Creates layouts that map to DMC Grid and AppShell components.
"""
def __init__(self):
"""Initialize layout tools."""
self._layouts: Dict[str, Dict[str, Any]] = {}
async def layout_create(
self,
name: str,
template: Optional[str] = None
) -> Dict[str, Any]:
"""
Create a new layout container.
Args:
name: Unique name for the layout
template: Optional template (dashboard, report, form, blank)
Returns:
Dict with:
- layout_ref: Reference to use in other tools
- template: Template used
- sections: Available sections
- grid: Default grid configuration
"""
# Validate template
template = template or "blank"
if template not in TEMPLATES:
return {
"error": f"Invalid template '{template}'. Must be one of: {list(TEMPLATES.keys())}",
"layout_ref": None
}
# Check for name collision
if name in self._layouts:
return {
"error": f"Layout '{name}' already exists. Use a different name or modify existing.",
"layout_ref": name
}
template_config = TEMPLATES[template]
# Create layout structure
layout = {
"id": str(uuid4()),
"name": name,
"template": template,
"sections": {section: {"items": []} for section in template_config["sections"]},
"grid": template_config["default_grid"].copy(),
"filters": [],
"metadata": {
"description": template_config["description"]
}
}
self._layouts[name] = layout
return {
"layout_ref": name,
"template": template,
"sections": template_config["sections"],
"grid": layout["grid"],
"description": template_config["description"]
}
async def layout_add_filter(
self,
layout_ref: str,
filter_type: str,
options: Dict[str, Any]
) -> Dict[str, Any]:
"""
Add a filter control to a layout.
Args:
layout_ref: Layout name to add filter to
filter_type: Type of filter (dropdown, date_range, search, checkbox_group, etc.)
options: Filter options (label, data for dropdown, placeholder, position)
Returns:
Dict with:
- filter_id: Unique ID for the filter
- component: DMC component that will be used
- props: Props that were set
- position: Where filter was placed
"""
# Validate layout exists
if layout_ref not in self._layouts:
return {
"error": f"Layout '{layout_ref}' not found. Create it first with layout_create.",
"filter_id": None
}
# Validate filter type
if filter_type not in FILTER_TYPES:
return {
"error": f"Invalid filter_type '{filter_type}'. Must be one of: {list(FILTER_TYPES.keys())}",
"filter_id": None
}
filter_config = FILTER_TYPES[filter_type]
layout = self._layouts[layout_ref]
# Generate filter ID
filter_id = f"filter_{filter_type}_{len(layout['filters'])}"
# Extract relevant props
props = {"id": filter_id}
for prop in filter_config["props"]:
if prop in options:
props[prop] = options[prop]
# Determine position
position = options.get("position", "filters")
if position not in layout["sections"]:
# Default to first available section
position = list(layout["sections"].keys())[0]
# Create filter definition
filter_def = {
"id": filter_id,
"type": filter_type,
"component": filter_config["component"],
"props": props,
"position": position
}
layout["filters"].append(filter_def)
layout["sections"][position]["items"].append({
"type": "filter",
"ref": filter_id
})
return {
"filter_id": filter_id,
"component": filter_config["component"],
"props": props,
"position": position,
"layout_ref": layout_ref
}
async def layout_set_grid(
self,
layout_ref: str,
grid: Dict[str, Any]
) -> Dict[str, Any]:
"""
Configure the grid system for a layout.
Args:
layout_ref: Layout name to configure
grid: Grid configuration:
- cols: Number of columns (default 12)
- spacing: Gap between items (xs, sm, md, lg, xl)
- breakpoints: Responsive breakpoints {xs: cols, sm: cols, ...}
- gutter: Gutter size
Returns:
Dict with:
- grid: Updated grid configuration
- layout_ref: Layout reference
"""
# Validate layout exists
if layout_ref not in self._layouts:
return {
"error": f"Layout '{layout_ref}' not found. Create it first with layout_create.",
"grid": None
}
layout = self._layouts[layout_ref]
# Validate spacing if provided
valid_spacing = ["xs", "sm", "md", "lg", "xl"]
if "spacing" in grid and grid["spacing"] not in valid_spacing:
return {
"error": f"Invalid spacing '{grid['spacing']}'. Must be one of: {valid_spacing}",
"grid": layout["grid"]
}
# Validate cols
if "cols" in grid:
cols = grid["cols"]
if not isinstance(cols, int) or cols < 1 or cols > 24:
return {
"error": f"Invalid cols '{cols}'. Must be integer between 1 and 24.",
"grid": layout["grid"]
}
# Update grid configuration
layout["grid"].update(grid)
# Process breakpoints if provided
if "breakpoints" in grid:
bp = grid["breakpoints"]
layout["grid"]["breakpoints"] = bp
return {
"grid": layout["grid"],
"layout_ref": layout_ref
}
async def layout_get(self, layout_ref: str) -> Dict[str, Any]:
"""
Get a layout's full configuration.
Args:
layout_ref: Layout name to retrieve
Returns:
Full layout configuration or error
"""
if layout_ref not in self._layouts:
return {
"error": f"Layout '{layout_ref}' not found.",
"layout": None
}
layout = self._layouts[layout_ref]
return {
"layout": layout,
"filter_count": len(layout["filters"]),
"sections": list(layout["sections"].keys())
}
async def layout_add_section(
self,
layout_ref: str,
section_name: str,
position: Optional[int] = None
) -> Dict[str, Any]:
"""
Add a custom section to a layout.
Args:
layout_ref: Layout name
section_name: Name for the new section
position: Optional position index (appends if not specified)
Returns:
Dict with sections list and the new section name
"""
if layout_ref not in self._layouts:
return {
"error": f"Layout '{layout_ref}' not found.",
"sections": []
}
layout = self._layouts[layout_ref]
if section_name in layout["sections"]:
return {
"error": f"Section '{section_name}' already exists.",
"sections": list(layout["sections"].keys())
}
# Add new section
layout["sections"][section_name] = {"items": []}
return {
"section_name": section_name,
"sections": list(layout["sections"].keys()),
"layout_ref": layout_ref
}
def get_available_templates(self) -> Dict[str, Any]:
"""Get list of available layout templates."""
return {
name: {
"sections": config["sections"],
"description": config["description"]
}
for name, config in TEMPLATES.items()
}
def get_available_filter_types(self) -> Dict[str, Any]:
"""Get list of available filter types."""
return {
name: {
"component": config["component"],
"props": config["props"]
}
for name, config in FILTER_TYPES.items()
}

View File

@@ -0,0 +1,366 @@
"""
Multi-page app tools for viz-platform.
Provides tools for building complete Dash applications with routing and navigation.
"""
import logging
from typing import Dict, List, Optional, Any
from uuid import uuid4
logger = logging.getLogger(__name__)
# Navigation position options
NAV_POSITIONS = ["top", "left", "right"]
# Auth types supported
AUTH_TYPES = ["none", "basic", "oauth", "custom"]
class PageTools:
"""
Multi-page Dash application tools.
Creates page definitions, navigation, and auth configuration.
"""
def __init__(self):
"""Initialize page tools."""
self._pages: Dict[str, Dict[str, Any]] = {}
self._navbars: Dict[str, Dict[str, Any]] = {}
self._app_config: Dict[str, Any] = {
"title": "Dash App",
"suppress_callback_exceptions": True
}
async def page_create(
self,
name: str,
path: str,
layout_ref: Optional[str] = None,
title: Optional[str] = None
) -> Dict[str, Any]:
"""
Create a new page definition.
Args:
name: Unique page name (used as identifier)
path: URL path for the page (e.g., "/", "/settings")
layout_ref: Optional layout reference to use for the page
title: Optional page title (defaults to name)
Returns:
Dict with:
- page_ref: Reference to use in other tools
- path: URL path
- registered: Whether page was registered
"""
# Validate path format
if not path.startswith('/'):
return {
"error": f"Path must start with '/'. Got: {path}",
"page_ref": None
}
# Check for name collision
if name in self._pages:
return {
"error": f"Page '{name}' already exists. Use a different name.",
"page_ref": name
}
# Check for path collision
for page_name, page_data in self._pages.items():
if page_data['path'] == path:
return {
"error": f"Path '{path}' already used by page '{page_name}'.",
"page_ref": None
}
# Create page definition
page = {
"id": str(uuid4()),
"name": name,
"path": path,
"title": title or name,
"layout_ref": layout_ref,
"auth": None,
"metadata": {}
}
self._pages[name] = page
return {
"page_ref": name,
"path": path,
"title": page["title"],
"layout_ref": layout_ref,
"registered": True
}
async def page_add_navbar(
self,
pages: List[str],
options: Optional[Dict[str, Any]] = None
) -> Dict[str, Any]:
"""
Generate a navigation component linking pages.
Args:
pages: List of page names to include in navigation
options: Navigation options:
- position: "top", "left", or "right"
- style: Style variant
- brand: Brand/logo text or config
- collapsible: Whether to collapse on mobile
Returns:
Dict with:
- navbar_id: Navigation ID
- pages: List of page links generated
- component: DMC component structure
"""
options = options or {}
# Validate pages exist
missing_pages = [p for p in pages if p not in self._pages]
if missing_pages:
return {
"error": f"Pages not found: {missing_pages}. Create them first.",
"navbar_id": None
}
# Validate position
position = options.get("position", "top")
if position not in NAV_POSITIONS:
return {
"error": f"Invalid position '{position}'. Must be one of: {NAV_POSITIONS}",
"navbar_id": None
}
# Generate navbar ID
navbar_id = f"navbar_{len(self._navbars)}"
# Build page links
page_links = []
for page_name in pages:
page = self._pages[page_name]
page_links.append({
"label": page["title"],
"href": page["path"],
"page_ref": page_name
})
# Build DMC component structure
if position == "top":
component = self._build_top_navbar(page_links, options)
else:
component = self._build_side_navbar(page_links, options, position)
# Store navbar config
self._navbars[navbar_id] = {
"id": navbar_id,
"position": position,
"pages": pages,
"options": options,
"component": component
}
return {
"navbar_id": navbar_id,
"position": position,
"pages": page_links,
"component": component
}
async def page_set_auth(
self,
page_ref: str,
auth_config: Dict[str, Any]
) -> Dict[str, Any]:
"""
Configure authentication for a page.
Args:
page_ref: Page name to configure
auth_config: Authentication configuration:
- type: "none", "basic", "oauth", "custom"
- required: Whether auth is required (default True)
- roles: List of required roles (optional)
- redirect: Redirect path for unauthenticated users
Returns:
Dict with:
- page_ref: Page reference
- auth_type: Type of auth configured
- protected: Whether page is protected
"""
# Validate page exists
if page_ref not in self._pages:
available = list(self._pages.keys())
return {
"error": f"Page '{page_ref}' not found. Available: {available}",
"page_ref": page_ref
}
# Validate auth type
auth_type = auth_config.get("type", "basic")
if auth_type not in AUTH_TYPES:
return {
"error": f"Invalid auth type '{auth_type}'. Must be one of: {AUTH_TYPES}",
"page_ref": page_ref
}
# Build auth config
auth = {
"type": auth_type,
"required": auth_config.get("required", True),
"roles": auth_config.get("roles", []),
"redirect": auth_config.get("redirect", "/login")
}
# Handle OAuth-specific config
if auth_type == "oauth":
auth["provider"] = auth_config.get("provider", "generic")
auth["scopes"] = auth_config.get("scopes", [])
# Update page
self._pages[page_ref]["auth"] = auth
return {
"page_ref": page_ref,
"auth_type": auth_type,
"protected": auth["required"],
"roles": auth["roles"],
"redirect": auth["redirect"]
}
async def page_list(self) -> Dict[str, Any]:
"""
List all registered pages.
Returns:
Dict with pages and their configurations
"""
pages_info = {}
for name, page in self._pages.items():
pages_info[name] = {
"path": page["path"],
"title": page["title"],
"layout_ref": page["layout_ref"],
"protected": page["auth"] is not None and page["auth"].get("required", False)
}
return {
"pages": pages_info,
"count": len(pages_info),
"navbars": list(self._navbars.keys())
}
async def page_get_app_config(self) -> Dict[str, Any]:
"""
Get the complete app configuration for Dash.
Returns:
Dict with app config including pages, navbars, and settings
"""
# Build pages config
pages_config = []
for name, page in self._pages.items():
pages_config.append({
"name": name,
"path": page["path"],
"title": page["title"],
"layout_ref": page["layout_ref"]
})
# Build routing config
routes = {page["path"]: name for name, page in self._pages.items()}
return {
"app": self._app_config,
"pages": pages_config,
"routes": routes,
"navbars": list(self._navbars.values()),
"page_count": len(self._pages)
}
def _build_top_navbar(
self,
page_links: List[Dict[str, str]],
options: Dict[str, Any]
) -> Dict[str, Any]:
"""Build a top navigation bar component."""
brand = options.get("brand", "App")
# Build nav links
nav_items = []
for link in page_links:
nav_items.append({
"component": "NavLink",
"props": {
"label": link["label"],
"href": link["href"]
}
})
return {
"component": "AppShell.Header",
"children": [
{
"component": "Group",
"props": {"justify": "space-between", "h": "100%", "px": "md"},
"children": [
{
"component": "Text",
"props": {"size": "lg", "fw": 700},
"children": brand
},
{
"component": "Group",
"props": {"gap": "sm"},
"children": nav_items
}
]
}
]
}
def _build_side_navbar(
self,
page_links: List[Dict[str, str]],
options: Dict[str, Any],
position: str
) -> Dict[str, Any]:
"""Build a side navigation bar component."""
brand = options.get("brand", "App")
# Build nav links
nav_items = []
for link in page_links:
nav_items.append({
"component": "NavLink",
"props": {
"label": link["label"],
"href": link["href"]
}
})
navbar_component = "AppShell.Navbar" if position == "left" else "AppShell.Aside"
return {
"component": navbar_component,
"props": {"p": "md"},
"children": [
{
"component": "Text",
"props": {"size": "lg", "fw": 700, "mb": "md"},
"children": brand
},
{
"component": "Stack",
"props": {"gap": "xs"},
"children": nav_items
}
]
}

View File

@@ -0,0 +1,701 @@
"""
MCP Server entry point for viz-platform integration.
Provides Dash Mantine Components validation, charting, layout, theming, and page tools
to Claude Code via JSON-RPC 2.0 over stdio.
"""
import asyncio
import logging
import json
from mcp.server import Server
from mcp.server.stdio import stdio_server
from mcp.types import Tool, TextContent
from .config import VizPlatformConfig
from .dmc_tools import DMCTools
from .chart_tools import ChartTools
from .layout_tools import LayoutTools
from .theme_tools import ThemeTools
from .page_tools import PageTools
# Suppress noisy MCP validation warnings on stderr
logging.basicConfig(level=logging.INFO)
logging.getLogger("root").setLevel(logging.ERROR)
logging.getLogger("mcp").setLevel(logging.ERROR)
logger = logging.getLogger(__name__)
class VizPlatformMCPServer:
"""MCP Server for visualization platform integration"""
def __init__(self):
self.server = Server("viz-platform-mcp")
self.config = None
self.dmc_tools = DMCTools()
self.chart_tools = ChartTools()
self.layout_tools = LayoutTools()
self.theme_tools = ThemeTools()
self.page_tools = PageTools()
async def initialize(self):
"""Initialize server and load configuration."""
try:
config_loader = VizPlatformConfig()
self.config = config_loader.load()
# Initialize DMC tools with detected version
dmc_version = self.config.get('dmc_version')
self.dmc_tools.initialize(dmc_version)
# Log available capabilities
caps = []
if self.config.get('dmc_available'):
caps.append(f"DMC {dmc_version}")
if self.dmc_tools._initialized:
caps.append(f"Registry loaded ({self.dmc_tools.registry.loaded_version})")
else:
caps.append("DMC (not installed)")
logger.info(f"viz-platform MCP Server initialized with: {', '.join(caps)}")
except Exception as e:
logger.error(f"Failed to initialize: {e}")
raise
def setup_tools(self):
"""Register all available tools with the MCP server"""
@self.server.list_tools()
async def list_tools() -> list[Tool]:
"""Return list of available tools"""
tools = []
# DMC validation tools (Issue #172)
tools.append(Tool(
name="list_components",
description=(
"List available Dash Mantine Components. "
"Returns components grouped by category with version info. "
"Use this to discover what components are available before building UI."
),
inputSchema={
"type": "object",
"properties": {
"category": {
"type": "string",
"description": (
"Optional category filter. Available categories: "
"buttons, inputs, navigation, feedback, overlays, "
"typography, layout, data_display, charts, dates"
)
}
},
"required": []
}
))
tools.append(Tool(
name="get_component_props",
description=(
"Get the props schema for a specific DMC component. "
"Returns all available props with types, defaults, and allowed values. "
"ALWAYS use this before creating a component to ensure valid props."
),
inputSchema={
"type": "object",
"properties": {
"component": {
"type": "string",
"description": "Component name (e.g., 'Button', 'TextInput', 'Select')"
}
},
"required": ["component"]
}
))
tools.append(Tool(
name="validate_component",
description=(
"Validate component props before use. "
"Checks for invalid props, type mismatches, and common mistakes. "
"Returns errors and warnings with suggestions for fixes."
),
inputSchema={
"type": "object",
"properties": {
"component": {
"type": "string",
"description": "Component name to validate"
},
"props": {
"type": "object",
"description": "Props object to validate"
}
},
"required": ["component", "props"]
}
))
# Chart tools (Issue #173)
tools.append(Tool(
name="chart_create",
description=(
"Create a Plotly chart for data visualization. "
"Supports line, bar, scatter, pie, heatmap, histogram, and area charts. "
"Automatically applies theme colors when a theme is active."
),
inputSchema={
"type": "object",
"properties": {
"chart_type": {
"type": "string",
"enum": ["line", "bar", "scatter", "pie", "heatmap", "histogram", "area"],
"description": "Type of chart to create"
},
"data": {
"type": "object",
"description": (
"Data for the chart. For most charts: {x: [], y: []}. "
"For pie: {labels: [], values: []}. "
"For heatmap: {x: [], y: [], z: [[]]}"
)
},
"options": {
"type": "object",
"description": (
"Optional settings: title, x_label, y_label, color, "
"showlegend, height, width, horizontal (for bar)"
)
}
},
"required": ["chart_type", "data"]
}
))
tools.append(Tool(
name="chart_configure_interaction",
description=(
"Configure interactions on an existing chart. "
"Add hover templates, enable click data capture, selection modes, "
"and zoom behavior for Dash callback integration."
),
inputSchema={
"type": "object",
"properties": {
"figure": {
"type": "object",
"description": "Plotly figure JSON to modify"
},
"interactions": {
"type": "object",
"description": (
"Interaction config: hover_template (string), "
"click_data (bool), selection ('box'|'lasso'), zoom (bool)"
)
}
},
"required": ["figure", "interactions"]
}
))
# Layout tools (Issue #174)
tools.append(Tool(
name="layout_create",
description=(
"Create a new dashboard layout container. "
"Templates available: dashboard, report, form, blank. "
"Returns layout reference for use with other layout tools."
),
inputSchema={
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Unique name for the layout"
},
"template": {
"type": "string",
"enum": ["dashboard", "report", "form", "blank"],
"description": "Layout template to use (default: blank)"
}
},
"required": ["name"]
}
))
tools.append(Tool(
name="layout_add_filter",
description=(
"Add a filter control to a layout. "
"Filter types: dropdown, multi_select, date_range, date, search, "
"checkbox_group, radio_group, slider, range_slider."
),
inputSchema={
"type": "object",
"properties": {
"layout_ref": {
"type": "string",
"description": "Layout name to add filter to"
},
"filter_type": {
"type": "string",
"enum": ["dropdown", "multi_select", "date_range", "date",
"search", "checkbox_group", "radio_group", "slider", "range_slider"],
"description": "Type of filter control"
},
"options": {
"type": "object",
"description": (
"Filter options: label, data (for dropdown), placeholder, "
"position (section name), value, etc."
)
}
},
"required": ["layout_ref", "filter_type", "options"]
}
))
tools.append(Tool(
name="layout_set_grid",
description=(
"Configure the grid system for a layout. "
"Uses DMC Grid component patterns with 12 or 24 column system."
),
inputSchema={
"type": "object",
"properties": {
"layout_ref": {
"type": "string",
"description": "Layout name to configure"
},
"grid": {
"type": "object",
"description": (
"Grid config: cols (1-24), spacing (xs|sm|md|lg|xl), "
"breakpoints ({xs: cols, sm: cols, ...}), gutter"
)
}
},
"required": ["layout_ref", "grid"]
}
))
# Theme tools (Issue #175)
tools.append(Tool(
name="theme_create",
description=(
"Create a new design theme with tokens. "
"Tokens include colors, spacing, typography, radii. "
"Missing tokens are filled from defaults."
),
inputSchema={
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Unique theme name"
},
"tokens": {
"type": "object",
"description": (
"Design tokens: colors (primary, background, text), "
"spacing (xs-xl), typography (fontFamily, fontSize), radii (sm-xl)"
)
}
},
"required": ["name", "tokens"]
}
))
tools.append(Tool(
name="theme_extend",
description=(
"Create a new theme by extending an existing one. "
"Only specify the tokens you want to override."
),
inputSchema={
"type": "object",
"properties": {
"base_theme": {
"type": "string",
"description": "Theme to extend (e.g., 'default')"
},
"overrides": {
"type": "object",
"description": "Token overrides to apply"
},
"new_name": {
"type": "string",
"description": "Name for the new theme (optional)"
}
},
"required": ["base_theme", "overrides"]
}
))
tools.append(Tool(
name="theme_validate",
description=(
"Validate a theme for completeness. "
"Checks for required tokens and common issues."
),
inputSchema={
"type": "object",
"properties": {
"theme_name": {
"type": "string",
"description": "Theme to validate"
}
},
"required": ["theme_name"]
}
))
tools.append(Tool(
name="theme_export_css",
description=(
"Export a theme as CSS custom properties. "
"Generates :root CSS variables for all tokens."
),
inputSchema={
"type": "object",
"properties": {
"theme_name": {
"type": "string",
"description": "Theme to export"
}
},
"required": ["theme_name"]
}
))
# Page tools (Issue #176)
tools.append(Tool(
name="page_create",
description=(
"Create a new page for a multi-page Dash application. "
"Defines page routing and can link to a layout."
),
inputSchema={
"type": "object",
"properties": {
"name": {
"type": "string",
"description": "Unique page name (identifier)"
},
"path": {
"type": "string",
"description": "URL path (e.g., '/', '/settings')"
},
"layout_ref": {
"type": "string",
"description": "Optional layout reference to use"
},
"title": {
"type": "string",
"description": "Page title (defaults to name)"
}
},
"required": ["name", "path"]
}
))
tools.append(Tool(
name="page_add_navbar",
description=(
"Generate navigation component linking pages. "
"Creates top or side navigation with DMC components."
),
inputSchema={
"type": "object",
"properties": {
"pages": {
"type": "array",
"items": {"type": "string"},
"description": "List of page names to include"
},
"options": {
"type": "object",
"description": (
"Navigation options: position (top|left|right), "
"brand (app name), collapsible (bool)"
)
}
},
"required": ["pages"]
}
))
tools.append(Tool(
name="page_set_auth",
description=(
"Configure authentication for a page. "
"Sets auth requirements, roles, and redirect behavior."
),
inputSchema={
"type": "object",
"properties": {
"page_ref": {
"type": "string",
"description": "Page name to configure"
},
"auth_config": {
"type": "object",
"description": (
"Auth config: type (none|basic|oauth|custom), "
"required (bool), roles (array), redirect (path)"
)
}
},
"required": ["page_ref", "auth_config"]
}
))
return tools
@self.server.call_tool()
async def call_tool(name: str, arguments: dict) -> list[TextContent]:
"""Handle tool invocation."""
try:
# DMC validation tools
if name == "list_components":
result = await self.dmc_tools.list_components(
category=arguments.get('category')
)
return [TextContent(
type="text",
text=json.dumps(result, indent=2)
)]
elif name == "get_component_props":
component = arguments.get('component')
if not component:
return [TextContent(
type="text",
text=json.dumps({"error": "component is required"}, indent=2)
)]
result = await self.dmc_tools.get_component_props(component)
return [TextContent(
type="text",
text=json.dumps(result, indent=2)
)]
elif name == "validate_component":
component = arguments.get('component')
props = arguments.get('props', {})
if not component:
return [TextContent(
type="text",
text=json.dumps({"error": "component is required"}, indent=2)
)]
result = await self.dmc_tools.validate_component(component, props)
return [TextContent(
type="text",
text=json.dumps(result, indent=2)
)]
# Chart tools
elif name == "chart_create":
chart_type = arguments.get('chart_type')
data = arguments.get('data', {})
options = arguments.get('options', {})
if not chart_type:
return [TextContent(
type="text",
text=json.dumps({"error": "chart_type is required"}, indent=2)
)]
result = await self.chart_tools.chart_create(chart_type, data, options)
return [TextContent(
type="text",
text=json.dumps(result, indent=2)
)]
elif name == "chart_configure_interaction":
figure = arguments.get('figure')
interactions = arguments.get('interactions', {})
if not figure:
return [TextContent(
type="text",
text=json.dumps({"error": "figure is required"}, indent=2)
)]
result = await self.chart_tools.chart_configure_interaction(figure, interactions)
return [TextContent(
type="text",
text=json.dumps(result, indent=2)
)]
# Layout tools
elif name == "layout_create":
layout_name = arguments.get('name')
template = arguments.get('template')
if not layout_name:
return [TextContent(
type="text",
text=json.dumps({"error": "name is required"}, indent=2)
)]
result = await self.layout_tools.layout_create(layout_name, template)
return [TextContent(
type="text",
text=json.dumps(result, indent=2)
)]
elif name == "layout_add_filter":
layout_ref = arguments.get('layout_ref')
filter_type = arguments.get('filter_type')
options = arguments.get('options', {})
if not layout_ref or not filter_type:
return [TextContent(
type="text",
text=json.dumps({"error": "layout_ref and filter_type are required"}, indent=2)
)]
result = await self.layout_tools.layout_add_filter(layout_ref, filter_type, options)
return [TextContent(
type="text",
text=json.dumps(result, indent=2)
)]
elif name == "layout_set_grid":
layout_ref = arguments.get('layout_ref')
grid = arguments.get('grid', {})
if not layout_ref:
return [TextContent(
type="text",
text=json.dumps({"error": "layout_ref is required"}, indent=2)
)]
result = await self.layout_tools.layout_set_grid(layout_ref, grid)
return [TextContent(
type="text",
text=json.dumps(result, indent=2)
)]
# Theme tools
elif name == "theme_create":
theme_name = arguments.get('name')
tokens = arguments.get('tokens', {})
if not theme_name:
return [TextContent(
type="text",
text=json.dumps({"error": "name is required"}, indent=2)
)]
result = await self.theme_tools.theme_create(theme_name, tokens)
return [TextContent(
type="text",
text=json.dumps(result, indent=2)
)]
elif name == "theme_extend":
base_theme = arguments.get('base_theme')
overrides = arguments.get('overrides', {})
new_name = arguments.get('new_name')
if not base_theme:
return [TextContent(
type="text",
text=json.dumps({"error": "base_theme is required"}, indent=2)
)]
result = await self.theme_tools.theme_extend(base_theme, overrides, new_name)
return [TextContent(
type="text",
text=json.dumps(result, indent=2)
)]
elif name == "theme_validate":
theme_name = arguments.get('theme_name')
if not theme_name:
return [TextContent(
type="text",
text=json.dumps({"error": "theme_name is required"}, indent=2)
)]
result = await self.theme_tools.theme_validate(theme_name)
return [TextContent(
type="text",
text=json.dumps(result, indent=2)
)]
elif name == "theme_export_css":
theme_name = arguments.get('theme_name')
if not theme_name:
return [TextContent(
type="text",
text=json.dumps({"error": "theme_name is required"}, indent=2)
)]
result = await self.theme_tools.theme_export_css(theme_name)
return [TextContent(
type="text",
text=json.dumps(result, indent=2)
)]
# Page tools
elif name == "page_create":
page_name = arguments.get('name')
path = arguments.get('path')
layout_ref = arguments.get('layout_ref')
title = arguments.get('title')
if not page_name or not path:
return [TextContent(
type="text",
text=json.dumps({"error": "name and path are required"}, indent=2)
)]
result = await self.page_tools.page_create(page_name, path, layout_ref, title)
return [TextContent(
type="text",
text=json.dumps(result, indent=2)
)]
elif name == "page_add_navbar":
pages = arguments.get('pages', [])
options = arguments.get('options', {})
if not pages:
return [TextContent(
type="text",
text=json.dumps({"error": "pages list is required"}, indent=2)
)]
result = await self.page_tools.page_add_navbar(pages, options)
return [TextContent(
type="text",
text=json.dumps(result, indent=2)
)]
elif name == "page_set_auth":
page_ref = arguments.get('page_ref')
auth_config = arguments.get('auth_config', {})
if not page_ref:
return [TextContent(
type="text",
text=json.dumps({"error": "page_ref is required"}, indent=2)
)]
result = await self.page_tools.page_set_auth(page_ref, auth_config)
return [TextContent(
type="text",
text=json.dumps(result, indent=2)
)]
raise ValueError(f"Unknown tool: {name}")
except Exception as e:
logger.error(f"Tool {name} failed: {e}")
return [TextContent(
type="text",
text=json.dumps({"error": str(e)}, indent=2)
)]
async def run(self):
"""Run the MCP server"""
await self.initialize()
self.setup_tools()
async with stdio_server() as (read_stream, write_stream):
await self.server.run(
read_stream,
write_stream,
self.server.create_initialization_options()
)
async def main():
"""Main entry point"""
server = VizPlatformMCPServer()
await server.run()
if __name__ == "__main__":
asyncio.run(main())

View File

@@ -0,0 +1,259 @@
"""
Theme storage and persistence for viz-platform.
Handles saving/loading themes from user and project locations.
"""
import json
import logging
from pathlib import Path
from typing import Dict, List, Optional, Any
logger = logging.getLogger(__name__)
# Default theme based on Mantine defaults
DEFAULT_THEME = {
"name": "default",
"version": "1.0.0",
"tokens": {
"colors": {
"primary": "#228be6",
"secondary": "#868e96",
"success": "#40c057",
"warning": "#fab005",
"error": "#fa5252",
"info": "#15aabf",
"background": {
"base": "#ffffff",
"subtle": "#f8f9fa",
"dark": "#212529"
},
"text": {
"primary": "#212529",
"secondary": "#495057",
"muted": "#868e96",
"inverse": "#ffffff"
},
"border": "#dee2e6"
},
"spacing": {
"xs": "4px",
"sm": "8px",
"md": "16px",
"lg": "24px",
"xl": "32px"
},
"typography": {
"fontFamily": "Inter, -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, sans-serif",
"fontFamilyMono": "ui-monospace, SFMono-Regular, Menlo, Monaco, monospace",
"fontSize": {
"xs": "12px",
"sm": "14px",
"md": "16px",
"lg": "18px",
"xl": "20px"
},
"fontWeight": {
"normal": 400,
"medium": 500,
"semibold": 600,
"bold": 700
},
"lineHeight": {
"tight": 1.25,
"normal": 1.5,
"relaxed": 1.75
}
},
"radii": {
"none": "0px",
"sm": "4px",
"md": "8px",
"lg": "16px",
"xl": "24px",
"full": "9999px"
},
"shadows": {
"none": "none",
"sm": "0 1px 2px 0 rgb(0 0 0 / 0.05)",
"md": "0 4px 6px -1px rgb(0 0 0 / 0.1)",
"lg": "0 10px 15px -3px rgb(0 0 0 / 0.1)",
"xl": "0 20px 25px -5px rgb(0 0 0 / 0.1)"
},
"transitions": {
"fast": "150ms",
"normal": "300ms",
"slow": "500ms"
}
}
}
# Required token categories for validation
REQUIRED_TOKEN_CATEGORIES = ["colors", "spacing", "typography", "radii"]
class ThemeStore:
"""
Store and manage design themes.
Handles persistence to user-level and project-level locations.
"""
def __init__(self, project_dir: Optional[Path] = None):
"""
Initialize theme store.
Args:
project_dir: Project directory for project-level themes
"""
self.project_dir = project_dir
self._themes: Dict[str, Dict[str, Any]] = {}
self._active_theme: Optional[str] = None
# Load default theme
self._themes["default"] = DEFAULT_THEME.copy()
@property
def user_themes_dir(self) -> Path:
"""User-level themes directory."""
return Path.home() / ".config" / "claude" / "themes"
@property
def project_themes_dir(self) -> Optional[Path]:
"""Project-level themes directory."""
if self.project_dir:
return self.project_dir / ".viz-platform" / "themes"
return None
def load_themes(self) -> int:
"""
Load themes from user and project directories.
Project themes take precedence over user themes.
Returns:
Number of themes loaded
"""
count = 0
# Load user themes
if self.user_themes_dir.exists():
for theme_file in self.user_themes_dir.glob("*.json"):
try:
with open(theme_file, 'r') as f:
theme = json.load(f)
name = theme.get('name', theme_file.stem)
self._themes[name] = theme
count += 1
logger.debug(f"Loaded user theme: {name}")
except Exception as e:
logger.warning(f"Failed to load theme {theme_file}: {e}")
# Load project themes (override user themes)
if self.project_themes_dir and self.project_themes_dir.exists():
for theme_file in self.project_themes_dir.glob("*.json"):
try:
with open(theme_file, 'r') as f:
theme = json.load(f)
name = theme.get('name', theme_file.stem)
self._themes[name] = theme
count += 1
logger.debug(f"Loaded project theme: {name}")
except Exception as e:
logger.warning(f"Failed to load theme {theme_file}: {e}")
return count
def save_theme(
self,
theme: Dict[str, Any],
location: str = "project"
) -> Path:
"""
Save a theme to disk.
Args:
theme: Theme dict to save
location: "user" or "project"
Returns:
Path where theme was saved
"""
name = theme.get('name', 'unnamed')
if location == "user":
target_dir = self.user_themes_dir
else:
target_dir = self.project_themes_dir
if not target_dir:
target_dir = self.user_themes_dir
target_dir.mkdir(parents=True, exist_ok=True)
theme_path = target_dir / f"{name}.json"
with open(theme_path, 'w') as f:
json.dump(theme, f, indent=2)
# Update in-memory store
self._themes[name] = theme
return theme_path
def get_theme(self, name: str) -> Optional[Dict[str, Any]]:
"""Get a theme by name."""
return self._themes.get(name)
def list_themes(self) -> List[str]:
"""List all available theme names."""
return list(self._themes.keys())
def set_active_theme(self, name: str) -> bool:
"""
Set the active theme.
Args:
name: Theme name to activate
Returns:
True if theme was activated
"""
if name in self._themes:
self._active_theme = name
return True
return False
def get_active_theme(self) -> Optional[Dict[str, Any]]:
"""Get the currently active theme."""
if self._active_theme:
return self._themes.get(self._active_theme)
return None
def delete_theme(self, name: str) -> bool:
"""
Delete a theme.
Args:
name: Theme name to delete
Returns:
True if theme was deleted
"""
if name == "default":
return False # Cannot delete default theme
if name in self._themes:
del self._themes[name]
# Remove file if exists
for themes_dir in [self.user_themes_dir, self.project_themes_dir]:
if themes_dir and themes_dir.exists():
theme_path = themes_dir / f"{name}.json"
if theme_path.exists():
theme_path.unlink()
if self._active_theme == name:
self._active_theme = None
return True
return False

View File

@@ -0,0 +1,391 @@
"""
Theme management tools for viz-platform.
Provides design token-based theming system for consistent visual styling.
"""
import copy
import logging
from typing import Dict, List, Optional, Any
from .theme_store import ThemeStore, DEFAULT_THEME, REQUIRED_TOKEN_CATEGORIES
logger = logging.getLogger(__name__)
class ThemeTools:
"""
Design token-based theming tools.
Creates and manages themes that integrate with DMC and Plotly.
"""
def __init__(self, store: Optional[ThemeStore] = None):
"""
Initialize theme tools.
Args:
store: Optional ThemeStore for persistence
"""
self.store = store or ThemeStore()
async def theme_create(
self,
name: str,
tokens: Dict[str, Any]
) -> Dict[str, Any]:
"""
Create a new theme with design tokens.
Args:
name: Unique theme name
tokens: Design tokens dict with colors, spacing, typography, radii
Returns:
Dict with:
- name: Theme name
- tokens: Full token set (merged with defaults)
- validation: Validation results
"""
# Check for name collision
if self.store.get_theme(name) and name != "default":
return {
"error": f"Theme '{name}' already exists. Use theme_extend to modify it.",
"name": name
}
# Start with default tokens and merge provided ones
theme_tokens = copy.deepcopy(DEFAULT_THEME["tokens"])
theme_tokens = self._deep_merge(theme_tokens, tokens)
# Create theme object
theme = {
"name": name,
"version": "1.0.0",
"tokens": theme_tokens
}
# Validate the theme
validation = self._validate_tokens(theme_tokens)
# Save to store
self.store._themes[name] = theme
return {
"name": name,
"tokens": theme_tokens,
"validation": validation,
"complete": validation["complete"]
}
async def theme_extend(
self,
base_theme: str,
overrides: Dict[str, Any],
new_name: Optional[str] = None
) -> Dict[str, Any]:
"""
Create a new theme by extending an existing one.
Args:
base_theme: Name of theme to extend
overrides: Token overrides to apply
new_name: Optional name for the new theme (defaults to base_theme_extended)
Returns:
Dict with the new theme or error
"""
# Get base theme
base = self.store.get_theme(base_theme)
if not base:
available = self.store.list_themes()
return {
"error": f"Base theme '{base_theme}' not found. Available: {available}",
"name": None
}
# Determine new name
name = new_name or f"{base_theme}_extended"
# Check for collision
if self.store.get_theme(name) and name != base_theme:
return {
"error": f"Theme '{name}' already exists. Choose a different name.",
"name": name
}
# Merge tokens
theme_tokens = copy.deepcopy(base.get("tokens", {}))
theme_tokens = self._deep_merge(theme_tokens, overrides)
# Create theme
theme = {
"name": name,
"version": "1.0.0",
"extends": base_theme,
"tokens": theme_tokens
}
# Validate
validation = self._validate_tokens(theme_tokens)
# Save to store
self.store._themes[name] = theme
return {
"name": name,
"extends": base_theme,
"tokens": theme_tokens,
"validation": validation,
"complete": validation["complete"]
}
async def theme_validate(self, theme_name: str) -> Dict[str, Any]:
"""
Validate a theme for completeness.
Args:
theme_name: Theme name to validate
Returns:
Dict with:
- valid: bool
- complete: bool (all optional tokens present)
- missing: List of missing required tokens
- warnings: List of warnings
"""
theme = self.store.get_theme(theme_name)
if not theme:
available = self.store.list_themes()
return {
"error": f"Theme '{theme_name}' not found. Available: {available}",
"valid": False
}
tokens = theme.get("tokens", {})
validation = self._validate_tokens(tokens)
return {
"theme_name": theme_name,
"valid": validation["valid"],
"complete": validation["complete"],
"missing_required": validation["missing_required"],
"missing_optional": validation["missing_optional"],
"warnings": validation["warnings"]
}
async def theme_export_css(self, theme_name: str) -> Dict[str, Any]:
"""
Export a theme as CSS custom properties.
Args:
theme_name: Theme name to export
Returns:
Dict with:
- css: CSS custom properties string
- variables: List of variable names
"""
theme = self.store.get_theme(theme_name)
if not theme:
available = self.store.list_themes()
return {
"error": f"Theme '{theme_name}' not found. Available: {available}",
"css": None
}
tokens = theme.get("tokens", {})
css_vars = []
var_names = []
# Convert tokens to CSS custom properties
css_vars.append(f"/* Theme: {theme_name} */")
css_vars.append(":root {")
# Colors
colors = tokens.get("colors", {})
css_vars.append(" /* Colors */")
for key, value in self._flatten_tokens(colors, "color").items():
var_name = f"--{key}"
css_vars.append(f" {var_name}: {value};")
var_names.append(var_name)
# Spacing
spacing = tokens.get("spacing", {})
css_vars.append("\n /* Spacing */")
for key, value in spacing.items():
var_name = f"--spacing-{key}"
css_vars.append(f" {var_name}: {value};")
var_names.append(var_name)
# Typography
typography = tokens.get("typography", {})
css_vars.append("\n /* Typography */")
for key, value in self._flatten_tokens(typography, "font").items():
var_name = f"--{key}"
css_vars.append(f" {var_name}: {value};")
var_names.append(var_name)
# Radii
radii = tokens.get("radii", {})
css_vars.append("\n /* Border Radius */")
for key, value in radii.items():
var_name = f"--radius-{key}"
css_vars.append(f" {var_name}: {value};")
var_names.append(var_name)
# Shadows
shadows = tokens.get("shadows", {})
if shadows:
css_vars.append("\n /* Shadows */")
for key, value in shadows.items():
var_name = f"--shadow-{key}"
css_vars.append(f" {var_name}: {value};")
var_names.append(var_name)
# Transitions
transitions = tokens.get("transitions", {})
if transitions:
css_vars.append("\n /* Transitions */")
for key, value in transitions.items():
var_name = f"--transition-{key}"
css_vars.append(f" {var_name}: {value};")
var_names.append(var_name)
css_vars.append("}")
css_content = "\n".join(css_vars)
return {
"theme_name": theme_name,
"css": css_content,
"variable_count": len(var_names),
"variables": var_names
}
async def theme_list(self) -> Dict[str, Any]:
"""
List all available themes.
Returns:
Dict with theme names and active theme
"""
themes = self.store.list_themes()
active = self.store._active_theme
theme_info = {}
for name in themes:
theme = self.store.get_theme(name)
theme_info[name] = {
"extends": theme.get("extends"),
"version": theme.get("version", "1.0.0")
}
return {
"themes": theme_info,
"active_theme": active,
"count": len(themes)
}
async def theme_activate(self, theme_name: str) -> Dict[str, Any]:
"""
Set the active theme.
Args:
theme_name: Theme to activate
Returns:
Dict with activation status
"""
if self.store.set_active_theme(theme_name):
return {
"active_theme": theme_name,
"success": True
}
return {
"error": f"Theme '{theme_name}' not found.",
"success": False
}
def _validate_tokens(self, tokens: Dict[str, Any]) -> Dict[str, Any]:
"""Validate token structure and completeness."""
missing_required = []
missing_optional = []
warnings = []
# Check required categories
for category in REQUIRED_TOKEN_CATEGORIES:
if category not in tokens:
missing_required.append(category)
# Check colors structure
colors = tokens.get("colors", {})
required_colors = ["primary", "background", "text"]
for color in required_colors:
if color not in colors:
missing_required.append(f"colors.{color}")
# Check spacing
spacing = tokens.get("spacing", {})
required_spacing = ["xs", "sm", "md", "lg"]
for size in required_spacing:
if size not in spacing:
missing_optional.append(f"spacing.{size}")
# Check typography
typography = tokens.get("typography", {})
if "fontFamily" not in typography:
missing_optional.append("typography.fontFamily")
if "fontSize" not in typography:
missing_optional.append("typography.fontSize")
# Check radii
radii = tokens.get("radii", {})
if "sm" not in radii and "md" not in radii:
missing_optional.append("radii.sm or radii.md")
# Warnings for common issues
if "shadows" not in tokens:
warnings.append("No shadows defined - components may have no elevation")
if "transitions" not in tokens:
warnings.append("No transitions defined - animations will use defaults")
return {
"valid": len(missing_required) == 0,
"complete": len(missing_required) == 0 and len(missing_optional) == 0,
"missing_required": missing_required,
"missing_optional": missing_optional,
"warnings": warnings
}
def _deep_merge(
self,
base: Dict[str, Any],
override: Dict[str, Any]
) -> Dict[str, Any]:
"""Deep merge two dictionaries."""
result = copy.deepcopy(base)
for key, value in override.items():
if key in result and isinstance(result[key], dict) and isinstance(value, dict):
result[key] = self._deep_merge(result[key], value)
else:
result[key] = value
return result
def _flatten_tokens(
self,
tokens: Dict[str, Any],
prefix: str
) -> Dict[str, str]:
"""Flatten nested token dict for CSS export."""
result = {}
for key, value in tokens.items():
if isinstance(value, dict):
nested = self._flatten_tokens(value, f"{prefix}-{key}")
result.update(nested)
else:
result[f"{prefix}-{key}"] = str(value)
return result

View File

@@ -0,0 +1,45 @@
[build-system]
requires = ["setuptools>=61.0", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "viz-platform-mcp"
version = "1.0.0"
description = "MCP Server for visualization with Dash Mantine Components validation and theming"
readme = "README.md"
license = {text = "MIT"}
requires-python = ">=3.10"
authors = [
{name = "Leo Miranda"}
]
classifiers = [
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
"License :: OSI Approved :: MIT License",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
]
dependencies = [
"mcp>=0.9.0",
"plotly>=5.18.0",
"dash>=2.14.0",
"dash-mantine-components>=2.0.0",
"python-dotenv>=1.0.0",
"pydantic>=2.5.0",
]
[project.optional-dependencies]
dev = [
"pytest>=7.4.3",
"pytest-asyncio>=0.23.0",
]
[tool.setuptools.packages.find]
where = ["."]
include = ["mcp_server*"]
[tool.pytest.ini_options]
asyncio_mode = "auto"
testpaths = ["tests"]

View File

@@ -0,0 +1,668 @@
{
"version": "2.5.1",
"generated": "2026-01-26",
"mantine_version": "7.x",
"categories": {
"buttons": ["Button", "ButtonGroup", "ActionIcon", "ActionIconGroup", "CopyButton", "CloseButton", "UnstyledButton"],
"inputs": [
"TextInput", "PasswordInput", "NumberInput", "Textarea", "Select", "MultiSelect",
"Checkbox", "CheckboxGroup", "CheckboxCard", "Switch", "Radio", "RadioGroup", "RadioCard",
"Slider", "RangeSlider", "ColorInput", "ColorPicker", "Autocomplete", "TagsInput",
"PinInput", "Rating", "SegmentedControl", "Chip", "ChipGroup", "JsonInput",
"NativeSelect", "FileInput", "Combobox"
],
"navigation": ["Anchor", "Breadcrumbs", "Burger", "NavLink", "Pagination", "Stepper", "Tabs", "TabsList", "TabsTab", "TabsPanel"],
"feedback": ["Alert", "Loader", "Notification", "NotificationContainer", "Progress", "RingProgress", "Skeleton"],
"overlays": ["Modal", "Drawer", "DrawerStack", "Popover", "HoverCard", "Tooltip", "FloatingTooltip", "Menu", "MenuTarget", "MenuDropdown", "MenuItem", "Affix"],
"typography": ["Text", "Title", "Highlight", "Mark", "Code", "CodeHighlight", "Blockquote", "List", "ListItem", "Kbd"],
"layout": [
"AppShell", "AppShellHeader", "AppShellNavbar", "AppShellAside", "AppShellFooter", "AppShellMain", "AppShellSection",
"Container", "Center", "Stack", "Group", "Flex", "Grid", "GridCol", "SimpleGrid",
"Paper", "Card", "CardSection", "Box", "Space", "Divider", "AspectRatio", "ScrollArea"
],
"data_display": [
"Accordion", "AccordionItem", "AccordionControl", "AccordionPanel",
"Avatar", "AvatarGroup", "Badge", "Image", "BackgroundImage",
"Indicator", "Spoiler", "Table", "ThemeIcon", "Timeline", "TimelineItem", "Tree"
],
"charts": ["AreaChart", "BarChart", "LineChart", "PieChart", "DonutChart", "RadarChart", "ScatterChart", "BubbleChart", "CompositeChart", "Sparkline"],
"dates": ["DatePicker", "DateTimePicker", "DateInput", "DatePickerInput", "MonthPicker", "YearPicker", "TimePicker", "TimeInput", "Calendar", "MiniCalendar", "DatesProvider"]
},
"components": {
"Button": {
"description": "Button component for user interactions",
"props": {
"children": {"type": "any", "description": "Button content"},
"variant": {"type": "string", "enum": ["filled", "light", "outline", "transparent", "white", "subtle", "default", "gradient"], "default": "filled"},
"color": {"type": "string", "default": "blue", "description": "Key of theme.colors or CSS color"},
"size": {"type": "string", "enum": ["xs", "sm", "md", "lg", "xl", "compact-xs", "compact-sm", "compact-md", "compact-lg", "compact-xl"], "default": "sm"},
"radius": {"type": "string", "enum": ["xs", "sm", "md", "lg", "xl"], "default": "sm"},
"disabled": {"type": "boolean", "default": false},
"loading": {"type": "boolean", "default": false},
"loaderProps": {"type": "object"},
"leftSection": {"type": "any", "description": "Content on the left side of label"},
"rightSection": {"type": "any", "description": "Content on the right side of label"},
"fullWidth": {"type": "boolean", "default": false},
"gradient": {"type": "object", "description": "Gradient for gradient variant"},
"justify": {"type": "string", "enum": ["center", "start", "end", "space-between"], "default": "center"},
"autoContrast": {"type": "boolean", "default": false},
"n_clicks": {"type": "integer", "default": 0, "description": "Dash callback trigger"}
}
},
"ActionIcon": {
"description": "Icon button without text label",
"props": {
"children": {"type": "any", "required": true, "description": "Icon element"},
"variant": {"type": "string", "enum": ["filled", "light", "outline", "transparent", "white", "subtle", "default", "gradient"], "default": "subtle"},
"color": {"type": "string", "default": "gray"},
"size": {"type": "string", "enum": ["xs", "sm", "md", "lg", "xl"], "default": "md"},
"radius": {"type": "string", "enum": ["xs", "sm", "md", "lg", "xl"], "default": "sm"},
"disabled": {"type": "boolean", "default": false},
"loading": {"type": "boolean", "default": false},
"autoContrast": {"type": "boolean", "default": false},
"n_clicks": {"type": "integer", "default": 0}
}
},
"TextInput": {
"description": "Text input field",
"props": {
"value": {"type": "string", "default": ""},
"placeholder": {"type": "string"},
"label": {"type": "any"},
"description": {"type": "any"},
"error": {"type": "any"},
"disabled": {"type": "boolean", "default": false},
"required": {"type": "boolean", "default": false},
"size": {"type": "string", "enum": ["xs", "sm", "md", "lg", "xl"], "default": "sm"},
"radius": {"type": "string", "enum": ["xs", "sm", "md", "lg", "xl"], "default": "sm"},
"variant": {"type": "string", "enum": ["default", "filled", "unstyled"], "default": "default"},
"leftSection": {"type": "any"},
"rightSection": {"type": "any"},
"withAsterisk": {"type": "boolean", "default": false},
"debounce": {"type": "integer", "description": "Debounce delay in ms"},
"leftSectionPointerEvents": {"type": "string", "enum": ["none", "all"], "default": "none"},
"rightSectionPointerEvents": {"type": "string", "enum": ["none", "all"], "default": "none"}
}
},
"NumberInput": {
"description": "Numeric input with optional controls",
"props": {
"value": {"type": "number"},
"placeholder": {"type": "string"},
"label": {"type": "any"},
"description": {"type": "any"},
"error": {"type": "any"},
"disabled": {"type": "boolean", "default": false},
"required": {"type": "boolean", "default": false},
"min": {"type": "number"},
"max": {"type": "number"},
"step": {"type": "number", "default": 1},
"hideControls": {"type": "boolean", "default": false},
"size": {"type": "string", "enum": ["xs", "sm", "md", "lg", "xl"], "default": "sm"},
"radius": {"type": "string", "enum": ["xs", "sm", "md", "lg", "xl"], "default": "sm"},
"allowNegative": {"type": "boolean", "default": true},
"allowDecimal": {"type": "boolean", "default": true},
"clampBehavior": {"type": "string", "enum": ["strict", "blur", "none"], "default": "blur"},
"decimalScale": {"type": "integer"},
"fixedDecimalScale": {"type": "boolean", "default": false},
"thousandSeparator": {"type": "string"},
"decimalSeparator": {"type": "string"},
"prefix": {"type": "string"},
"suffix": {"type": "string"}
}
},
"Select": {
"description": "Dropdown select input",
"props": {
"value": {"type": "string"},
"data": {"type": "array", "required": true, "description": "Array of options: strings or {value, label} objects"},
"placeholder": {"type": "string"},
"label": {"type": "any"},
"description": {"type": "any"},
"error": {"type": "any"},
"disabled": {"type": "boolean", "default": false},
"required": {"type": "boolean", "default": false},
"searchable": {"type": "boolean", "default": false},
"clearable": {"type": "boolean", "default": false},
"nothingFoundMessage": {"type": "string"},
"size": {"type": "string", "enum": ["xs", "sm", "md", "lg", "xl"], "default": "sm"},
"radius": {"type": "string", "enum": ["xs", "sm", "md", "lg", "xl"], "default": "sm"},
"maxDropdownHeight": {"type": "number", "default": 250},
"allowDeselect": {"type": "boolean", "default": true},
"checkIconPosition": {"type": "string", "enum": ["left", "right"], "default": "left"},
"comboboxProps": {"type": "object"},
"withScrollArea": {"type": "boolean", "default": true}
}
},
"MultiSelect": {
"description": "Multiple selection dropdown",
"props": {
"value": {"type": "array", "default": []},
"data": {"type": "array", "required": true},
"placeholder": {"type": "string"},
"label": {"type": "any"},
"description": {"type": "any"},
"error": {"type": "any"},
"disabled": {"type": "boolean", "default": false},
"required": {"type": "boolean", "default": false},
"searchable": {"type": "boolean", "default": false},
"clearable": {"type": "boolean", "default": false},
"maxValues": {"type": "integer"},
"hidePickedOptions": {"type": "boolean", "default": false},
"size": {"type": "string", "enum": ["xs", "sm", "md", "lg", "xl"], "default": "sm"},
"radius": {"type": "string", "enum": ["xs", "sm", "md", "lg", "xl"], "default": "sm"},
"maxDropdownHeight": {"type": "number", "default": 250},
"withCheckIcon": {"type": "boolean", "default": true}
}
},
"Checkbox": {
"description": "Checkbox input",
"props": {
"checked": {"type": "boolean", "default": false},
"label": {"type": "any"},
"description": {"type": "any"},
"error": {"type": "any"},
"disabled": {"type": "boolean", "default": false},
"indeterminate": {"type": "boolean", "default": false},
"size": {"type": "string", "enum": ["xs", "sm", "md", "lg", "xl"], "default": "sm"},
"radius": {"type": "string", "enum": ["xs", "sm", "md", "lg", "xl"], "default": "sm"},
"color": {"type": "string", "default": "blue"},
"labelPosition": {"type": "string", "enum": ["left", "right"], "default": "right"},
"autoContrast": {"type": "boolean", "default": false},
"icon": {"type": "any"},
"iconColor": {"type": "string"}
}
},
"Switch": {
"description": "Toggle switch input",
"props": {
"checked": {"type": "boolean", "default": false},
"label": {"type": "any"},
"description": {"type": "any"},
"error": {"type": "any"},
"disabled": {"type": "boolean", "default": false},
"size": {"type": "string", "enum": ["xs", "sm", "md", "lg", "xl"], "default": "sm"},
"radius": {"type": "string", "enum": ["xs", "sm", "md", "lg", "xl"], "default": "xl"},
"color": {"type": "string", "default": "blue"},
"onLabel": {"type": "any"},
"offLabel": {"type": "any"},
"thumbIcon": {"type": "any"},
"labelPosition": {"type": "string", "enum": ["left", "right"], "default": "right"}
}
},
"Slider": {
"description": "Slider input for numeric values",
"props": {
"value": {"type": "number"},
"min": {"type": "number", "default": 0},
"max": {"type": "number", "default": 100},
"step": {"type": "number", "default": 1},
"label": {"type": "any"},
"disabled": {"type": "boolean", "default": false},
"marks": {"type": "array"},
"size": {"type": "string", "enum": ["xs", "sm", "md", "lg", "xl"], "default": "md"},
"radius": {"type": "string", "enum": ["xs", "sm", "md", "lg", "xl"], "default": "xl"},
"color": {"type": "string", "default": "blue"},
"showLabelOnHover": {"type": "boolean", "default": true},
"labelAlwaysOn": {"type": "boolean", "default": false},
"thumbLabel": {"type": "string"},
"precision": {"type": "integer", "default": 0},
"inverted": {"type": "boolean", "default": false},
"thumbSize": {"type": "number"},
"restrictToMarks": {"type": "boolean", "default": false}
}
},
"Alert": {
"description": "Alert component for feedback messages",
"props": {
"children": {"type": "any"},
"title": {"type": "any"},
"color": {"type": "string", "default": "blue"},
"variant": {"type": "string", "enum": ["filled", "light", "outline", "default", "transparent", "white"], "default": "light"},
"radius": {"type": "string", "enum": ["xs", "sm", "md", "lg", "xl"], "default": "sm"},
"icon": {"type": "any"},
"withCloseButton": {"type": "boolean", "default": false},
"closeButtonLabel": {"type": "string"},
"autoContrast": {"type": "boolean", "default": false}
}
},
"Loader": {
"description": "Loading indicator",
"props": {
"color": {"type": "string", "default": "blue"},
"size": {"type": "string", "enum": ["xs", "sm", "md", "lg", "xl"], "default": "md"},
"type": {"type": "string", "enum": ["oval", "bars", "dots"], "default": "oval"}
}
},
"Progress": {
"description": "Progress bar",
"props": {
"value": {"type": "number", "required": true},
"color": {"type": "string", "default": "blue"},
"size": {"type": "string", "enum": ["xs", "sm", "md", "lg", "xl"], "default": "md"},
"radius": {"type": "string", "enum": ["xs", "sm", "md", "lg", "xl"], "default": "sm"},
"striped": {"type": "boolean", "default": false},
"animated": {"type": "boolean", "default": false},
"autoContrast": {"type": "boolean", "default": false},
"transitionDuration": {"type": "number", "default": 100}
}
},
"Modal": {
"description": "Modal dialog overlay",
"props": {
"children": {"type": "any"},
"opened": {"type": "boolean", "required": true},
"title": {"type": "any"},
"size": {"type": "string", "enum": ["xs", "sm", "md", "lg", "xl", "auto"], "default": "md"},
"radius": {"type": "string", "enum": ["xs", "sm", "md", "lg", "xl"], "default": "sm"},
"centered": {"type": "boolean", "default": false},
"fullScreen": {"type": "boolean", "default": false},
"withCloseButton": {"type": "boolean", "default": true},
"closeOnClickOutside": {"type": "boolean", "default": true},
"closeOnEscape": {"type": "boolean", "default": true},
"overlayProps": {"type": "object"},
"padding": {"type": "string", "enum": ["xs", "sm", "md", "lg", "xl"], "default": "md"},
"transitionProps": {"type": "object"},
"zIndex": {"type": "number", "default": 200},
"trapFocus": {"type": "boolean", "default": true},
"returnFocus": {"type": "boolean", "default": true},
"lockScroll": {"type": "boolean", "default": true}
}
},
"Drawer": {
"description": "Sliding panel drawer",
"props": {
"children": {"type": "any"},
"opened": {"type": "boolean", "required": true},
"title": {"type": "any"},
"position": {"type": "string", "enum": ["left", "right", "top", "bottom"], "default": "left"},
"size": {"type": "string", "enum": ["xs", "sm", "md", "lg", "xl"], "default": "md"},
"radius": {"type": "string", "enum": ["xs", "sm", "md", "lg", "xl"]},
"withCloseButton": {"type": "boolean", "default": true},
"closeOnClickOutside": {"type": "boolean", "default": true},
"closeOnEscape": {"type": "boolean", "default": true},
"overlayProps": {"type": "object"},
"padding": {"type": "string", "enum": ["xs", "sm", "md", "lg", "xl"], "default": "md"},
"zIndex": {"type": "number", "default": 200},
"offset": {"type": "number", "default": 0},
"trapFocus": {"type": "boolean", "default": true},
"returnFocus": {"type": "boolean", "default": true},
"lockScroll": {"type": "boolean", "default": true}
}
},
"Tooltip": {
"description": "Tooltip on hover",
"props": {
"children": {"type": "any", "required": true},
"label": {"type": "any", "required": true},
"position": {"type": "string", "enum": ["top", "right", "bottom", "left", "top-start", "top-end", "right-start", "right-end", "bottom-start", "bottom-end", "left-start", "left-end"], "default": "top"},
"color": {"type": "string"},
"radius": {"type": "string", "enum": ["xs", "sm", "md", "lg", "xl"], "default": "sm"},
"withArrow": {"type": "boolean", "default": false},
"arrowSize": {"type": "number", "default": 4},
"arrowOffset": {"type": "number", "default": 5},
"offset": {"type": "number", "default": 5},
"multiline": {"type": "boolean", "default": false},
"disabled": {"type": "boolean", "default": false},
"openDelay": {"type": "number", "default": 0},
"closeDelay": {"type": "number", "default": 0},
"transitionProps": {"type": "object"},
"zIndex": {"type": "number", "default": 300}
}
},
"Text": {
"description": "Text component with styling",
"props": {
"children": {"type": "any"},
"size": {"type": "string", "enum": ["xs", "sm", "md", "lg", "xl"]},
"c": {"type": "string", "description": "Color"},
"fw": {"type": "number", "description": "Font weight"},
"fs": {"type": "string", "enum": ["normal", "italic"], "description": "Font style"},
"td": {"type": "string", "enum": ["none", "underline", "line-through"], "description": "Text decoration"},
"tt": {"type": "string", "enum": ["none", "capitalize", "uppercase", "lowercase"], "description": "Text transform"},
"ta": {"type": "string", "enum": ["left", "center", "right", "justify"], "description": "Text align"},
"lineClamp": {"type": "integer"},
"truncate": {"type": "boolean", "default": false},
"inherit": {"type": "boolean", "default": false},
"gradient": {"type": "object"},
"span": {"type": "boolean", "default": false},
"lh": {"type": "string", "description": "Line height"}
}
},
"Title": {
"description": "Heading component",
"props": {
"children": {"type": "any"},
"order": {"type": "integer", "enum": [1, 2, 3, 4, 5, 6], "default": 1},
"size": {"type": "string"},
"c": {"type": "string", "description": "Color"},
"ta": {"type": "string", "enum": ["left", "center", "right", "justify"]},
"td": {"type": "string", "enum": ["none", "underline", "line-through"]},
"tt": {"type": "string", "enum": ["none", "capitalize", "uppercase", "lowercase"]},
"lineClamp": {"type": "integer"},
"truncate": {"type": "boolean", "default": false}
}
},
"Stack": {
"description": "Vertical stack layout",
"props": {
"children": {"type": "any"},
"gap": {"type": "string", "enum": ["xs", "sm", "md", "lg", "xl"], "default": "md"},
"align": {"type": "string", "enum": ["stretch", "center", "flex-start", "flex-end"], "default": "stretch"},
"justify": {"type": "string", "enum": ["flex-start", "flex-end", "center", "space-between", "space-around", "space-evenly"], "default": "flex-start"}
}
},
"Group": {
"description": "Horizontal group layout",
"props": {
"children": {"type": "any"},
"gap": {"type": "string", "enum": ["xs", "sm", "md", "lg", "xl"], "default": "md"},
"align": {"type": "string", "enum": ["stretch", "center", "flex-start", "flex-end"], "default": "center"},
"justify": {"type": "string", "enum": ["flex-start", "flex-end", "center", "space-between", "space-around"], "default": "flex-start"},
"grow": {"type": "boolean", "default": false},
"wrap": {"type": "string", "enum": ["wrap", "nowrap", "wrap-reverse"], "default": "wrap"},
"preventGrowOverflow": {"type": "boolean", "default": true}
}
},
"Flex": {
"description": "Flexbox container",
"props": {
"children": {"type": "any"},
"gap": {"type": "string", "enum": ["xs", "sm", "md", "lg", "xl"]},
"rowGap": {"type": "string", "enum": ["xs", "sm", "md", "lg", "xl"]},
"columnGap": {"type": "string", "enum": ["xs", "sm", "md", "lg", "xl"]},
"align": {"type": "string", "enum": ["stretch", "center", "flex-start", "flex-end", "baseline"]},
"justify": {"type": "string", "enum": ["flex-start", "flex-end", "center", "space-between", "space-around", "space-evenly"]},
"wrap": {"type": "string", "enum": ["wrap", "nowrap", "wrap-reverse"], "default": "nowrap"},
"direction": {"type": "string", "enum": ["row", "column", "row-reverse", "column-reverse"], "default": "row"}
}
},
"Grid": {
"description": "Grid layout component",
"props": {
"children": {"type": "any"},
"columns": {"type": "integer", "default": 12},
"gutter": {"type": "string", "enum": ["xs", "sm", "md", "lg", "xl"], "default": "md"},
"grow": {"type": "boolean", "default": false},
"justify": {"type": "string", "enum": ["flex-start", "flex-end", "center", "space-between", "space-around"], "default": "flex-start"},
"align": {"type": "string", "enum": ["stretch", "center", "flex-start", "flex-end"], "default": "stretch"},
"overflow": {"type": "string", "enum": ["visible", "hidden"], "default": "visible"}
}
},
"SimpleGrid": {
"description": "Simple grid with equal columns",
"props": {
"children": {"type": "any"},
"cols": {"type": "integer", "default": 1},
"spacing": {"type": "string", "enum": ["xs", "sm", "md", "lg", "xl"], "default": "md"},
"verticalSpacing": {"type": "string", "enum": ["xs", "sm", "md", "lg", "xl"]}
}
},
"Container": {
"description": "Centered container with max-width",
"props": {
"children": {"type": "any"},
"size": {"type": "string", "enum": ["xs", "sm", "md", "lg", "xl"], "default": "md"},
"fluid": {"type": "boolean", "default": false}
}
},
"Paper": {
"description": "Paper surface component",
"props": {
"children": {"type": "any"},
"shadow": {"type": "string", "enum": ["xs", "sm", "md", "lg", "xl"]},
"radius": {"type": "string", "enum": ["xs", "sm", "md", "lg", "xl"], "default": "sm"},
"p": {"type": "string", "enum": ["xs", "sm", "md", "lg", "xl"], "description": "Padding"},
"withBorder": {"type": "boolean", "default": false}
}
},
"Card": {
"description": "Card container",
"props": {
"children": {"type": "any"},
"shadow": {"type": "string", "enum": ["xs", "sm", "md", "lg", "xl"], "default": "sm"},
"radius": {"type": "string", "enum": ["xs", "sm", "md", "lg", "xl"], "default": "sm"},
"padding": {"type": "string", "enum": ["xs", "sm", "md", "lg", "xl"], "default": "md"},
"withBorder": {"type": "boolean", "default": false}
}
},
"Tabs": {
"description": "Tabbed interface",
"props": {
"children": {"type": "any"},
"value": {"type": "string"},
"defaultValue": {"type": "string"},
"orientation": {"type": "string", "enum": ["horizontal", "vertical"], "default": "horizontal"},
"variant": {"type": "string", "enum": ["default", "outline", "pills"], "default": "default"},
"color": {"type": "string", "default": "blue"},
"radius": {"type": "string", "enum": ["xs", "sm", "md", "lg", "xl"], "default": "sm"},
"placement": {"type": "string", "enum": ["left", "right"], "default": "left"},
"grow": {"type": "boolean", "default": false},
"inverted": {"type": "boolean", "default": false},
"keepMounted": {"type": "boolean", "default": true},
"activateTabWithKeyboard": {"type": "boolean", "default": true},
"allowTabDeactivation": {"type": "boolean", "default": false},
"autoContrast": {"type": "boolean", "default": false}
}
},
"Accordion": {
"description": "Collapsible content panels",
"props": {
"children": {"type": "any"},
"value": {"type": "any"},
"defaultValue": {"type": "any"},
"multiple": {"type": "boolean", "default": false},
"variant": {"type": "string", "enum": ["default", "contained", "filled", "separated"], "default": "default"},
"radius": {"type": "string", "enum": ["xs", "sm", "md", "lg", "xl"], "default": "sm"},
"chevronPosition": {"type": "string", "enum": ["left", "right"], "default": "right"},
"disableChevronRotation": {"type": "boolean", "default": false},
"transitionDuration": {"type": "number", "default": 200},
"chevronSize": {"type": "any"},
"order": {"type": "integer", "enum": [2, 3, 4, 5, 6]}
}
},
"Badge": {
"description": "Badge for status or labels",
"props": {
"children": {"type": "any"},
"color": {"type": "string", "default": "blue"},
"variant": {"type": "string", "enum": ["filled", "light", "outline", "dot", "gradient", "default", "transparent", "white"], "default": "filled"},
"size": {"type": "string", "enum": ["xs", "sm", "md", "lg", "xl"], "default": "md"},
"radius": {"type": "string", "enum": ["xs", "sm", "md", "lg", "xl"], "default": "xl"},
"fullWidth": {"type": "boolean", "default": false},
"leftSection": {"type": "any"},
"rightSection": {"type": "any"},
"autoContrast": {"type": "boolean", "default": false},
"circle": {"type": "boolean", "default": false}
}
},
"Avatar": {
"description": "User avatar image",
"props": {
"src": {"type": "string"},
"alt": {"type": "string"},
"children": {"type": "any", "description": "Fallback content"},
"color": {"type": "string", "default": "gray"},
"size": {"type": "string", "enum": ["xs", "sm", "md", "lg", "xl"], "default": "md"},
"radius": {"type": "string", "enum": ["xs", "sm", "md", "lg", "xl"], "default": "xl"},
"variant": {"type": "string", "enum": ["filled", "light", "outline", "gradient", "default", "transparent", "white"], "default": "filled"},
"autoContrast": {"type": "boolean", "default": false}
}
},
"Image": {
"description": "Image with fallback",
"props": {
"src": {"type": "string"},
"alt": {"type": "string"},
"w": {"type": "any", "description": "Width"},
"h": {"type": "any", "description": "Height"},
"radius": {"type": "string", "enum": ["xs", "sm", "md", "lg", "xl"]},
"fit": {"type": "string", "enum": ["contain", "cover", "fill", "none", "scale-down"], "default": "cover"},
"fallbackSrc": {"type": "string"}
}
},
"Table": {
"description": "Data table component",
"props": {
"children": {"type": "any"},
"data": {"type": "object", "description": "Table data object with head, body, foot"},
"striped": {"type": "boolean", "default": false},
"highlightOnHover": {"type": "boolean", "default": false},
"withTableBorder": {"type": "boolean", "default": false},
"withColumnBorders": {"type": "boolean", "default": false},
"withRowBorders": {"type": "boolean", "default": true},
"verticalSpacing": {"type": "string", "enum": ["xs", "sm", "md", "lg", "xl"], "default": "xs"},
"horizontalSpacing": {"type": "string", "enum": ["xs", "sm", "md", "lg", "xl"], "default": "xs"},
"captionSide": {"type": "string", "enum": ["top", "bottom"], "default": "bottom"},
"stickyHeader": {"type": "boolean", "default": false},
"stickyHeaderOffset": {"type": "number", "default": 0}
}
},
"AreaChart": {
"description": "Area chart for time series data",
"props": {
"data": {"type": "array", "required": true},
"dataKey": {"type": "string", "required": true, "description": "X-axis data key"},
"series": {"type": "array", "required": true, "description": "Array of {name, color} objects"},
"h": {"type": "any", "description": "Chart height"},
"w": {"type": "any", "description": "Chart width"},
"curveType": {"type": "string", "enum": ["bump", "linear", "natural", "monotone", "step", "stepBefore", "stepAfter"], "default": "monotone"},
"connectNulls": {"type": "boolean", "default": true},
"withDots": {"type": "boolean", "default": true},
"withGradient": {"type": "boolean", "default": true},
"withLegend": {"type": "boolean", "default": false},
"withTooltip": {"type": "boolean", "default": true},
"withXAxis": {"type": "boolean", "default": true},
"withYAxis": {"type": "boolean", "default": true},
"gridAxis": {"type": "string", "enum": ["x", "y", "xy", "none"], "default": "x"},
"tickLine": {"type": "string", "enum": ["x", "y", "xy", "none"], "default": "y"},
"strokeDasharray": {"type": "string"},
"fillOpacity": {"type": "number", "default": 0.2},
"splitColors": {"type": "array"},
"areaChartProps": {"type": "object"},
"type": {"type": "string", "enum": ["default", "stacked", "percent", "split"], "default": "default"}
}
},
"BarChart": {
"description": "Bar chart for categorical data",
"props": {
"data": {"type": "array", "required": true},
"dataKey": {"type": "string", "required": true},
"series": {"type": "array", "required": true},
"h": {"type": "any"},
"w": {"type": "any"},
"orientation": {"type": "string", "enum": ["horizontal", "vertical"], "default": "vertical"},
"withLegend": {"type": "boolean", "default": false},
"withTooltip": {"type": "boolean", "default": true},
"withXAxis": {"type": "boolean", "default": true},
"withYAxis": {"type": "boolean", "default": true},
"gridAxis": {"type": "string", "enum": ["x", "y", "xy", "none"], "default": "x"},
"tickLine": {"type": "string", "enum": ["x", "y", "xy", "none"], "default": "y"},
"barProps": {"type": "object"},
"type": {"type": "string", "enum": ["default", "stacked", "percent", "waterfall"], "default": "default"}
}
},
"LineChart": {
"description": "Line chart for trends",
"props": {
"data": {"type": "array", "required": true},
"dataKey": {"type": "string", "required": true},
"series": {"type": "array", "required": true},
"h": {"type": "any"},
"w": {"type": "any"},
"curveType": {"type": "string", "enum": ["bump", "linear", "natural", "monotone", "step", "stepBefore", "stepAfter"], "default": "monotone"},
"connectNulls": {"type": "boolean", "default": true},
"withDots": {"type": "boolean", "default": true},
"withLegend": {"type": "boolean", "default": false},
"withTooltip": {"type": "boolean", "default": true},
"withXAxis": {"type": "boolean", "default": true},
"withYAxis": {"type": "boolean", "default": true},
"gridAxis": {"type": "string", "enum": ["x", "y", "xy", "none"], "default": "x"},
"strokeWidth": {"type": "number", "default": 2}
}
},
"PieChart": {
"description": "Pie chart for proportions",
"props": {
"data": {"type": "array", "required": true, "description": "Array of {name, value, color} objects"},
"h": {"type": "any"},
"w": {"type": "any"},
"withLabels": {"type": "boolean", "default": false},
"withLabelsLine": {"type": "boolean", "default": true},
"withTooltip": {"type": "boolean", "default": true},
"labelsPosition": {"type": "string", "enum": ["inside", "outside"], "default": "outside"},
"labelsType": {"type": "string", "enum": ["value", "percent"], "default": "value"},
"strokeWidth": {"type": "number", "default": 1},
"strokeColor": {"type": "string"},
"startAngle": {"type": "number", "default": 0},
"endAngle": {"type": "number", "default": 360}
}
},
"DonutChart": {
"description": "Donut chart (pie with hole)",
"props": {
"data": {"type": "array", "required": true},
"h": {"type": "any"},
"w": {"type": "any"},
"withLabels": {"type": "boolean", "default": false},
"withLabelsLine": {"type": "boolean", "default": true},
"withTooltip": {"type": "boolean", "default": true},
"thickness": {"type": "number", "default": 20},
"chartLabel": {"type": "any"},
"strokeWidth": {"type": "number", "default": 1},
"strokeColor": {"type": "string"},
"startAngle": {"type": "number", "default": 0},
"endAngle": {"type": "number", "default": 360},
"paddingAngle": {"type": "number", "default": 0}
}
},
"DatePicker": {
"description": "Date picker calendar",
"props": {
"value": {"type": "string"},
"type": {"type": "string", "enum": ["default", "range", "multiple"], "default": "default"},
"defaultValue": {"type": "any"},
"allowDeselect": {"type": "boolean", "default": false},
"allowSingleDateInRange": {"type": "boolean", "default": false},
"numberOfColumns": {"type": "integer", "default": 1},
"columnsToScroll": {"type": "integer", "default": 1},
"ariaLabels": {"type": "object"},
"hideOutsideDates": {"type": "boolean", "default": false},
"hideWeekdays": {"type": "boolean", "default": false},
"weekendDays": {"type": "array", "default": [0, 6]},
"renderDay": {"type": "any"},
"minDate": {"type": "string"},
"maxDate": {"type": "string"},
"size": {"type": "string", "enum": ["xs", "sm", "md", "lg", "xl"], "default": "sm"}
}
},
"DatePickerInput": {
"description": "Date picker input field",
"props": {
"value": {"type": "string"},
"label": {"type": "any"},
"description": {"type": "any"},
"error": {"type": "any"},
"placeholder": {"type": "string"},
"clearable": {"type": "boolean", "default": false},
"type": {"type": "string", "enum": ["default", "range", "multiple"], "default": "default"},
"valueFormat": {"type": "string", "default": "MMMM D, YYYY"},
"size": {"type": "string", "enum": ["xs", "sm", "md", "lg", "xl"], "default": "sm"},
"radius": {"type": "string", "enum": ["xs", "sm", "md", "lg", "xl"], "default": "sm"},
"disabled": {"type": "boolean", "default": false},
"required": {"type": "boolean", "default": false},
"minDate": {"type": "string"},
"maxDate": {"type": "string"},
"popoverProps": {"type": "object"},
"dropdownType": {"type": "string", "enum": ["popover", "modal"], "default": "popover"}
}
},
"DatesProvider": {
"description": "Provider for date localization settings",
"props": {
"children": {"type": "any", "required": true},
"settings": {"type": "object", "description": "Locale and formatting settings"}
}
}
}
}

View File

@@ -0,0 +1,15 @@
# MCP SDK
mcp>=0.9.0
# Visualization
plotly>=5.18.0
dash>=2.14.0
dash-mantine-components>=2.0.0
# Utilities
python-dotenv>=1.0.0
pydantic>=2.5.0
# Testing
pytest>=7.4.3
pytest-asyncio>=0.23.0

View File

@@ -0,0 +1,262 @@
#!/usr/bin/env python3
"""
Generate DMC Component Registry from installed dash-mantine-components package.
This script introspects the installed DMC package and generates a JSON registry
file containing component definitions, props, types, and defaults.
Usage:
python generate-dmc-registry.py [--output registry/dmc_X_Y.json]
Requirements:
- dash-mantine-components must be installed
- Run from the mcp-servers/viz-platform directory
"""
import argparse
import inspect
import json
import sys
from datetime import date
from pathlib import Path
from typing import Any, Dict, List, Optional, get_type_hints
def get_dmc_version() -> Optional[str]:
"""Get installed DMC version."""
try:
from importlib.metadata import version
return version('dash-mantine-components')
except Exception:
return None
def get_component_categories() -> Dict[str, List[str]]:
"""Define component categories."""
return {
"buttons": ["Button", "ActionIcon", "CopyButton", "FileButton", "UnstyledButton"],
"inputs": [
"TextInput", "PasswordInput", "NumberInput", "Textarea",
"Select", "MultiSelect", "Checkbox", "Switch", "Radio",
"Slider", "RangeSlider", "ColorInput", "ColorPicker",
"DateInput", "DatePicker", "TimeInput"
],
"navigation": ["Anchor", "Breadcrumbs", "Burger", "NavLink", "Pagination", "Stepper", "Tabs"],
"feedback": ["Alert", "Loader", "Notification", "Progress", "RingProgress", "Skeleton"],
"overlays": ["Dialog", "Drawer", "HoverCard", "Menu", "Modal", "Popover", "Tooltip"],
"typography": ["Blockquote", "Code", "Highlight", "Mark", "Text", "Title"],
"layout": [
"AppShell", "AspectRatio", "Center", "Container", "Flex",
"Grid", "Group", "Paper", "SimpleGrid", "Space", "Stack"
],
"data": [
"Accordion", "Avatar", "Badge", "Card", "Image",
"Indicator", "Kbd", "Spoiler", "Table", "ThemeIcon", "Timeline"
]
}
def extract_prop_type(prop_info: Dict[str, Any]) -> Dict[str, Any]:
"""Extract prop type information from Dash component prop."""
result = {"type": "any"}
if 'type' not in prop_info:
return result
prop_type = prop_info['type']
if isinstance(prop_type, dict):
type_name = prop_type.get('name', 'any')
# Map Dash types to JSON schema types
type_mapping = {
'string': 'string',
'number': 'number',
'bool': 'boolean',
'boolean': 'boolean',
'array': 'array',
'object': 'object',
'node': 'any',
'element': 'any',
'any': 'any',
'func': 'any',
}
result['type'] = type_mapping.get(type_name, 'any')
# Handle enums
if type_name == 'enum' and 'value' in prop_type:
values = prop_type['value']
if isinstance(values, list):
enum_values = []
for v in values:
if isinstance(v, dict) and 'value' in v:
# Remove quotes from string values
val = v['value'].strip("'\"")
enum_values.append(val)
elif isinstance(v, str):
enum_values.append(v.strip("'\""))
if enum_values:
result['enum'] = enum_values
result['type'] = 'string'
# Handle union types
elif type_name == 'union' and 'value' in prop_type:
# For unions, just mark as any for simplicity
result['type'] = 'any'
elif isinstance(prop_type, str):
result['type'] = prop_type
return result
def extract_component_props(component_class) -> Dict[str, Any]:
"""Extract props from a Dash component class."""
props = {}
# Try to get _prop_names or similar
if hasattr(component_class, '_prop_names'):
prop_names = component_class._prop_names
else:
prop_names = []
# Try to get _type attribute for prop definitions
if hasattr(component_class, '_type'):
prop_types = getattr(component_class, '_type', {})
else:
prop_types = {}
# Get default values
if hasattr(component_class, '_default_props'):
defaults = component_class._default_props
else:
defaults = {}
# Try to extract from _prop_descriptions
if hasattr(component_class, '_prop_descriptions'):
descriptions = component_class._prop_descriptions
else:
descriptions = {}
for prop_name in prop_names:
if prop_name.startswith('_'):
continue
prop_info = {}
# Get type info if available
if prop_name in prop_types:
prop_info = extract_prop_type({'type': prop_types[prop_name]})
else:
prop_info = {'type': 'any'}
# Add default if exists
if prop_name in defaults:
prop_info['default'] = defaults[prop_name]
# Add description if exists
if prop_name in descriptions:
prop_info['description'] = descriptions[prop_name]
props[prop_name] = prop_info
return props
def generate_registry() -> Dict[str, Any]:
"""Generate the component registry from installed DMC."""
try:
import dash_mantine_components as dmc
except ImportError:
print("ERROR: dash-mantine-components not installed")
print("Install with: pip install dash-mantine-components")
sys.exit(1)
version = get_dmc_version()
categories = get_component_categories()
registry = {
"version": version,
"generated": date.today().isoformat(),
"categories": categories,
"components": {}
}
# Get all components from categories
all_components = set()
for comp_list in categories.values():
all_components.update(comp_list)
# Extract props for each component
for comp_name in sorted(all_components):
if hasattr(dmc, comp_name):
comp_class = getattr(dmc, comp_name)
try:
props = extract_component_props(comp_class)
if props:
registry["components"][comp_name] = {
"description": comp_class.__doc__ or f"{comp_name} component",
"props": props
}
print(f" Extracted: {comp_name} ({len(props)} props)")
except Exception as e:
print(f" Warning: Failed to extract {comp_name}: {e}")
else:
print(f" Warning: Component not found: {comp_name}")
return registry
def main():
parser = argparse.ArgumentParser(
description="Generate DMC component registry from installed package"
)
parser.add_argument(
'--output', '-o',
type=str,
help='Output file path (default: auto-generated based on version)'
)
parser.add_argument(
'--dry-run',
action='store_true',
help='Print to stdout instead of writing file'
)
args = parser.parse_args()
print("Generating DMC Component Registry...")
print("=" * 50)
registry = generate_registry()
print("=" * 50)
print(f"Generated registry for DMC {registry['version']}")
print(f"Total components: {len(registry['components'])}")
if args.dry_run:
print(json.dumps(registry, indent=2))
return
# Determine output path
if args.output:
output_path = Path(args.output)
else:
version = registry['version']
if version:
major_minor = '_'.join(version.split('.')[:2])
output_path = Path(__file__).parent.parent / 'registry' / f'dmc_{major_minor}.json'
else:
output_path = Path(__file__).parent.parent / 'registry' / 'dmc_unknown.json'
# Create directory if needed
output_path.parent.mkdir(parents=True, exist_ok=True)
# Write registry
with open(output_path, 'w') as f:
json.dump(registry, indent=2, fp=f)
print(f"Registry written to: {output_path}")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1 @@
"""viz-platform MCP Server tests."""

View File

@@ -0,0 +1,271 @@
"""
Unit tests for chart creation tools.
"""
import pytest
from unittest.mock import MagicMock, patch
@pytest.fixture
def chart_tools():
"""Create ChartTools instance"""
from mcp_server.chart_tools import ChartTools
return ChartTools()
@pytest.fixture
def chart_tools_with_theme():
"""Create ChartTools instance with a theme"""
from mcp_server.chart_tools import ChartTools
tools = ChartTools()
tools.set_theme({
"colors": {
"primary": "#ff0000",
"secondary": "#00ff00",
"success": "#0000ff"
}
})
return tools
def test_chart_tools_init():
"""Test chart tools initialization"""
from mcp_server.chart_tools import ChartTools
tools = ChartTools()
assert tools.theme_store is None
assert tools._active_theme is None
def test_set_theme(chart_tools):
"""Test setting active theme"""
theme = {"colors": {"primary": "#123456"}}
chart_tools.set_theme(theme)
assert chart_tools._active_theme == theme
def test_get_color_palette_default(chart_tools):
"""Test getting default color palette"""
from mcp_server.chart_tools import DEFAULT_COLORS
palette = chart_tools._get_color_palette()
assert palette == DEFAULT_COLORS
def test_get_color_palette_with_theme(chart_tools_with_theme):
"""Test getting color palette from theme"""
palette = chart_tools_with_theme._get_color_palette()
# Should start with theme colors
assert palette[0] == "#ff0000"
assert palette[1] == "#00ff00"
assert palette[2] == "#0000ff"
def test_resolve_color_from_theme(chart_tools_with_theme):
"""Test resolving color token from theme"""
color = chart_tools_with_theme._resolve_color("primary")
assert color == "#ff0000"
def test_resolve_color_hex(chart_tools):
"""Test resolving hex color"""
color = chart_tools._resolve_color("#abcdef")
assert color == "#abcdef"
def test_resolve_color_rgb(chart_tools):
"""Test resolving rgb color"""
color = chart_tools._resolve_color("rgb(255, 0, 0)")
assert color == "rgb(255, 0, 0)"
def test_resolve_color_named(chart_tools):
"""Test resolving named color"""
color = chart_tools._resolve_color("blue")
assert color == "#228be6" # DEFAULT_COLORS[0]
def test_resolve_color_none(chart_tools):
"""Test resolving None color defaults to first palette color"""
from mcp_server.chart_tools import DEFAULT_COLORS
color = chart_tools._resolve_color(None)
assert color == DEFAULT_COLORS[0]
@pytest.mark.asyncio
async def test_chart_create_line(chart_tools):
"""Test creating a line chart"""
data = {
"x": [1, 2, 3, 4, 5],
"y": [10, 20, 15, 25, 30]
}
result = await chart_tools.chart_create("line", data)
assert "figure" in result
assert result["chart_type"] == "line"
assert "error" not in result or result["error"] is None
@pytest.mark.asyncio
async def test_chart_create_bar(chart_tools):
"""Test creating a bar chart"""
data = {
"x": ["A", "B", "C"],
"y": [10, 20, 15]
}
result = await chart_tools.chart_create("bar", data)
assert "figure" in result
assert result["chart_type"] == "bar"
@pytest.mark.asyncio
async def test_chart_create_scatter(chart_tools):
"""Test creating a scatter chart"""
data = {
"x": [1, 2, 3, 4, 5],
"y": [10, 20, 15, 25, 30]
}
result = await chart_tools.chart_create("scatter", data)
assert "figure" in result
assert result["chart_type"] == "scatter"
@pytest.mark.asyncio
async def test_chart_create_pie(chart_tools):
"""Test creating a pie chart"""
data = {
"labels": ["A", "B", "C"],
"values": [30, 50, 20]
}
result = await chart_tools.chart_create("pie", data)
assert "figure" in result
assert result["chart_type"] == "pie"
@pytest.mark.asyncio
async def test_chart_create_histogram(chart_tools):
"""Test creating a histogram"""
data = {
"x": [1, 1, 2, 2, 2, 3, 3, 4, 5, 5, 5, 5]
}
result = await chart_tools.chart_create("histogram", data)
assert "figure" in result
assert result["chart_type"] == "histogram"
@pytest.mark.asyncio
async def test_chart_create_area(chart_tools):
"""Test creating an area chart"""
data = {
"x": [1, 2, 3, 4, 5],
"y": [10, 20, 15, 25, 30]
}
result = await chart_tools.chart_create("area", data)
assert "figure" in result
assert result["chart_type"] == "area"
@pytest.mark.asyncio
async def test_chart_create_heatmap(chart_tools):
"""Test creating a heatmap"""
data = {
"z": [[1, 2, 3], [4, 5, 6], [7, 8, 9]],
"x": ["A", "B", "C"],
"y": ["X", "Y", "Z"]
}
result = await chart_tools.chart_create("heatmap", data)
assert "figure" in result
assert result["chart_type"] == "heatmap"
@pytest.mark.asyncio
async def test_chart_create_invalid_type(chart_tools):
"""Test creating chart with invalid type"""
data = {"x": [1, 2, 3], "y": [10, 20, 30]}
result = await chart_tools.chart_create("invalid_type", data)
assert "error" in result
assert "invalid" in result["error"].lower()
@pytest.mark.asyncio
async def test_chart_create_with_options(chart_tools):
"""Test creating chart with options"""
data = {
"x": [1, 2, 3],
"y": [10, 20, 30]
}
options = {
"title": "My Chart",
"color": "red"
}
result = await chart_tools.chart_create("line", data, options=options)
assert "figure" in result
# The title should be applied to the figure
@pytest.mark.asyncio
async def test_chart_create_with_theme(chart_tools_with_theme):
"""Test that theme colors are applied to chart"""
data = {
"x": [1, 2, 3],
"y": [10, 20, 30]
}
result = await chart_tools_with_theme.chart_create("line", data)
assert "figure" in result
# Chart should use theme colors
@pytest.mark.asyncio
async def test_chart_configure_interaction(chart_tools):
"""Test configuring chart interaction"""
# Create a simple figure first
data = {"x": [1, 2, 3], "y": [10, 20, 30]}
chart_result = await chart_tools.chart_create("line", data)
figure = chart_result.get("figure", {})
if hasattr(chart_tools, 'chart_configure_interaction'):
result = await chart_tools.chart_configure_interaction(
figure=figure,
interactions={"zoom": True, "pan": True}
)
# Just verify it doesn't crash
assert result is not None
def test_default_colors_defined():
"""Test that DEFAULT_COLORS is properly defined"""
from mcp_server.chart_tools import DEFAULT_COLORS
assert len(DEFAULT_COLORS) == 10
assert all(c.startswith("#") for c in DEFAULT_COLORS)

View File

@@ -0,0 +1,292 @@
"""
Unit tests for DMC component registry.
"""
import pytest
import json
from pathlib import Path
from unittest.mock import patch, MagicMock
@pytest.fixture
def sample_registry_data():
"""Sample registry data for testing"""
return {
"version": "2.5.1",
"categories": {
"buttons": ["Button", "ActionIcon"],
"inputs": ["TextInput", "NumberInput", "Select"]
},
"components": {
"Button": {
"description": "Button component",
"props": {
"variant": {
"type": "string",
"enum": ["filled", "outline", "light"],
"default": "filled"
},
"color": {
"type": "string",
"default": "blue"
},
"size": {
"type": "string",
"enum": ["xs", "sm", "md", "lg", "xl"],
"default": "sm"
},
"disabled": {
"type": "boolean",
"default": False
}
}
},
"TextInput": {
"description": "Text input field",
"props": {
"value": {"type": "string", "default": ""},
"placeholder": {"type": "string"},
"disabled": {"type": "boolean", "default": False},
"required": {"type": "boolean", "default": False}
}
}
}
}
@pytest.fixture
def registry_file(tmp_path, sample_registry_data):
"""Create a temporary registry file"""
registry_dir = tmp_path / "registry"
registry_dir.mkdir()
registry_file = registry_dir / "dmc_2_5.json"
registry_file.write_text(json.dumps(sample_registry_data))
return registry_file
@pytest.fixture
def registry(registry_file):
"""Create a ComponentRegistry with mock registry directory"""
from mcp_server.component_registry import ComponentRegistry
reg = ComponentRegistry(dmc_version="2.5.1")
reg.registry_dir = registry_file.parent
reg.load()
return reg
def test_registry_init():
"""Test registry initialization"""
from mcp_server.component_registry import ComponentRegistry
reg = ComponentRegistry(dmc_version="2.5.1")
assert reg.dmc_version == "2.5.1"
assert reg.components == {}
assert reg.categories == {}
assert reg.loaded_version is None
def test_registry_load_success(registry, sample_registry_data):
"""Test successful registry loading"""
assert registry.is_loaded()
assert registry.loaded_version == "2.5.1"
assert len(registry.components) == 2
assert "Button" in registry.components
assert "TextInput" in registry.components
def test_registry_load_no_file():
"""Test registry loading when no file exists"""
from mcp_server.component_registry import ComponentRegistry
reg = ComponentRegistry(dmc_version="99.99.99")
reg.registry_dir = Path("/nonexistent/path")
result = reg.load()
assert result is False
assert not reg.is_loaded()
def test_get_component(registry):
"""Test getting a component by name"""
button = registry.get_component("Button")
assert button is not None
assert button["description"] == "Button component"
assert "props" in button
def test_get_component_not_found(registry):
"""Test getting a nonexistent component"""
result = registry.get_component("NonexistentComponent")
assert result is None
def test_get_component_props(registry):
"""Test getting component props"""
props = registry.get_component_props("Button")
assert props is not None
assert "variant" in props
assert "color" in props
assert props["variant"]["type"] == "string"
assert props["variant"]["enum"] == ["filled", "outline", "light"]
def test_get_component_props_not_found(registry):
"""Test getting props for nonexistent component"""
props = registry.get_component_props("Nonexistent")
assert props is None
def test_list_components_all(registry):
"""Test listing all components"""
result = registry.list_components()
assert "buttons" in result
assert "inputs" in result
assert "Button" in result["buttons"]
assert "TextInput" in result["inputs"]
def test_list_components_by_category(registry):
"""Test listing components by category"""
result = registry.list_components(category="buttons")
assert len(result) == 1
assert "buttons" in result
assert "Button" in result["buttons"]
def test_list_components_invalid_category(registry):
"""Test listing components with invalid category"""
result = registry.list_components(category="nonexistent")
assert result == {}
def test_get_categories(registry):
"""Test getting available categories"""
categories = registry.get_categories()
assert "buttons" in categories
assert "inputs" in categories
def test_validate_prop_valid_enum(registry):
"""Test validating a valid enum prop"""
result = registry.validate_prop("Button", "variant", "filled")
assert result["valid"] is True
def test_validate_prop_invalid_enum(registry):
"""Test validating an invalid enum prop"""
result = registry.validate_prop("Button", "variant", "invalid_variant")
assert result["valid"] is False
assert "expects one of" in result["error"]
def test_validate_prop_valid_type(registry):
"""Test validating a valid type"""
result = registry.validate_prop("Button", "disabled", True)
assert result["valid"] is True
def test_validate_prop_invalid_type(registry):
"""Test validating an invalid type"""
result = registry.validate_prop("Button", "disabled", "not_a_boolean")
assert result["valid"] is False
assert "expects type" in result["error"]
def test_validate_prop_unknown_component(registry):
"""Test validating prop for unknown component"""
result = registry.validate_prop("Nonexistent", "prop", "value")
assert result["valid"] is False
assert "Unknown component" in result["error"]
def test_validate_prop_unknown_prop(registry):
"""Test validating an unknown prop"""
result = registry.validate_prop("Button", "unknownProp", "value")
assert result["valid"] is False
assert "Unknown prop" in result["error"]
def test_validate_prop_typo_detection(registry):
"""Test typo detection for similar prop names"""
# colour vs color
result = registry.validate_prop("Button", "colour", "blue")
assert result["valid"] is False
# Should suggest 'color'
assert "color" in result.get("error", "").lower()
def test_find_similar_props(registry):
"""Test finding similar prop names"""
available = ["color", "variant", "size", "disabled"]
# Should match despite case difference
similar = registry._find_similar_props("Color", available)
assert similar == "color"
# Should match with slight typo
similar = registry._find_similar_props("colours", ["color", "variant"])
# May or may not match depending on heuristic
def test_load_registry_convenience_function(registry_file):
"""Test the convenience function"""
from mcp_server.component_registry import load_registry, ComponentRegistry
with patch.object(ComponentRegistry, '__init__', return_value=None) as mock_init:
with patch.object(ComponentRegistry, 'load', return_value=True):
mock_init.return_value = None
# Can't easily test this without mocking more - just ensure it doesn't crash
pass
def test_find_registry_file_exact_match(tmp_path):
"""Test finding exact registry file match"""
from mcp_server.component_registry import ComponentRegistry
# Create registry files
registry_dir = tmp_path / "registry"
registry_dir.mkdir()
(registry_dir / "dmc_2_5.json").write_text('{"version": "2.5.0"}')
reg = ComponentRegistry(dmc_version="2.5.1")
reg.registry_dir = registry_dir
result = reg._find_registry_file()
assert result is not None
assert result.name == "dmc_2_5.json"
def test_find_registry_file_fallback(tmp_path):
"""Test fallback to latest registry when no exact match"""
from mcp_server.component_registry import ComponentRegistry
# Create registry files
registry_dir = tmp_path / "registry"
registry_dir.mkdir()
(registry_dir / "dmc_0_14.json").write_text('{"version": "0.14.0"}')
reg = ComponentRegistry(dmc_version="2.5.1") # No exact match
reg.registry_dir = registry_dir
result = reg._find_registry_file()
assert result is not None
assert result.name == "dmc_0_14.json" # Falls back to available

View File

@@ -0,0 +1,156 @@
"""
Unit tests for viz-platform configuration loader.
"""
import pytest
import os
from pathlib import Path
from unittest.mock import patch, MagicMock
@pytest.fixture
def clean_env():
"""Clean environment variables before test"""
env_vars = ['DMC_VERSION', 'CLAUDE_PROJECT_DIR', 'VIZ_DEFAULT_THEME']
saved = {k: os.environ.get(k) for k in env_vars}
for k in env_vars:
if k in os.environ:
del os.environ[k]
yield
# Restore after test
for k, v in saved.items():
if v is not None:
os.environ[k] = v
elif k in os.environ:
del os.environ[k]
@pytest.fixture
def config():
"""Create VizPlatformConfig instance"""
from mcp_server.config import VizPlatformConfig
return VizPlatformConfig()
def test_config_init(config):
"""Test config initialization"""
assert config.dmc_version is None
assert config.theme_dir_user == Path.home() / '.config' / 'claude' / 'themes'
assert config.theme_dir_project is None
assert config.default_theme is None
def test_config_load_returns_dict(config, clean_env):
"""Test config.load() returns expected structure"""
result = config.load()
assert isinstance(result, dict)
assert 'dmc_version' in result
assert 'dmc_available' in result
assert 'theme_dir_user' in result
assert 'theme_dir_project' in result
assert 'default_theme' in result
assert 'project_dir' in result
def test_config_respects_env_dmc_version(config, clean_env):
"""Test that DMC_VERSION env var is respected"""
os.environ['DMC_VERSION'] = '0.14.7'
result = config.load()
assert result['dmc_version'] == '0.14.7'
assert result['dmc_available'] is True
def test_config_respects_default_theme_env(config, clean_env):
"""Test that VIZ_DEFAULT_THEME env var is respected"""
os.environ['VIZ_DEFAULT_THEME'] = 'my-dark-theme'
result = config.load()
assert result['default_theme'] == 'my-dark-theme'
def test_detect_dmc_version_not_installed(config):
"""Test DMC version detection when not installed"""
with patch('importlib.metadata.version', side_effect=ImportError("not installed")):
version = config._detect_dmc_version()
assert version is None
def test_detect_dmc_version_installed(config):
"""Test DMC version detection when installed"""
with patch('importlib.metadata.version', return_value='0.14.7'):
version = config._detect_dmc_version()
assert version == '0.14.7'
def test_find_project_directory_from_env(config, clean_env, tmp_path):
"""Test project directory detection from CLAUDE_PROJECT_DIR"""
os.environ['CLAUDE_PROJECT_DIR'] = str(tmp_path)
result = config._find_project_directory()
assert result == tmp_path
def test_find_project_directory_with_git(config, clean_env, tmp_path):
"""Test project directory detection with .git folder"""
git_dir = tmp_path / '.git'
git_dir.mkdir()
with patch.dict(os.environ, {'PWD': str(tmp_path)}):
result = config._find_project_directory()
assert result == tmp_path
def test_find_project_directory_with_env_file(config, clean_env, tmp_path):
"""Test project directory detection with .env file"""
env_file = tmp_path / '.env'
env_file.touch()
with patch.dict(os.environ, {'PWD': str(tmp_path)}):
result = config._find_project_directory()
assert result == tmp_path
def test_load_config_convenience_function(clean_env):
"""Test the convenience function load_config()"""
from mcp_server.config import load_config
result = load_config()
assert isinstance(result, dict)
assert 'dmc_version' in result
def test_check_dmc_version_not_installed(clean_env):
"""Test check_dmc_version when DMC not installed"""
from mcp_server.config import check_dmc_version
with patch('mcp_server.config.load_config', return_value={'dmc_available': False}):
result = check_dmc_version()
assert result['installed'] is False
assert 'not installed' in result['message'].lower()
def test_check_dmc_version_installed_with_registry(clean_env, tmp_path):
"""Test check_dmc_version when DMC installed with matching registry"""
from mcp_server.config import check_dmc_version
mock_config = {
'dmc_available': True,
'dmc_version': '2.5.1'
}
with patch('mcp_server.config.load_config', return_value=mock_config):
with patch('pathlib.Path.exists', return_value=True):
result = check_dmc_version()
assert result['installed'] is True
assert result['version'] == '2.5.1'

View File

@@ -0,0 +1,283 @@
"""
Unit tests for DMC validation tools.
"""
import pytest
from unittest.mock import MagicMock, patch
@pytest.fixture
def mock_registry():
"""Create a mock component registry"""
registry = MagicMock()
registry.is_loaded.return_value = True
registry.loaded_version = "2.5.1"
registry.categories = {
"buttons": ["Button", "ActionIcon"],
"inputs": ["TextInput", "Select"]
}
registry.list_components.return_value = registry.categories
registry.get_categories.return_value = ["buttons", "inputs"]
# Mock Button component
registry.get_component.side_effect = lambda name: {
"Button": {
"description": "Button component",
"props": {
"variant": {"type": "string", "enum": ["filled", "outline"], "default": "filled"},
"color": {"type": "string", "default": "blue"},
"size": {"type": "string", "enum": ["xs", "sm", "md", "lg"], "default": "sm"},
"disabled": {"type": "boolean", "default": False, "required": False}
}
},
"TextInput": {
"description": "Text input",
"props": {
"value": {"type": "string", "required": True},
"placeholder": {"type": "string"}
}
}
}.get(name)
registry.get_component_props.side_effect = lambda name: {
"Button": {
"variant": {"type": "string", "enum": ["filled", "outline"], "default": "filled"},
"color": {"type": "string", "default": "blue"},
"size": {"type": "string", "enum": ["xs", "sm", "md", "lg"], "default": "sm"},
"disabled": {"type": "boolean", "default": False}
},
"TextInput": {
"value": {"type": "string", "required": True},
"placeholder": {"type": "string"}
}
}.get(name)
registry.validate_prop.side_effect = lambda comp, prop, val: (
{"valid": True} if prop in ["variant", "color", "size", "disabled", "value", "placeholder"]
else {"valid": False, "error": f"Unknown prop '{prop}'"}
)
return registry
@pytest.fixture
def dmc_tools(mock_registry):
"""Create DMCTools instance with mock registry"""
from mcp_server.dmc_tools import DMCTools
tools = DMCTools(registry=mock_registry)
tools._initialized = True
return tools
@pytest.fixture
def uninitialized_tools():
"""Create uninitialized DMCTools instance"""
from mcp_server.dmc_tools import DMCTools
return DMCTools()
@pytest.mark.asyncio
async def test_list_components_all(dmc_tools):
"""Test listing all components"""
result = await dmc_tools.list_components()
assert "components" in result
assert "categories" in result
assert "version" in result
assert result["version"] == "2.5.1"
@pytest.mark.asyncio
async def test_list_components_by_category(dmc_tools, mock_registry):
"""Test listing components by category"""
mock_registry.list_components.return_value = {"buttons": ["Button", "ActionIcon"]}
result = await dmc_tools.list_components(category="buttons")
assert "buttons" in result["components"]
mock_registry.list_components.assert_called_with("buttons")
@pytest.mark.asyncio
async def test_list_components_not_initialized(uninitialized_tools):
"""Test listing components when not initialized"""
result = await uninitialized_tools.list_components()
assert "error" in result
assert result["total_count"] == 0
@pytest.mark.asyncio
async def test_get_component_props_success(dmc_tools):
"""Test getting component props"""
result = await dmc_tools.get_component_props("Button")
assert result["component"] == "Button"
assert "props" in result
assert result["prop_count"] > 0
@pytest.mark.asyncio
async def test_get_component_props_not_found(dmc_tools, mock_registry):
"""Test getting props for nonexistent component"""
mock_registry.get_component.return_value = None
result = await dmc_tools.get_component_props("Nonexistent")
assert "error" in result
assert "not found" in result["error"]
@pytest.mark.asyncio
async def test_get_component_props_not_initialized(uninitialized_tools):
"""Test getting props when not initialized"""
result = await uninitialized_tools.get_component_props("Button")
assert "error" in result
assert result["prop_count"] == 0
@pytest.mark.asyncio
async def test_validate_component_valid(dmc_tools, mock_registry):
"""Test validating valid component props"""
props = {
"variant": "filled",
"color": "blue",
"size": "md"
}
result = await dmc_tools.validate_component("Button", props)
assert result["valid"] is True
assert len(result["errors"]) == 0
assert result["component"] == "Button"
@pytest.mark.asyncio
async def test_validate_component_invalid_prop(dmc_tools, mock_registry):
"""Test validating with invalid prop name"""
mock_registry.validate_prop.side_effect = lambda comp, prop, val: (
{"valid": False, "error": f"Unknown prop '{prop}'"} if prop == "unknownProp"
else {"valid": True}
)
props = {"unknownProp": "value"}
result = await dmc_tools.validate_component("Button", props)
assert result["valid"] is False
assert len(result["errors"]) > 0
@pytest.mark.asyncio
async def test_validate_component_missing_required(dmc_tools, mock_registry):
"""Test validating with missing required prop"""
# TextInput has required value prop
mock_registry.get_component.return_value = {
"props": {
"value": {"type": "string", "required": True}
}
}
result = await dmc_tools.validate_component("TextInput", {})
assert result["valid"] is False
assert any("required" in e.lower() for e in result["errors"])
@pytest.mark.asyncio
async def test_validate_component_not_found(dmc_tools, mock_registry):
"""Test validating nonexistent component"""
mock_registry.get_component.return_value = None
result = await dmc_tools.validate_component("Nonexistent", {"prop": "value"})
assert result["valid"] is False
assert "Unknown component" in result["errors"][0]
@pytest.mark.asyncio
async def test_validate_component_not_initialized(uninitialized_tools):
"""Test validating when not initialized"""
result = await uninitialized_tools.validate_component("Button", {})
assert result["valid"] is False
assert "not initialized" in result["errors"][0].lower()
@pytest.mark.asyncio
async def test_validate_component_skips_special_props(dmc_tools, mock_registry):
"""Test that special props (id, children, etc) are skipped"""
props = {
"id": "my-button",
"children": "Click me",
"className": "my-class",
"style": {"color": "red"},
"key": "btn-1"
}
result = await dmc_tools.validate_component("Button", props)
# Should not error on special props
assert result["valid"] is True
def test_find_similar_component(dmc_tools, mock_registry):
"""Test finding similar component names"""
# Should find Button when given 'button' (case mismatch)
similar = dmc_tools._find_similar_component("button")
assert similar == "Button"
def test_find_similar_component_prefix(dmc_tools, mock_registry):
"""Test finding similar component with prefix match"""
similar = dmc_tools._find_similar_component("Butt")
assert similar == "Button"
def test_check_common_mistakes_onclick(dmc_tools):
"""Test detection of onclick event handler mistake"""
warnings = []
dmc_tools._check_common_mistakes("Button", {"onClick": "handler"}, warnings)
assert len(warnings) > 0
assert any("callback" in w.lower() for w in warnings)
def test_check_common_mistakes_class(dmc_tools):
"""Test detection of 'class' instead of 'className'"""
warnings = []
dmc_tools._check_common_mistakes("Button", {"class": "my-class"}, warnings)
assert len(warnings) > 0
assert any("classname" in w.lower() for w in warnings)
def test_check_common_mistakes_button_href(dmc_tools):
"""Test detection of Button with href but no component prop"""
warnings = []
dmc_tools._check_common_mistakes("Button", {"href": "/link"}, warnings)
assert len(warnings) > 0
assert any("component" in w.lower() for w in warnings)
def test_initialize_with_version():
"""Test initializing tools with DMC version"""
from mcp_server.dmc_tools import DMCTools
tools = DMCTools()
with patch('mcp_server.dmc_tools.ComponentRegistry') as MockRegistry:
mock_instance = MagicMock()
mock_instance.is_loaded.return_value = True
MockRegistry.return_value = mock_instance
result = tools.initialize(dmc_version="2.5.1")
MockRegistry.assert_called_once_with("2.5.1")
assert result is True

View File

@@ -0,0 +1,304 @@
"""
Unit tests for theme management tools.
"""
import pytest
from unittest.mock import MagicMock, patch
@pytest.fixture
def theme_store():
"""Create a fresh ThemeStore instance"""
from mcp_server.theme_store import ThemeStore
store = ThemeStore()
store._themes = {} # Clear any existing themes
return store
@pytest.fixture
def theme_tools(theme_store):
"""Create ThemeTools instance with fresh store"""
from mcp_server.theme_tools import ThemeTools
return ThemeTools(store=theme_store)
def test_theme_store_init():
"""Test theme store initialization"""
from mcp_server.theme_store import ThemeStore
store = ThemeStore()
# Should have default theme
assert store.get_theme("default") is not None
def test_default_theme_structure():
"""Test default theme has required structure"""
from mcp_server.theme_store import DEFAULT_THEME
assert "name" in DEFAULT_THEME
assert "tokens" in DEFAULT_THEME
assert "colors" in DEFAULT_THEME["tokens"]
assert "spacing" in DEFAULT_THEME["tokens"]
assert "typography" in DEFAULT_THEME["tokens"]
assert "radii" in DEFAULT_THEME["tokens"]
def test_default_theme_colors():
"""Test default theme has required color tokens"""
from mcp_server.theme_store import DEFAULT_THEME
colors = DEFAULT_THEME["tokens"]["colors"]
assert "primary" in colors
assert "secondary" in colors
assert "success" in colors
assert "warning" in colors
assert "error" in colors
assert "background" in colors
assert "text" in colors
@pytest.mark.asyncio
async def test_theme_create(theme_tools):
"""Test creating a new theme"""
tokens = {
"colors": {
"primary": "#ff0000"
}
}
result = await theme_tools.theme_create("my-theme", tokens)
assert result["name"] == "my-theme"
assert "tokens" in result
assert result["tokens"]["colors"]["primary"] == "#ff0000"
@pytest.mark.asyncio
async def test_theme_create_merges_with_defaults(theme_tools):
"""Test that new theme merges with default tokens"""
tokens = {
"colors": {
"primary": "#ff0000"
}
}
result = await theme_tools.theme_create("partial-theme", tokens)
# Should have primary from our tokens
assert result["tokens"]["colors"]["primary"] == "#ff0000"
# Should inherit secondary from defaults
assert "secondary" in result["tokens"]["colors"]
@pytest.mark.asyncio
async def test_theme_create_duplicate_name(theme_tools, theme_store):
"""Test creating theme with existing name fails"""
# Create first theme
await theme_tools.theme_create("existing", {"colors": {}})
# Try to create with same name
result = await theme_tools.theme_create("existing", {"colors": {}})
assert "error" in result
assert "already exists" in result["error"]
@pytest.mark.asyncio
async def test_theme_extend(theme_tools, theme_store):
"""Test extending an existing theme"""
# Create base theme
await theme_tools.theme_create("base", {
"colors": {"primary": "#0000ff"}
})
# Extend it
result = await theme_tools.theme_extend(
base_theme="base",
overrides={"colors": {"secondary": "#00ff00"}},
new_name="extended"
)
assert result["name"] == "extended"
# Should have base primary
assert result["tokens"]["colors"]["primary"] == "#0000ff"
# Should have override secondary
assert result["tokens"]["colors"]["secondary"] == "#00ff00"
@pytest.mark.asyncio
async def test_theme_extend_nonexistent_base(theme_tools):
"""Test extending nonexistent theme fails"""
result = await theme_tools.theme_extend(
base_theme="nonexistent",
overrides={},
new_name="new"
)
assert "error" in result
assert "not found" in result["error"]
@pytest.mark.asyncio
async def test_theme_extend_default_name(theme_tools, theme_store):
"""Test extending creates default name if not provided"""
await theme_tools.theme_create("base", {"colors": {}})
result = await theme_tools.theme_extend(
base_theme="base",
overrides={}
# No new_name provided
)
assert result["name"] == "base_extended"
@pytest.mark.asyncio
async def test_theme_validate(theme_tools, theme_store):
"""Test theme validation"""
await theme_tools.theme_create("test-theme", {
"colors": {"primary": "#ff0000"},
"spacing": {"md": "16px"}
})
result = await theme_tools.theme_validate("test-theme")
assert "complete" in result or "validation" in result
@pytest.mark.asyncio
async def test_theme_validate_nonexistent(theme_tools):
"""Test validating nonexistent theme"""
result = await theme_tools.theme_validate("nonexistent")
assert "error" in result
@pytest.mark.asyncio
async def test_theme_export_css(theme_tools, theme_store):
"""Test exporting theme as CSS"""
await theme_tools.theme_create("css-theme", {
"colors": {"primary": "#ff0000"},
"spacing": {"md": "16px"}
})
result = await theme_tools.theme_export_css("css-theme")
assert "css" in result
# CSS should contain custom properties
assert "--" in result["css"]
@pytest.mark.asyncio
async def test_theme_export_css_nonexistent(theme_tools):
"""Test exporting nonexistent theme"""
result = await theme_tools.theme_export_css("nonexistent")
assert "error" in result
@pytest.mark.asyncio
async def test_theme_list(theme_tools, theme_store):
"""Test listing themes"""
await theme_tools.theme_create("theme1", {"colors": {}})
await theme_tools.theme_create("theme2", {"colors": {}})
result = await theme_tools.theme_list()
assert "themes" in result
assert "theme1" in result["themes"]
assert "theme2" in result["themes"]
@pytest.mark.asyncio
async def test_theme_activate(theme_tools, theme_store):
"""Test activating a theme"""
await theme_tools.theme_create("active-theme", {"colors": {}})
result = await theme_tools.theme_activate("active-theme")
assert result.get("active_theme") == "active-theme" or result.get("success") is True
@pytest.mark.asyncio
async def test_theme_activate_nonexistent(theme_tools):
"""Test activating nonexistent theme"""
result = await theme_tools.theme_activate("nonexistent")
assert "error" in result
def test_theme_store_get_theme(theme_store):
"""Test getting theme from store"""
from mcp_server.theme_store import DEFAULT_THEME
# Add a theme first, then retrieve it
theme_store._themes["test-theme"] = {"name": "test-theme", "tokens": {}}
result = theme_store.get_theme("test-theme")
assert result is not None
assert result["name"] == "test-theme"
def test_theme_store_list_themes(theme_store):
"""Test listing themes from store"""
result = theme_store.list_themes()
assert isinstance(result, list)
def test_deep_merge(theme_tools):
"""Test deep merging of token dicts"""
base = {
"colors": {
"primary": "#000",
"secondary": "#111"
},
"spacing": {"sm": "8px"}
}
override = {
"colors": {
"primary": "#fff"
}
}
result = theme_tools._deep_merge(base, override)
# primary should be overridden
assert result["colors"]["primary"] == "#fff"
# secondary should remain
assert result["colors"]["secondary"] == "#111"
# spacing should remain
assert result["spacing"]["sm"] == "8px"
def test_validate_tokens(theme_tools):
"""Test token validation"""
from mcp_server.theme_store import REQUIRED_TOKEN_CATEGORIES
tokens = {
"colors": {"primary": "#000"},
"spacing": {"md": "16px"},
"typography": {"fontFamily": "Inter"},
"radii": {"md": "8px"}
}
result = theme_tools._validate_tokens(tokens)
assert "complete" in result
# Check for either "missing" or "missing_required" key
assert "missing" in result or "missing_required" in result or "missing_optional" in result
def test_validate_tokens_incomplete(theme_tools):
"""Test validation of incomplete tokens"""
tokens = {
"colors": {"primary": "#000"}
# Missing spacing, typography, radii
}
result = theme_tools._validate_tokens(tokens)
# Should flag missing categories
assert result["complete"] is False or len(result.get("missing", [])) > 0

View File

@@ -0,0 +1,24 @@
{
"name": "viz-platform",
"version": "1.0.0",
"description": "Visualization tools with Dash Mantine Components validation, Plotly charts, and theming",
"author": {
"name": "Leo Miranda",
"email": "leobmiranda@gmail.com"
},
"homepage": "https://gitea.hotserv.cloud/personal-projects/leo-claude-mktplace/src/branch/main/plugins/viz-platform/README.md",
"repository": "https://gitea.hotserv.cloud/personal-projects/leo-claude-mktplace.git",
"license": "MIT",
"keywords": [
"dash",
"plotly",
"mantine",
"charts",
"dashboards",
"theming",
"visualization",
"dmc"
],
"commands": ["./commands/"],
"mcpServers": ["./.mcp.json"]
}

View File

@@ -0,0 +1,10 @@
{
"mcpServers": {
"viz-platform": {
"type": "stdio",
"command": "${CLAUDE_PLUGIN_ROOT}/mcp-servers/viz-platform/.venv/bin/python",
"args": ["-m", "mcp_server.server"],
"cwd": "${CLAUDE_PLUGIN_ROOT}/mcp-servers/viz-platform"
}
}
}

View File

@@ -0,0 +1,196 @@
# viz-platform Plugin
Visualization tools with Dash Mantine Components validation, Plotly charts, and theming for Claude Code.
## Features
- **DMC Validation**: Prevent prop hallucination with version-locked component registry
- **Chart Creation**: Plotly charts with automatic theme token application
- **Layout Builder**: Dashboard layouts with filters, grids, and responsive design
- **Theme System**: Create, extend, and export design tokens
## Installation
This plugin is part of the leo-claude-mktplace. Install via:
```bash
# From marketplace
claude plugins install leo-claude-mktplace/viz-platform
# Setup MCP server venv
cd ~/.claude/plugins/marketplaces/leo-claude-mktplace/mcp-servers/viz-platform
python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
```
## Configuration
### System-Level (Optional)
Create `~/.config/claude/viz-platform.env` for default theme preferences:
```env
VIZ_PLATFORM_COLOR_SCHEME=light
VIZ_PLATFORM_PRIMARY_COLOR=blue
```
### Project-Level (Optional)
Add to project `.env` for project-specific settings:
```env
VIZ_PLATFORM_THEME=my-custom-theme
DMC_VERSION=0.14.7
```
## Commands
| Command | Description |
|---------|-------------|
| `/initial-setup` | Interactive setup wizard for DMC and theme preferences |
| `/component {name}` | Inspect component props and validation |
| `/chart {type}` | Create a Plotly chart |
| `/dashboard {template}` | Create a dashboard layout |
| `/theme {name}` | Apply an existing theme |
| `/theme-new {name}` | Create a new custom theme |
| `/theme-css {name}` | Export theme as CSS |
## Agents
| Agent | Description |
|-------|-------------|
| `theme-setup` | Design-focused theme creation specialist |
| `layout-builder` | Dashboard layout and filter specialist |
| `component-check` | Strict component validation specialist |
## Tool Categories
### DMC Validation (3 tools)
Prevent invalid component props before runtime.
| Tool | Description |
|------|-------------|
| `list_components` | List available components by category |
| `get_component_props` | Get detailed prop specifications |
| `validate_component` | Validate a component configuration |
### Charts (2 tools)
Create Plotly charts with theme integration.
| Tool | Description |
|------|-------------|
| `chart_create` | Create a chart (line, bar, scatter, pie, etc.) |
| `chart_configure_interaction` | Configure chart interactivity |
### Layouts (5 tools)
Build dashboard structures with filters and grids.
| Tool | Description |
|------|-------------|
| `layout_create` | Create a layout structure |
| `layout_add_filter` | Add filter components |
| `layout_set_grid` | Configure responsive grid |
| `layout_add_section` | Add content sections |
| `layout_get` | Retrieve layout details |
### Themes (6 tools)
Manage design tokens and styling.
| Tool | Description |
|------|-------------|
| `theme_create` | Create a new theme |
| `theme_extend` | Extend an existing theme |
| `theme_validate` | Validate theme configuration |
| `theme_export_css` | Export as CSS custom properties |
| `theme_list` | List available themes |
| `theme_activate` | Set the active theme |
### Pages (5 tools)
Create full Dash app configurations.
| Tool | Description |
|------|-------------|
| `page_create` | Create a page structure |
| `page_add_navbar` | Add navigation bar |
| `page_set_auth` | Configure authentication |
| `page_list` | List pages |
| `page_get_app_config` | Get full app configuration |
## Component Validation
The key differentiator of viz-platform is the component registry system:
```python
# Before writing component code
get_component_props("Button")
# Returns: all valid props with types, enums, defaults
# After writing code
validate_component("Button", {"variant": "filled", "color": "blue"})
# Returns: {valid: true} or {valid: false, errors: [...]}
```
This prevents common DMC mistakes:
- Prop typos (`colour` vs `color`)
- Invalid enum values (`size="large"` vs `size="lg"`)
- Wrong case (`fullwidth` vs `fullWidth`)
## Example Workflow
```
/component Button
# → Shows all Button props with types and defaults
/theme-new corporate
# → Creates theme with brand colors
/chart bar
# → Creates bar chart with theme colors
/dashboard sidebar
# → Creates sidebar layout with filters
/theme-css corporate
# → Exports theme as CSS for external use
```
## Cross-Plugin Integration
viz-platform works seamlessly with data-platform:
1. **Load data** with data-platform: `/ingest sales.csv`
2. **Create chart** with viz-platform: `/chart line` using the data_ref
3. **Build layout** with viz-platform: `/dashboard` with filters
4. **Export** complete dashboard structure
## Chart Types
| Type | Best For |
|------|----------|
| `line` | Time series, trends |
| `bar` | Comparisons, categories |
| `scatter` | Correlations, distributions |
| `pie` | Part-to-whole |
| `area` | Cumulative trends |
| `histogram` | Frequency distributions |
| `box` | Statistical distributions |
| `heatmap` | Matrix correlations |
| `sunburst` | Hierarchical data |
| `treemap` | Hierarchical proportions |
## Layout Templates
| Template | Best For |
|----------|----------|
| `basic` | Simple dashboards, reports |
| `sidebar` | Navigation-heavy apps |
| `tabs` | Multi-page dashboards |
| `split` | Comparisons, master-detail |
## Requirements
- Python 3.10+
- dash-mantine-components >= 0.14.0
- plotly >= 5.18.0
- dash >= 2.14.0

View File

@@ -0,0 +1,145 @@
# Component Check Agent
You are a strict component validation specialist. Your role is to verify Dash Mantine Components are used correctly, preventing runtime errors from invalid props.
## Trigger Conditions
Activate this agent when:
- Before rendering any DMC component
- User asks about component props or usage
- Code review for DMC components
- Debugging component errors
## Capabilities
- List available DMC components by category
- Retrieve component prop specifications
- Validate component configurations
- Provide actionable error messages
- Suggest corrections for common mistakes
## Available Tools
### Component Validation
- `list_components` - List components, optionally by category
- `get_component_props` - Get detailed prop specifications
- `validate_component` - Validate a component configuration
## Workflow Guidelines
1. **Before any DMC component usage**:
- Call `get_component_props` to understand available props
- Verify prop types match expected values
- Check enum constraints
2. **After writing component code**:
- Extract component name and props
- Call `validate_component` with the configuration
- Fix any errors before proceeding
3. **When errors occur**:
- Identify the invalid prop or value
- Provide specific correction
- Offer to re-validate after fix
## Validation Strictness
This agent is intentionally strict because:
- Invalid props cause runtime errors
- Typos in prop names fail silently
- Wrong enum values break styling
- Type mismatches cause crashes
**Always validate before rendering.**
## Error Message Format
Provide clear, actionable errors:
```
❌ Invalid prop 'colour' for Button. Did you mean 'color'?
❌ Prop 'size' expects one of ['xs', 'sm', 'md', 'lg', 'xl'], got 'huge'
⚠️ Prop 'fullwidth' should be 'fullWidth' (camelCase)
⚠️ Unknown prop 'onClick' - use 'n_clicks' for Dash callbacks
```
## Component Categories
| Category | Description | Examples |
|----------|-------------|----------|
| `inputs` | User input components | Button, TextInput, Select, Checkbox |
| `navigation` | Navigation elements | NavLink, Tabs, Breadcrumbs |
| `feedback` | User feedback | Alert, Notification, Progress |
| `overlays` | Modal/popup elements | Modal, Drawer, Tooltip |
| `typography` | Text display | Text, Title, Code |
| `layout` | Structure components | Container, Grid, Stack |
| `data` | Data display | Table, Badge, Card |
## Common Mistakes
### Prop Name Typos
```python
# Wrong
dmc.Button(colour="blue") # 'colour' vs 'color'
# Correct
dmc.Button(color="blue")
```
### Invalid Enum Values
```python
# Wrong
dmc.Button(size="large") # 'large' not valid
# Correct
dmc.Button(size="lg") # Use 'lg'
```
### Wrong Case
```python
# Wrong
dmc.Button(fullwidth=True) # lowercase
# Correct
dmc.Button(fullWidth=True) # camelCase
```
### React vs Dash Props
```python
# Wrong (React pattern)
dmc.Button(onClick=handler)
# Correct (Dash pattern)
dmc.Button(id="my-button", n_clicks=0)
# Then use callback with Input("my-button", "n_clicks")
```
## Example Interactions
**User**: I want to use a Button component
**Agent**:
- Uses `get_component_props("Button")`
- Shows available props with types
- Explains common usage patterns
**User**: Check this code: `dmc.Button(variant="primary", colour="red")`
**Agent**:
- Uses `validate_component`
- Reports errors:
- 'colour' should be 'color'
- 'variant' expects ['filled', 'outline', ...], not 'primary'
- Suggests: `dmc.Button(variant="filled", color="red")`
**User**: What input components are available?
**Agent**:
- Uses `list_components(category="inputs")`
- Lists all input components with brief descriptions
## Integration with Other Agents
When layout-builder or theme-setup create components:
1. They should call component-check first
2. Validate all props before finalizing
3. Ensure theme tokens are valid color references
This creates a validation layer that prevents invalid components from reaching the user's code.

View File

@@ -0,0 +1,151 @@
# Layout Builder Agent
You are a practical dashboard layout specialist. Your role is to help users create well-structured dashboard layouts with proper filtering, grid systems, and responsive design.
## Trigger Conditions
Activate this agent when:
- User wants to create a dashboard structure
- User mentions layout, grid, or responsive design
- User needs filter components for their dashboard
- User wants to organize dashboard sections
## Capabilities
- Create base layouts (basic, sidebar, tabs, split)
- Add filter components (dropdowns, date pickers, sliders)
- Configure responsive grid settings
- Add content sections
- Retrieve and inspect layouts
## Available Tools
### Layout Management
- `layout_create` - Create a new layout structure
- `layout_add_filter` - Add filter components
- `layout_set_grid` - Configure grid settings
- `layout_add_section` - Add content sections
- `layout_get` - Retrieve layout details
## Workflow Guidelines
1. **Understand the purpose**:
- What data will the dashboard display?
- Who is the target audience?
- What actions do users need to take?
2. **Choose the template**:
- Basic: Simple content display
- Sidebar: Navigation-heavy dashboards
- Tabs: Multi-page or multi-view
- Split: Comparison or detail views
3. **Add filters**:
- What dimensions can users filter by?
- Date ranges? Categories? Search?
- Position filters appropriately
4. **Configure the grid**:
- How many columns?
- Mobile responsiveness?
- Spacing between components?
5. **Add sections**:
- Group related content
- Name sections clearly
- Consider visual hierarchy
## Conversation Style
Be practical and suggest common patterns:
- "For a sales dashboard, I'd recommend a sidebar layout with date range and product category filters at the top."
- "Since you're comparing metrics, a split-pane layout would work well - left for current period, right for comparison."
- "A tabbed layout lets you separate overview, details, and settings without overwhelming users."
## Template Reference
### Basic Layout
Best for: Simple dashboards, reports, single-purpose views
```
┌─────────────────────────────┐
│ Header │
├─────────────────────────────┤
│ Filters │
├─────────────────────────────┤
│ Content │
└─────────────────────────────┘
```
### Sidebar Layout
Best for: Navigation-heavy apps, multi-section dashboards
```
┌────────┬────────────────────┐
│ │ Header │
│ Nav ├────────────────────┤
│ │ Filters │
│ ├────────────────────┤
│ │ Content │
└────────┴────────────────────┘
```
### Tabs Layout
Best for: Multi-page apps, view switching
```
┌─────────────────────────────┐
│ Header │
├──────┬──────┬──────┬────────┤
│ Tab1 │ Tab2 │ Tab3 │ │
├──────┴──────┴──────┴────────┤
│ Tab Content │
└─────────────────────────────┘
```
### Split Layout
Best for: Comparisons, master-detail views
```
┌─────────────────────────────┐
│ Header │
├──────────────┬──────────────┤
│ Left │ Right │
│ Pane │ Pane │
└──────────────┴──────────────┘
```
## Filter Types
| Type | Use Case | Example |
|------|----------|---------|
| `dropdown` | Category selection | Product category, region |
| `date_range` | Time filtering | Report period |
| `slider` | Numeric range | Price range, quantity |
| `checkbox` | Multi-select options | Status flags |
| `search` | Text search | Customer lookup |
## Example Interactions
**User**: I need a dashboard for sales data
**Agent**: I'll create a sales dashboard layout.
- Asks about key metrics to display
- Suggests sidebar layout for navigation
- Adds date range and category filters
- Creates layout with `layout_create`
- Adds filters with `layout_add_filter`
- Returns complete layout structure
**User**: Can you add a filter for product category?
**Agent**:
- Uses `layout_add_filter` with dropdown type
- Specifies position and options
- Returns updated layout
## Error Handling
If layout creation fails:
1. Check if layout name already exists
2. Validate template type
3. Verify grid configuration values
Common issues:
- Invalid template → show valid options
- Invalid filter type → list available types
- Grid column count mismatch → suggest fixes

View File

@@ -0,0 +1,93 @@
# Theme Setup Agent
You are a design-focused theme setup specialist. Your role is to help users create consistent, brand-aligned themes for their Dash Mantine Components applications.
## Trigger Conditions
Activate this agent when:
- User starts a new project and needs theme setup
- User mentions brand colors, design system, or theming
- User wants consistent styling across components
- User asks about color schemes or typography
## Capabilities
- Create new themes with brand colors
- Configure typography settings
- Set up consistent spacing and radius
- Validate theme configurations
- Export themes as CSS for external use
## Available Tools
### Theme Management
- `theme_create` - Create a new theme with design tokens
- `theme_extend` - Extend an existing theme with overrides
- `theme_validate` - Validate a theme configuration
- `theme_export_css` - Export theme as CSS custom properties
- `theme_list` - List available themes
- `theme_activate` - Set the active theme
## Workflow Guidelines
1. **Understand the brand**:
- What colors represent the brand?
- Light mode, dark mode, or both?
- Any specific font preferences?
- Rounded or sharp corners?
2. **Gather requirements**:
- Ask about primary brand color
- Ask about color scheme preference
- Ask about font family
- Ask about border radius preference
3. **Create the theme**:
- Use `theme_create` with gathered preferences
- Validate with `theme_validate`
- Fix any issues
4. **Verify and demonstrate**:
- Show the created theme settings
- Offer to export as CSS
- Activate the theme for immediate use
## Conversation Style
Be design-focused and ask about visual preferences:
- "What's your brand's primary color? I can use any Mantine color like blue, indigo, violet, or a custom hex code."
- "Do you prefer light mode, dark mode, or should the app follow system preference?"
- "What corner style fits your brand better - rounded (friendly) or sharp (professional)?"
## Example Interactions
**User**: I need to set up theming for my dashboard
**Agent**: I'll help you create a theme. Let me ask a few questions about your brand...
- Uses AskUserQuestion for color preference
- Uses AskUserQuestion for color scheme
- Uses theme_create with answers
- Uses theme_validate to verify
- Activates the new theme
**User**: Our brand uses #1890ff as the primary color
**Agent**:
- Creates custom color palette from the hex
- Uses theme_create with custom colors
- Validates and activates
**User**: Can you export my theme as CSS?
**Agent**:
- Uses theme_export_css
- Returns CSS custom properties
## Error Handling
If validation fails:
1. Show the specific errors clearly
2. Suggest fixes based on the error
3. Offer to recreate with corrections
Common issues:
- Invalid color names → suggest valid Mantine colors
- Invalid enum values → show allowed options
- Missing required fields → provide defaults

View File

@@ -0,0 +1,144 @@
# viz-platform CLAUDE.md Integration
Add this snippet to your project's CLAUDE.md to enable viz-platform capabilities.
## Integration Snippet
```markdown
## Visualization (viz-platform)
This project uses viz-platform for Dash Mantine Components dashboards.
### Available Commands
- `/component {name}` - Inspect DMC component props
- `/chart {type}` - Create Plotly charts (line, bar, scatter, pie, area, histogram, box, heatmap, sunburst, treemap)
- `/dashboard {template}` - Create layouts (basic, sidebar, tabs, split)
- `/theme {name}` - Apply a theme
- `/theme-new {name}` - Create custom theme
- `/theme-css {name}` - Export theme as CSS
### MCP Tools Available
- **DMC**: list_components, get_component_props, validate_component
- **Charts**: chart_create, chart_configure_interaction
- **Layouts**: layout_create, layout_add_filter, layout_set_grid, layout_add_section, layout_get
- **Themes**: theme_create, theme_extend, theme_validate, theme_export_css, theme_list, theme_activate
- **Pages**: page_create, page_add_navbar, page_set_auth, page_list, page_get_app_config
### Component Validation
ALWAYS validate DMC components before use:
1. Check props with `get_component_props(component_name)`
2. Validate usage with `validate_component(component_name, props)`
3. Fix any errors before proceeding
### Project Theme
Theme: [YOUR_THEME_NAME or "default"]
Color scheme: [light/dark]
Primary color: [color name]
```
## Cross-Plugin Configuration
If using with data-platform, add this section:
```markdown
## Data + Visualization Workflow
### Data Loading (data-platform)
- `/ingest {file}` - Load CSV, Parquet, or JSON
- `/schema {table}` - View database schema
- `/profile {data_ref}` - Statistical summary
### Visualization (viz-platform)
- `/chart {type}` - Create charts from loaded data
- `/dashboard {template}` - Build dashboard layouts
### Workflow Pattern
1. Load data: `read_csv("data.csv")` → returns `data_ref`
2. Create chart: `chart_create(data_ref="data_ref", ...)`
3. Add to layout: `layout_add_section(chart_ref="...")`
4. Apply theme: `theme_activate("my-theme")`
```
## Agent Configuration
### Using theme-setup agent
When user mentions theming or brand colors:
```markdown
Use the theme-setup agent for:
- Creating new themes with brand colors
- Configuring typography and spacing
- Exporting themes as CSS
```
### Using layout-builder agent
When user wants dashboard structure:
```markdown
Use the layout-builder agent for:
- Creating dashboard layouts
- Adding filter components
- Configuring responsive grids
```
### Using component-check agent
For code review and debugging:
```markdown
Use the component-check agent for:
- Validating DMC component usage
- Fixing prop errors
- Understanding component APIs
```
## Example Project CLAUDE.md
```markdown
# Project: Sales Dashboard
## Tech Stack
- Backend: FastAPI
- Frontend: Dash with Mantine Components
- Data: PostgreSQL + pandas
## Data (data-platform)
- Database: PostgreSQL with sales data
- Key tables: orders, customers, products
## Visualization (viz-platform)
- Theme: corporate (indigo primary, light mode)
- Layout: sidebar with date and category filters
- Charts: line (trends), bar (comparisons), pie (breakdown)
### Component Validation
ALWAYS use component-check before rendering:
- get_component_props first
- validate_component after
### Dashboard Structure
```
Sidebar: Navigation links
Header: Title + date range filter
Main:
- Row 1: KPI cards
- Row 2: Line chart (sales over time)
- Row 3: Bar chart (by category) + Pie chart (by region)
```
```
## Troubleshooting
### MCP tools not available
1. Check venv exists: `ls mcp-servers/viz-platform/.venv/`
2. Rebuild if needed: `cd mcp-servers/viz-platform && python -m venv .venv && pip install -r requirements.txt`
3. Restart Claude Code session
### Component validation fails
1. Check DMC version matches registry
2. Use `list_components()` to see available components
3. Verify prop names are camelCase
### Charts not rendering
1. Verify data_ref exists with `list_data()`
2. Check column names match data
3. Validate theme is active

View File

@@ -0,0 +1,86 @@
---
description: Create a Plotly chart with theme integration
---
# Create Chart
Create a Plotly chart with automatic theme token application.
## Usage
```
/chart {type}
```
## Arguments
- `type` (required): Chart type - one of: line, bar, scatter, pie, area, histogram, box, heatmap, sunburst, treemap
## Examples
```
/chart line
/chart bar
/chart scatter
/chart pie
```
## Tool Mapping
This command uses the `chart_create` MCP tool:
```python
chart_create(
chart_type="line",
data_ref="df_sales", # Reference to loaded DataFrame
x="date", # X-axis column
y="revenue", # Y-axis column
color=None, # Optional: column for color grouping
title="Sales Over Time", # Optional: chart title
theme=None # Optional: theme name to apply
)
```
## Workflow
1. **User invokes**: `/chart line`
2. **Agent asks**: Which DataFrame to use? (list available with `list_data` from data-platform)
3. **Agent asks**: Which columns for X and Y axes?
4. **Agent asks**: Any grouping/color column?
5. **Agent creates**: Chart with `chart_create` tool
6. **Agent returns**: Plotly figure JSON ready for rendering
## Chart Types
| Type | Best For |
|------|----------|
| `line` | Time series, trends |
| `bar` | Comparisons, categories |
| `scatter` | Correlations, distributions |
| `pie` | Part-to-whole relationships |
| `area` | Cumulative trends |
| `histogram` | Frequency distributions |
| `box` | Statistical distributions |
| `heatmap` | Matrix correlations |
| `sunburst` | Hierarchical data |
| `treemap` | Hierarchical proportions |
## Theme Integration
Charts automatically inherit colors from the active theme:
- Primary color for main data
- Color palette for multi-series
- Font family and sizes
- Background colors
Override with explicit theme:
```python
chart_create(chart_type="bar", ..., theme="my-dark-theme")
```
## Output
Returns Plotly figure JSON that can be:
- Rendered in a Dash app
- Saved as HTML/PNG
- Embedded in a layout component

View File

@@ -0,0 +1,161 @@
---
description: Inspect Dash Mantine Component props and validation
---
# Inspect Component
Inspect a Dash Mantine Component's available props, types, and defaults.
## Usage
```
/component {name}
```
## Arguments
- `name` (required): DMC component name (e.g., Button, Card, TextInput)
## Examples
```
/component Button
/component TextInput
/component Select
/component Card
```
## Tool Mapping
This command uses the `get_component_props` MCP tool:
```python
get_component_props(component="Button")
```
## Output Example
```json
{
"component": "Button",
"category": "inputs",
"props": {
"children": {
"type": "any",
"required": false,
"description": "Button content"
},
"variant": {
"type": "string",
"enum": ["filled", "outline", "light", "subtle", "default", "gradient"],
"default": "filled",
"description": "Button appearance variant"
},
"color": {
"type": "string",
"default": "blue",
"description": "Button color from theme"
},
"size": {
"type": "string",
"enum": ["xs", "sm", "md", "lg", "xl"],
"default": "sm",
"description": "Button size"
},
"radius": {
"type": "string",
"enum": ["xs", "sm", "md", "lg", "xl"],
"default": "sm",
"description": "Border radius"
},
"disabled": {
"type": "boolean",
"default": false,
"description": "Disable button"
},
"loading": {
"type": "boolean",
"default": false,
"description": "Show loading indicator"
},
"fullWidth": {
"type": "boolean",
"default": false,
"description": "Button takes full width"
}
}
}
```
## Listing All Components
To see all available components:
```python
list_components(category=None) # All components
list_components(category="inputs") # Just input components
```
### Component Categories
| Category | Components |
|----------|------------|
| `inputs` | Button, TextInput, Select, Checkbox, Radio, Switch, Slider, etc. |
| `navigation` | NavLink, Tabs, Breadcrumbs, Pagination, Stepper |
| `feedback` | Alert, Notification, Progress, Loader, Skeleton |
| `overlays` | Modal, Drawer, Tooltip, Popover, Menu |
| `typography` | Text, Title, Code, Blockquote, List |
| `layout` | Container, Grid, Stack, Group, Space, Divider |
| `data` | Table, Badge, Card, Paper, Timeline |
## Validating Component Usage
After inspecting props, validate your usage:
```python
validate_component(
component="Button",
props={
"variant": "filled",
"color": "blue",
"size": "lg",
"children": "Click me"
}
)
```
Returns:
```json
{
"valid": true,
"errors": [],
"warnings": []
}
```
Or with errors:
```json
{
"valid": false,
"errors": [
"Invalid prop 'colour' for Button. Did you mean 'color'?",
"Prop 'size' expects one of ['xs', 'sm', 'md', 'lg', 'xl'], got 'huge'"
],
"warnings": [
"Prop 'fullwidth' should be 'fullWidth' (camelCase)"
]
}
```
## Why This Matters
DMC components have many props with specific type constraints. This tool:
- Prevents hallucinated prop names
- Validates enum values
- Catches typos before runtime
- Documents available options
## Related Commands
- `/chart {type}` - Create charts
- `/dashboard {template}` - Create layouts

View File

@@ -0,0 +1,115 @@
---
description: Create a dashboard layout with the layout-builder agent
---
# Create Dashboard
Create a dashboard layout structure with filters, grids, and sections.
## Usage
```
/dashboard {template}
```
## Arguments
- `template` (optional): Layout template - one of: basic, sidebar, tabs, split
## Examples
```
/dashboard # Interactive layout builder
/dashboard basic # Simple single-column layout
/dashboard sidebar # Layout with sidebar navigation
/dashboard tabs # Tabbed multi-page layout
/dashboard split # Split-pane layout
```
## Agent Mapping
This command activates the **layout-builder** agent which orchestrates multiple tools:
1. `layout_create` - Create the base layout structure
2. `layout_add_filter` - Add filter components (dropdowns, date pickers)
3. `layout_set_grid` - Configure responsive grid settings
4. `layout_add_section` - Add content sections
## Workflow
1. **User invokes**: `/dashboard sidebar`
2. **Agent asks**: What is the dashboard purpose?
3. **Agent asks**: What filters are needed?
4. **Agent creates**: Base layout with `layout_create`
5. **Agent adds**: Filters with `layout_add_filter`
6. **Agent configures**: Grid with `layout_set_grid`
7. **Agent returns**: Complete layout structure
## Templates
### Basic
Single-column layout with header and content area.
```
┌─────────────────────────────┐
│ Header │
├─────────────────────────────┤
│ │
│ Content │
│ │
└─────────────────────────────┘
```
### Sidebar
Layout with collapsible sidebar navigation.
```
┌────────┬────────────────────┐
│ │ Header │
│ Nav ├────────────────────┤
│ │ │
│ │ Content │
│ │ │
└────────┴────────────────────┘
```
### Tabs
Tabbed layout for multi-page dashboards.
```
┌─────────────────────────────┐
│ Header │
├──────┬──────┬──────┬────────┤
│ Tab1 │ Tab2 │ Tab3 │ │
├──────┴──────┴──────┴────────┤
│ │
│ Tab Content │
│ │
└─────────────────────────────┘
```
### Split
Split-pane layout for comparisons.
```
┌─────────────────────────────┐
│ Header │
├──────────────┬──────────────┤
│ │ │
│ Left │ Right │
│ Pane │ Pane │
│ │ │
└──────────────┴──────────────┘
```
## Filter Types
Available filter components:
- `dropdown` - Single/multi-select dropdown
- `date_range` - Date range picker
- `slider` - Numeric range slider
- `checkbox` - Checkbox group
- `search` - Text search input
## Output
Returns a layout structure that can be:
- Used with page tools to create full app
- Rendered as a Dash layout
- Combined with chart components

View File

@@ -0,0 +1,166 @@
---
description: Interactive setup wizard for viz-platform plugin - configures MCP server and theme preferences
---
# Viz-Platform Setup Wizard
This command sets up the viz-platform plugin with Dash Mantine Components validation and theming.
## Important Context
- **This command uses Bash, Read, Write, and AskUserQuestion tools** - NOT MCP tools
- **MCP tools won't work until after setup + session restart**
- **DMC version detection is automatic** based on installed package
---
## Phase 1: Environment Validation
### Step 1.1: Check Python Version
```bash
python3 --version
```
Requires Python 3.10+. If below, stop setup and inform user.
### Step 1.2: Check DMC Installation
```bash
python3 -c "import dash_mantine_components as dmc; print(f'DMC {dmc.__version__}')" 2>/dev/null || echo "DMC_NOT_INSTALLED"
```
If DMC is not installed, inform user:
```
Dash Mantine Components is not installed. Install it with:
pip install dash-mantine-components>=0.14.0
```
---
## Phase 2: MCP Server Setup
### Step 2.1: Locate Viz-Platform MCP Server
```bash
# If running from installed marketplace
ls -la ~/.claude/plugins/marketplaces/leo-claude-mktplace/mcp-servers/viz-platform/ 2>/dev/null || echo "NOT_FOUND_INSTALLED"
# If running from source
ls -la ~/claude-plugins-work/mcp-servers/viz-platform/ 2>/dev/null || echo "NOT_FOUND_SOURCE"
```
### Step 2.2: Check Virtual Environment
```bash
ls -la /path/to/mcp-servers/viz-platform/.venv/bin/python 2>/dev/null && echo "VENV_EXISTS" || echo "VENV_MISSING"
```
### Step 2.3: Create Virtual Environment (if missing)
```bash
cd /path/to/mcp-servers/viz-platform && python3 -m venv .venv && source .venv/bin/activate && pip install --upgrade pip && pip install -r requirements.txt && deactivate
```
---
## Phase 3: Theme Preferences (Optional)
### Step 3.1: Ask About Theme Setup
Use AskUserQuestion:
- Question: "Do you want to configure a default theme for your projects?"
- Header: "Theme"
- Options:
- "Yes, set up a custom theme"
- "No, use Mantine defaults"
**If user chooses "No":** Skip to Phase 4.
### Step 3.2: Choose Base Theme
Use AskUserQuestion:
- Question: "Which base color scheme do you prefer?"
- Header: "Colors"
- Options:
- "Light mode (default)"
- "Dark mode"
- "System preference (auto)"
### Step 3.3: Choose Primary Color
Use AskUserQuestion:
- Question: "What primary color should be used for buttons and accents?"
- Header: "Primary"
- Options:
- "Blue (default)"
- "Indigo"
- "Violet"
- "Other (I'll specify)"
### Step 3.4: Create System Theme Config
```bash
mkdir -p ~/.config/claude
cat > ~/.config/claude/viz-platform.env << 'EOF'
# Viz-Platform Configuration
# Generated by viz-platform /initial-setup
VIZ_PLATFORM_COLOR_SCHEME=<SELECTED_SCHEME>
VIZ_PLATFORM_PRIMARY_COLOR=<SELECTED_COLOR>
EOF
chmod 600 ~/.config/claude/viz-platform.env
```
---
## Phase 4: Validation
### Step 4.1: Verify MCP Server
```bash
cd /path/to/mcp-servers/viz-platform && .venv/bin/python -c "from mcp_server.server import VizPlatformMCPServer; print('MCP Server OK')"
```
### Step 4.2: Summary
```
╔════════════════════════════════════════════════════════════╗
║ VIZ-PLATFORM SETUP COMPLETE ║
╠════════════════════════════════════════════════════════════╣
║ MCP Server: ✓ Ready ║
║ DMC Version: [Detected version] ║
║ DMC Tools: ✓ Available (3 tools) ║
║ Chart Tools: ✓ Available (2 tools) ║
║ Layout Tools: ✓ Available (5 tools) ║
║ Theme Tools: ✓ Available (6 tools) ║
║ Page Tools: ✓ Available (5 tools) ║
╚════════════════════════════════════════════════════════════╝
```
### Step 4.3: Session Restart Notice
---
**⚠️ Session Restart Required**
Restart your Claude Code session for MCP tools to become available.
**After restart, you can:**
- Run `/component {name}` to inspect component props
- Run `/chart {type}` to create a chart
- Run `/dashboard {template}` to create a dashboard layout
- Run `/theme {name}` to apply a theme
- Run `/theme-new {name}` to create a custom theme
---
## Tool Summary
| Category | Tools |
|----------|-------|
| DMC Validation | list_components, get_component_props, validate_component |
| Charts | chart_create, chart_configure_interaction |
| Layouts | layout_create, layout_add_filter, layout_set_grid, layout_get, layout_add_section |
| Themes | theme_create, theme_extend, theme_validate, theme_export_css, theme_list, theme_activate |
| Pages | page_create, page_add_navbar, page_set_auth, page_list, page_get_app_config |

View File

@@ -0,0 +1,111 @@
---
description: Export a theme as CSS custom properties
---
# Export Theme as CSS
Export a theme's design tokens as CSS custom properties for use outside Dash.
## Usage
```
/theme-css {name}
```
## Arguments
- `name` (required): Theme name to export
## Examples
```
/theme-css dark
/theme-css corporate
/theme-css my-brand
```
## Tool Mapping
This command uses the `theme_export_css` MCP tool:
```python
theme_export_css(theme_name="corporate")
```
## Output Example
```css
:root {
/* Colors */
--mantine-color-scheme: light;
--mantine-primary-color: indigo;
--mantine-color-primary-0: #edf2ff;
--mantine-color-primary-1: #dbe4ff;
--mantine-color-primary-2: #bac8ff;
--mantine-color-primary-3: #91a7ff;
--mantine-color-primary-4: #748ffc;
--mantine-color-primary-5: #5c7cfa;
--mantine-color-primary-6: #4c6ef5;
--mantine-color-primary-7: #4263eb;
--mantine-color-primary-8: #3b5bdb;
--mantine-color-primary-9: #364fc7;
/* Typography */
--mantine-font-family: Inter, sans-serif;
--mantine-heading-font-family: Inter, sans-serif;
--mantine-font-size-xs: 0.75rem;
--mantine-font-size-sm: 0.875rem;
--mantine-font-size-md: 1rem;
--mantine-font-size-lg: 1.125rem;
--mantine-font-size-xl: 1.25rem;
/* Spacing */
--mantine-spacing-xs: 0.625rem;
--mantine-spacing-sm: 0.75rem;
--mantine-spacing-md: 1rem;
--mantine-spacing-lg: 1.25rem;
--mantine-spacing-xl: 2rem;
/* Border Radius */
--mantine-radius-xs: 0.125rem;
--mantine-radius-sm: 0.25rem;
--mantine-radius-md: 0.5rem;
--mantine-radius-lg: 1rem;
--mantine-radius-xl: 2rem;
/* Shadows */
--mantine-shadow-xs: 0 1px 3px rgba(0, 0, 0, 0.05);
--mantine-shadow-sm: 0 1px 3px rgba(0, 0, 0, 0.1);
--mantine-shadow-md: 0 4px 6px rgba(0, 0, 0, 0.1);
--mantine-shadow-lg: 0 10px 15px rgba(0, 0, 0, 0.1);
--mantine-shadow-xl: 0 20px 25px rgba(0, 0, 0, 0.1);
}
```
## Use Cases
### External CSS Files
Include the exported CSS in non-Dash projects:
```html
<link rel="stylesheet" href="theme-tokens.css">
```
### Design Handoff
Share design tokens with designers or other teams.
### Documentation
Generate theme documentation for style guides.
### Other Frameworks
Use Mantine-compatible tokens in React, Vue, or other projects.
## Workflow
1. **User invokes**: `/theme-css corporate`
2. **Tool exports**: Theme tokens as CSS
3. **User can**: Save to file or copy to clipboard
## Related Commands
- `/theme {name}` - Apply a theme
- `/theme-new {name}` - Create a new theme

View File

@@ -0,0 +1,117 @@
---
description: Create a new custom theme with design tokens
---
# Create New Theme
Create a new custom theme with specified design tokens.
## Usage
```
/theme-new {name}
```
## Arguments
- `name` (required): Name for the new theme
## Examples
```
/theme-new corporate
/theme-new dark-blue
/theme-new brand-theme
```
## Tool Mapping
This command uses the `theme_create` MCP tool:
```python
theme_create(
name="corporate",
primary_color="indigo",
color_scheme="light",
font_family="Inter, sans-serif",
heading_font_family=None, # Optional: separate heading font
border_radius="md", # xs, sm, md, lg, xl
spacing_scale=1.0, # Multiplier for spacing
colors=None # Optional: custom color palette
)
```
## Workflow
1. **User invokes**: `/theme-new corporate`
2. **Agent asks**: Primary color preference?
3. **Agent asks**: Light or dark color scheme?
4. **Agent asks**: Font family preference?
5. **Agent creates**: Theme with `theme_create`
6. **Agent validates**: Theme with `theme_validate`
7. **Agent activates**: New theme is ready to use
## Theme Properties
### Colors
- `primary_color`: Main accent color (blue, indigo, violet, etc.)
- `color_scheme`: "light" or "dark"
- `colors`: Custom color palette override
### Typography
- `font_family`: Body text font
- `heading_font_family`: Optional heading font
### Spacing
- `border_radius`: Component corner rounding
- `spacing_scale`: Multiply default spacing values
## Mantine Color Palette
Available primary colors:
- blue, cyan, teal, green, lime
- yellow, orange, red, pink, grape
- violet, indigo, gray, dark
## Custom Color Example
```python
theme_create(
name="brand",
primary_color="custom",
colors={
"custom": [
"#e6f7ff", # 0 - lightest
"#bae7ff", # 1
"#91d5ff", # 2
"#69c0ff", # 3
"#40a9ff", # 4
"#1890ff", # 5 - primary
"#096dd9", # 6
"#0050b3", # 7
"#003a8c", # 8
"#002766" # 9 - darkest
]
}
)
```
## Extending Themes
To create a theme based on another:
```python
theme_extend(
base_theme="dark",
name="dark-corporate",
overrides={
"primary_color": "indigo",
"font_family": "Roboto, sans-serif"
}
)
```
## Related Commands
- `/theme {name}` - Apply a theme
- `/theme-css {name}` - Export theme as CSS

View File

@@ -0,0 +1,69 @@
---
description: Apply an existing theme to the current context
---
# Apply Theme
Apply an existing theme to activate its design tokens.
## Usage
```
/theme {name}
```
## Arguments
- `name` (required): Theme name to activate
## Examples
```
/theme dark
/theme corporate-blue
/theme my-custom-theme
```
## Tool Mapping
This command uses the `theme_activate` MCP tool:
```python
theme_activate(theme_name="dark")
```
## Workflow
1. **User invokes**: `/theme dark`
2. **Tool activates**: Theme becomes active for subsequent operations
3. **Charts/layouts**: Automatically use active theme tokens
## Built-in Themes
| Theme | Description |
|-------|-------------|
| `light` | Mantine default light mode |
| `dark` | Mantine default dark mode |
## Listing Available Themes
To see all available themes:
```python
theme_list()
```
Returns both built-in and custom themes.
## Theme Effects
When a theme is activated:
- New charts inherit theme colors
- New layouts use theme spacing
- Components use theme typography
- Callbacks can read active theme tokens
## Related Commands
- `/theme-new {name}` - Create a new theme
- `/theme-css {name}` - Export theme as CSS

View File

@@ -0,0 +1,10 @@
{
"hooks": [
{
"event": "SessionStart",
"type": "command",
"command": "echo 'viz-platform plugin loaded'",
"timeout": 5000
}
]
}

View File

@@ -0,0 +1 @@
2026-01-26T11:59:05 | .claude-plugin | /home/lmiranda/claude-plugins-work/.claude-plugin/marketplace.json | CLAUDE.md .claude-plugin/marketplace.json

View File

@@ -0,0 +1 @@
../../../mcp-servers/viz-platform