diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 9eb91f7..07b609d 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -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" } ] } diff --git a/CHANGELOG.md b/CHANGELOG.md index c8317a9..03c26d3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/CLAUDE.md b/CLAUDE.md index 10785aa..651a11e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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/ diff --git a/mcp-servers/viz-platform/mcp_server/__init__.py b/mcp-servers/viz-platform/mcp_server/__init__.py new file mode 100644 index 0000000..46123ee --- /dev/null +++ b/mcp-servers/viz-platform/mcp_server/__init__.py @@ -0,0 +1,7 @@ +""" +viz-platform MCP Server package. + +Provides Dash Mantine Components validation and visualization tools to Claude Code. +""" + +__version__ = "1.0.0" diff --git a/mcp-servers/viz-platform/mcp_server/chart_tools.py b/mcp-servers/viz-platform/mcp_server/chart_tools.py new file mode 100644 index 0000000..4ba81d6 --- /dev/null +++ b/mcp-servers/viz-platform/mcp_server/chart_tools.py @@ -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": [] + } diff --git a/mcp-servers/viz-platform/mcp_server/component_registry.py b/mcp-servers/viz-platform/mcp_server/component_registry.py new file mode 100644 index 0000000..77641f2 --- /dev/null +++ b/mcp-servers/viz-platform/mcp_server/component_registry.py @@ -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 diff --git a/mcp-servers/viz-platform/mcp_server/config.py b/mcp-servers/viz-platform/mcp_server/config.py new file mode 100644 index 0000000..e552d02 --- /dev/null +++ b/mcp-servers/viz-platform/mcp_server/config.py @@ -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.' + } diff --git a/mcp-servers/viz-platform/mcp_server/dmc_tools.py b/mcp-servers/viz-platform/mcp_server/dmc_tools.py new file mode 100644 index 0000000..0f62043 --- /dev/null +++ b/mcp-servers/viz-platform/mcp_server/dmc_tools.py @@ -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." + ) diff --git a/mcp-servers/viz-platform/mcp_server/layout_tools.py b/mcp-servers/viz-platform/mcp_server/layout_tools.py new file mode 100644 index 0000000..1453ced --- /dev/null +++ b/mcp-servers/viz-platform/mcp_server/layout_tools.py @@ -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() + } diff --git a/mcp-servers/viz-platform/mcp_server/page_tools.py b/mcp-servers/viz-platform/mcp_server/page_tools.py new file mode 100644 index 0000000..dc5c7da --- /dev/null +++ b/mcp-servers/viz-platform/mcp_server/page_tools.py @@ -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 + } + ] + } diff --git a/mcp-servers/viz-platform/mcp_server/server.py b/mcp-servers/viz-platform/mcp_server/server.py new file mode 100644 index 0000000..a48d8e3 --- /dev/null +++ b/mcp-servers/viz-platform/mcp_server/server.py @@ -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()) diff --git a/mcp-servers/viz-platform/mcp_server/theme_store.py b/mcp-servers/viz-platform/mcp_server/theme_store.py new file mode 100644 index 0000000..b2335ee --- /dev/null +++ b/mcp-servers/viz-platform/mcp_server/theme_store.py @@ -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 diff --git a/mcp-servers/viz-platform/mcp_server/theme_tools.py b/mcp-servers/viz-platform/mcp_server/theme_tools.py new file mode 100644 index 0000000..8ff0eaa --- /dev/null +++ b/mcp-servers/viz-platform/mcp_server/theme_tools.py @@ -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 diff --git a/mcp-servers/viz-platform/pyproject.toml b/mcp-servers/viz-platform/pyproject.toml new file mode 100644 index 0000000..f85b01b --- /dev/null +++ b/mcp-servers/viz-platform/pyproject.toml @@ -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"] diff --git a/mcp-servers/viz-platform/registry/dmc_2_5.json b/mcp-servers/viz-platform/registry/dmc_2_5.json new file mode 100644 index 0000000..b8fed35 --- /dev/null +++ b/mcp-servers/viz-platform/registry/dmc_2_5.json @@ -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"} + } + } + } +} diff --git a/mcp-servers/viz-platform/requirements.txt b/mcp-servers/viz-platform/requirements.txt new file mode 100644 index 0000000..4b5159e --- /dev/null +++ b/mcp-servers/viz-platform/requirements.txt @@ -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 diff --git a/mcp-servers/viz-platform/scripts/generate-dmc-registry.py b/mcp-servers/viz-platform/scripts/generate-dmc-registry.py new file mode 100644 index 0000000..b500dd9 --- /dev/null +++ b/mcp-servers/viz-platform/scripts/generate-dmc-registry.py @@ -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() diff --git a/mcp-servers/viz-platform/tests/__init__.py b/mcp-servers/viz-platform/tests/__init__.py new file mode 100644 index 0000000..6eeed7d --- /dev/null +++ b/mcp-servers/viz-platform/tests/__init__.py @@ -0,0 +1 @@ +"""viz-platform MCP Server tests.""" diff --git a/mcp-servers/viz-platform/tests/test_chart_tools.py b/mcp-servers/viz-platform/tests/test_chart_tools.py new file mode 100644 index 0000000..b65fcf0 --- /dev/null +++ b/mcp-servers/viz-platform/tests/test_chart_tools.py @@ -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) diff --git a/mcp-servers/viz-platform/tests/test_component_registry.py b/mcp-servers/viz-platform/tests/test_component_registry.py new file mode 100644 index 0000000..e884ec9 --- /dev/null +++ b/mcp-servers/viz-platform/tests/test_component_registry.py @@ -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 diff --git a/mcp-servers/viz-platform/tests/test_config.py b/mcp-servers/viz-platform/tests/test_config.py new file mode 100644 index 0000000..0740434 --- /dev/null +++ b/mcp-servers/viz-platform/tests/test_config.py @@ -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' diff --git a/mcp-servers/viz-platform/tests/test_dmc_tools.py b/mcp-servers/viz-platform/tests/test_dmc_tools.py new file mode 100644 index 0000000..9da2ea4 --- /dev/null +++ b/mcp-servers/viz-platform/tests/test_dmc_tools.py @@ -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 diff --git a/mcp-servers/viz-platform/tests/test_theme_tools.py b/mcp-servers/viz-platform/tests/test_theme_tools.py new file mode 100644 index 0000000..3e5dfcf --- /dev/null +++ b/mcp-servers/viz-platform/tests/test_theme_tools.py @@ -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 diff --git a/plugins/viz-platform/.claude-plugin/plugin.json b/plugins/viz-platform/.claude-plugin/plugin.json new file mode 100644 index 0000000..c25d1d6 --- /dev/null +++ b/plugins/viz-platform/.claude-plugin/plugin.json @@ -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"] +} diff --git a/plugins/viz-platform/.mcp.json b/plugins/viz-platform/.mcp.json new file mode 100644 index 0000000..d4540f8 --- /dev/null +++ b/plugins/viz-platform/.mcp.json @@ -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" + } + } +} diff --git a/plugins/viz-platform/README.md b/plugins/viz-platform/README.md new file mode 100644 index 0000000..9aced87 --- /dev/null +++ b/plugins/viz-platform/README.md @@ -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 diff --git a/plugins/viz-platform/agents/component-check.md b/plugins/viz-platform/agents/component-check.md new file mode 100644 index 0000000..805af86 --- /dev/null +++ b/plugins/viz-platform/agents/component-check.md @@ -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. diff --git a/plugins/viz-platform/agents/layout-builder.md b/plugins/viz-platform/agents/layout-builder.md new file mode 100644 index 0000000..dd65483 --- /dev/null +++ b/plugins/viz-platform/agents/layout-builder.md @@ -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 diff --git a/plugins/viz-platform/agents/theme-setup.md b/plugins/viz-platform/agents/theme-setup.md new file mode 100644 index 0000000..f2fd299 --- /dev/null +++ b/plugins/viz-platform/agents/theme-setup.md @@ -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 diff --git a/plugins/viz-platform/claude-md-integration.md b/plugins/viz-platform/claude-md-integration.md new file mode 100644 index 0000000..f7da582 --- /dev/null +++ b/plugins/viz-platform/claude-md-integration.md @@ -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 diff --git a/plugins/viz-platform/commands/chart.md b/plugins/viz-platform/commands/chart.md new file mode 100644 index 0000000..8901cd9 --- /dev/null +++ b/plugins/viz-platform/commands/chart.md @@ -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 diff --git a/plugins/viz-platform/commands/component.md b/plugins/viz-platform/commands/component.md new file mode 100644 index 0000000..c268f9b --- /dev/null +++ b/plugins/viz-platform/commands/component.md @@ -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 diff --git a/plugins/viz-platform/commands/dashboard.md b/plugins/viz-platform/commands/dashboard.md new file mode 100644 index 0000000..7432252 --- /dev/null +++ b/plugins/viz-platform/commands/dashboard.md @@ -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 diff --git a/plugins/viz-platform/commands/initial-setup.md b/plugins/viz-platform/commands/initial-setup.md new file mode 100644 index 0000000..2b5d8d8 --- /dev/null +++ b/plugins/viz-platform/commands/initial-setup.md @@ -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= +VIZ_PLATFORM_PRIMARY_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 | diff --git a/plugins/viz-platform/commands/theme-css.md b/plugins/viz-platform/commands/theme-css.md new file mode 100644 index 0000000..7de53d9 --- /dev/null +++ b/plugins/viz-platform/commands/theme-css.md @@ -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 + +``` + +### 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 diff --git a/plugins/viz-platform/commands/theme-new.md b/plugins/viz-platform/commands/theme-new.md new file mode 100644 index 0000000..1e9c95b --- /dev/null +++ b/plugins/viz-platform/commands/theme-new.md @@ -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 diff --git a/plugins/viz-platform/commands/theme.md b/plugins/viz-platform/commands/theme.md new file mode 100644 index 0000000..4119774 --- /dev/null +++ b/plugins/viz-platform/commands/theme.md @@ -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 diff --git a/plugins/viz-platform/hooks/hooks.json b/plugins/viz-platform/hooks/hooks.json new file mode 100644 index 0000000..6e590e3 --- /dev/null +++ b/plugins/viz-platform/hooks/hooks.json @@ -0,0 +1,10 @@ +{ + "hooks": [ + { + "event": "SessionStart", + "type": "command", + "command": "echo 'viz-platform plugin loaded'", + "timeout": 5000 + } + ] +} diff --git a/plugins/viz-platform/mcp-servers/.doc-guardian-queue b/plugins/viz-platform/mcp-servers/.doc-guardian-queue new file mode 100644 index 0000000..0d12717 --- /dev/null +++ b/plugins/viz-platform/mcp-servers/.doc-guardian-queue @@ -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 diff --git a/plugins/viz-platform/mcp-servers/viz-platform b/plugins/viz-platform/mcp-servers/viz-platform new file mode 120000 index 0000000..bb81f51 --- /dev/null +++ b/plugins/viz-platform/mcp-servers/viz-platform @@ -0,0 +1 @@ +../../../mcp-servers/viz-platform \ No newline at end of file