diff --git a/CLAUDE.md b/CLAUDE.md index fac4a60..669c4b1 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -21,6 +21,7 @@ The codebase organizes into: - `src/prompts/` - MCP prompt implementations with `BasePrompt` abstract class and registry - `src/resources/` - Static reference data (style specs, token scopes, Streets v8 fields) - `src/utils/` - HTTP pipeline, JWT parsing, tracing, and version utilities +- `skills/` - Agent Skills providing domain expertise (cartography, security, style patterns) ## Key Architectural Patterns @@ -38,6 +39,8 @@ The codebase organizes into: **Prompt Registry:** Prompts are registered in `src/prompts/promptRegistry.ts`. To add a new prompt, create the prompt class and add it to the `ALL_PROMPTS` array. The main server automatically registers all prompts with proper Zod schema conversion. +**Agent Skills:** Domain expertise provided through `skills/` directory. Each skill is a folder with `SKILL.md` containing YAML frontmatter and markdown instructions. Skills teach AI assistants about map design (cartography), security (token management), and implementation (style patterns). Skills are discovered by Claude Code, uploadable to Claude API, or usable in Claude.ai. See `skills/README.md` for details. + ## Essential Workflows **Development commands:** diff --git a/README.md b/README.md index deb24ba..d8f64a8 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,6 @@ https://github.com/user-attachments/assets/8b1b8ef2-9fba-4951-bc9a-beaed4f6aff6 - [Hosted MCP Endpoint](#hosted-mcp-endpoint) - [Getting Your Mapbox Access Token](#getting-your-mapbox-access-token) - [Tools](#tools) - - [Prompts](#prompts) - [Documentation Tools](#documentation-tools) - [Reference Tools](#reference-tools) - [Style Management Tools](#style-management-tools) @@ -454,6 +453,40 @@ An array of four numbers representing the bounding box: `[minX, minY, maxX, maxY - "Calculate the bounding box of this GeoJSON file" (then upload a .geojson file) - "What's the bounding box for the coordinates in the uploaded parks.geojson file?" +## Agent Skills + +This repository includes [Agent Skills](https://agentskills.io) that provide domain expertise for building maps with Mapbox. Skills teach AI assistants about map design, security best practices, and common implementation patterns. + +**Available Skills:** + +- **🎨 mapbox-cartography**: Map design principles, color theory, visual hierarchy, typography +- **πŸ” mapbox-token-security**: Token management, scope control, URL restrictions, rotation strategies +- **πŸ“ mapbox-style-patterns**: Common style patterns and layer configurations for typical scenarios +- **πŸ”§ mapbox-integration-patterns**: Framework-specific integration patterns for React, Vue, Svelte, Angular, and vanilla JS + +Skills complement the MCP server by providing expertise (how to think about design) while tools provide capabilities (how to execute actions). + +For complete documentation and usage instructions, see [skills/README.md](./skills/README.md). + +### Using Skills with Claude Code + +To use these skills in Claude Code, create a symlink: + +```bash +mkdir -p .claude +ln -s ../skills .claude/skills +``` + +Or copy to your global skills directory: + +```bash +cp -r skills/* ~/.claude/skills/ +``` + +### Using Skills with Claude API + +Upload skills as zip files via the Skills API. See [Claude API Skills documentation](https://docs.anthropic.com/en/build-with-claude/skills-guide). + ## Prompts MCP Prompts are pre-built workflow templates that guide AI assistants through multi-step tasks. They orchestrate multiple tools in the correct sequence, providing best practices and error handling built-in. diff --git a/skills/README.md b/skills/README.md new file mode 100644 index 0000000..a368976 --- /dev/null +++ b/skills/README.md @@ -0,0 +1,269 @@ +# Mapbox Agent Skills + +This directory contains [Agent Skills](https://agentskills.io) that provide domain expertise and best practices for building maps with Mapbox. These skills complement the MCP server by teaching AI assistants how to make better decisions about map design, security, and implementation patterns. + +## What are Agent Skills? + +Agent Skills are folders containing instructions, scripts, and resources that AI agents can discover and use to perform tasks more effectively. Unlike MCP tools (which provide actions) or prompts (which provide workflows), skills provide **domain expertise** - the "know-how" that helps agents make informed decisions. + +Think of skills as giving Claude a specialized education in cartography, security, and Mapbox development best practices. + +## Available Skills + +### 🎨 mapbox-cartography + +**Expert guidance on map design principles, color theory, visual hierarchy, typography, and cartographic best practices.** + +Use this skill when: + +- Designing a new map style +- Choosing colors for map elements +- Making decisions about visual hierarchy +- Optimizing for specific use cases +- Ensuring accessibility +- Creating themed maps (dark mode, vintage, etc.) + +**Key topics:** + +- Core cartographic principles (visual hierarchy, color theory) +- Typography best practices for maps +- Map context considerations (audience, platform, use case) +- Zoom level strategies +- Color palette templates +- Common mapping scenarios (restaurant finders, real estate, etc.) + +[View skill β†’](./mapbox-cartography/SKILL.md) + +### πŸ” mapbox-token-security + +**Security best practices for Mapbox access tokens, including scope management, URL restrictions, and rotation strategies.** + +Use this skill when: + +- Creating new tokens +- Deciding between public vs secret tokens +- Setting up token restrictions +- Implementing token rotation +- Investigating security incidents +- Conducting security audits + +**Key topics:** + +- Token types and when to use them (public, secret, temporary) +- Scope management (principle of least privilege) +- URL restrictions and patterns +- Token storage and handling +- Rotation strategies +- Monitoring and auditing +- Incident response plans + +[View skill β†’](./mapbox-token-security/SKILL.md) + +### πŸ“ mapbox-style-patterns + +**Common style patterns, layer configurations, and recipes for typical mapping scenarios.** + +Use this skill when: + +- Starting a new map style for a specific use case +- Looking for layer configuration examples +- Implementing common mapping patterns +- Optimizing existing styles +- Need proven recipes for typical scenarios + +**Key topics:** + +- Restaurant/POI finder pattern +- Real estate map pattern +- Data visualization base map pattern +- Navigation/routing map pattern +- Dark mode / night theme pattern +- Layer optimization patterns +- Common modifications (3D buildings, terrain, custom markers) + +[View skill β†’](./mapbox-style-patterns/SKILL.md) + +### πŸ”§ mapbox-integration-patterns + +**Official integration patterns for Mapbox GL JS across popular web frameworks including React, Vue, Svelte, Angular, and vanilla JavaScript.** + +Use this skill when: + +- Setting up Mapbox GL JS in a new project +- Integrating Mapbox into a specific framework +- Adding Mapbox Search functionality +- Implementing proper cleanup and lifecycle management +- Debugging map initialization issues +- Converting between frameworks +- Reviewing code for integration best practices + +**Key topics:** + +- Framework-specific patterns (React, Vue, Svelte, Angular, Next.js) +- Token management (environment variables across frameworks) +- Lifecycle management and cleanup (preventing memory leaks) +- Mapbox Search JS integration +- Common mistakes and how to avoid them +- SSR handling (Angular Universal, Next.js) +- Testing patterns for maps + +**Based on:** Mapbox's official `create-web-app` scaffolding tool + +[View skill β†’](./mapbox-integration-patterns/SKILL.md) + +## How Skills Work with the MCP Server + +The Mapbox MCP DevKit Server and Agent Skills work together: + +| Component | Purpose | Example | +| ---------------- | --------------------- | ---------------------------------------------- | +| **MCP Tools** | Execute actions | `create_style_tool`, `list_tokens_tool` | +| **MCP Prompts** | Orchestrate workflows | `create-and-preview-style` workflow | +| **Agent Skills** | Provide expertise | Map design principles, security best practices | + +**Example workflow:** + +``` +User: "Create a map for my restaurant finder app" + +Without Skills: +Claude β†’ Uses create_style_tool with basic styling + +With Skills: +Claude β†’ +1. [mapbox-cartography skill] Understands restaurant maps need: + - High contrast for restaurant markers + - Muted background (food photos will overlay) + - Clear street labels for navigation + - Mobile-optimized design + +2. [mapbox-token-security skill] Knows to create public token with: + - Only styles:read scope (principle of least privilege) + - URL restrictions to app domain + +3. [mapbox-style-patterns skill] Applies POI Finder pattern: + - Desaturated base map + - Orange markers (#FF6B35) for visibility + - White roads on light gray background + +4. β†’ Uses style_builder_tool with informed design decisions +5. β†’ Uses create_style_tool with optimized configuration +6. β†’ Uses create_token_tool with secure settings +7. β†’ Uses preview_style_tool to generate shareable link +``` + +## Using Skills + +### With Claude Code + +Skills in this directory are automatically discovered by Claude Code when placed in: + +- Project-level: `.claude/skills/` (recommended for this repository) +- Global: `~/.claude/skills/` (available across all projects) + +To use these skills in Claude Code: + +```bash +# Option 1: Symlink from project root (recommended) +mkdir -p .claude +ln -s ../skills .claude/skills + +# Option 2: Copy to global skills directory +cp -r skills/* ~/.claude/skills/ +``` + +For more information, see [Claude Code Skills documentation](https://code.claude.com/docs/en/skills). + +### With Claude API + +Upload skills via the Skills API: + +```bash +# Create a zip of the skill directory +cd skills/mapbox-cartography +zip -r mapbox-cartography.zip . + +# Upload via API +curl https://api.anthropic.com/v1/skills \ + -H "anthropic-version: 2025-08-25" \ + -H "x-api-key: $ANTHROPIC_API_KEY" \ + -H "anthropic-beta: skills-2025-10-02" \ + -F file=@mapbox-cartography.zip +``` + +For more information, see [Claude API Skills guide](https://docs.anthropic.com/en/build-with-claude/skills-guide). + +### With Claude.ai + +1. Go to Settings β†’ Features +2. Upload skill as a zip file +3. Claude will automatically use the skill when relevant + +For more information, see [Using Skills in Claude](https://support.claude.com/en/articles/12512180-using-skills-in-claude). + +## Skill Structure + +Each skill follows the Agent Skills specification: + +``` +skill-name/ +β”œβ”€β”€ SKILL.md # Main skill file (required) +β”‚ β”œβ”€β”€ YAML frontmatter # name, description +β”‚ └── Markdown content # Instructions and guidance +└── [optional files] # Additional resources +``` + +**SKILL.md format:** + +```yaml +--- +name: skill-name +description: What the skill does and when to use it +--- +# Skill Name + +[Instructions and guidance for Claude to follow] +``` + +## Creating Custom Skills + +To create your own Mapbox-related skill: + +1. **Create a new directory** in `skills/` +2. **Create SKILL.md** with YAML frontmatter and instructions +3. **Add reference materials** (optional) +4. **Test with Claude Code** or upload to API +5. **Share with team** via git or Skills API + +**Guidelines:** + +- Keep instructions clear and actionable +- Provide concrete examples +- Include decision trees when applicable +- Reference official Mapbox documentation +- Test with real scenarios + +For more best practices, see [Agent Skills authoring guide](https://platform.claude.com/docs/en/agents-and-tools/agent-skills/best-practices). + +## License + +These skills are provided under the same license as the Mapbox MCP DevKit Server (MIT License). + +## Resources + +- [Agent Skills Overview](https://agentskills.io) +- [Agent Skills Specification](https://github.com/anthropics/skills) +- [Claude Skills Documentation](https://docs.anthropic.com/en/agents-and-tools/agent-skills/overview) +- [Mapbox Documentation](https://docs.mapbox.com) +- [Mapbox Style Specification](https://docs.mapbox.com/style-spec/) + +## Contributing + +We welcome contributions of new skills or improvements to existing ones! Please: + +1. Follow the existing skill structure +2. Test your skill thoroughly +3. Include clear examples +4. Submit a pull request with description + +For questions or suggestions, please open an issue. diff --git a/skills/mapbox-cartography/SKILL.md b/skills/mapbox-cartography/SKILL.md new file mode 100644 index 0000000..4b843e3 --- /dev/null +++ b/skills/mapbox-cartography/SKILL.md @@ -0,0 +1,323 @@ +--- +name: mapbox-cartography +description: Expert guidance on map design principles, color theory, visual hierarchy, typography, and cartographic best practices for creating effective and beautiful maps with Mapbox. Use when designing map styles, choosing colors, or making cartographic decisions. +--- + +# Mapbox Cartography Skill + +This skill provides expert cartographic knowledge to help you design effective, beautiful, and functional maps using Mapbox. + +## Core Cartographic Principles + +### Visual Hierarchy + +Maps must guide the viewer's attention to what matters most: + +- **Most important**: POIs, user location, route highlights +- **Secondary**: Major roads, city labels, landmarks +- **Tertiary**: Minor streets, administrative boundaries +- **Background**: Water, land use, terrain + +**Implementation:** + +- Use size, color intensity, and contrast to establish hierarchy +- Primary features: high contrast, larger symbols, bold colors +- Background features: low contrast, muted colors, smaller text + +### Color Theory for Maps + +**Color Harmony:** + +- **Analogous colors**: Use colors next to each other on color wheel (blue-green-teal) for cohesive designs +- **Complementary colors**: Use opposite colors (blue/orange, red/green) for high contrast emphasis +- **Monochromatic**: Single hue with varying saturation/brightness for elegant, minimal designs + +**Color Psychology:** + +- **Blue**: Water, trust, calm, professional (default for water bodies) +- **Green**: Parks, nature, growth, eco-friendly (vegetation, parks) +- **Red/Orange**: Urgent, important, dining (alerts, restaurants) +- **Yellow**: Caution, highlight, attention (warnings, selected items) +- **Gray**: Neutral, background, roads (infrastructure) + +**Accessibility:** + +- Ensure 4.5:1 contrast ratio for text (WCAG AA) +- Don't rely solely on color to convey information +- Test designs with colorblind simulators +- Avoid red/green combinations for critical distinctions + +### Typography Best Practices + +**Font Selection:** + +- **Sans-serif** (Roboto, Open Sans): Modern, clean, high legibility at small sizes - use for labels +- **Serif** (Noto Serif): Traditional, formal - use sparingly for titles or historic maps +- **Monospace**: Technical data, coordinates + +**Text Sizing:** + +``` +Place labels (cities, POIs): 11-14px +Street labels: 9-11px +Feature labels (parks): 10-12px +Map title: 16-20px +Attribution: 8-9px +``` + +**Label Placement:** + +- Point labels: Center or slightly offset (avoid overlap with symbol) +- Line labels: Follow line curve, repeat for long features +- Area labels: Center in polygon, sized appropriately +- Prioritize: Major features get labels first, minor features labeled if space allows + +### Map Context Considerations + +**Know Your Audience:** + +- **General public**: Simplify, use familiar patterns (Google/Apple style) +- **Technical users**: Include more detail, technical layers, data precision +- **Domain experts**: Show specialized data, use domain-specific symbology + +**Use Case Optimization:** + +- **Navigation**: Emphasize roads, clear hierarchy, route visibility +- **Data visualization**: Muted base map, let data stand out +- **Storytelling**: Guide viewer attention, establish mood with colors +- **Location selection**: Show POIs clearly, provide context +- **Analysis**: Include relevant layers, maintain clarity at different zooms + +**Platform Considerations:** + +- **Mobile**: Larger touch targets (44x44px minimum), simpler designs, readable at arm's length +- **Desktop**: Can include more detail, hover interactions, complex overlays +- **Print**: Higher contrast, larger text, consider CMYK color space +- **Outdoor/Bright**: Higher contrast, avoid subtle grays + +## Mapbox-Specific Guidance + +### Style Layer Best Practices + +**Layer Ordering (bottom to top):** + +1. Background (solid color or pattern) +2. Landuse (parks, residential, commercial) +3. Water bodies (oceans, lakes, rivers) +4. Terrain/hillshade (if using elevation) +5. Buildings (3D or 2D footprints) +6. Roads (highways β†’ local streets) +7. Borders (country, state lines) +8. Labels (place names, street names) +9. POI symbols +10. User-generated content (routes, markers) + +### Zoom Level Strategy + +**Zoom 0-4** (World to Continent): + +- Major country boundaries +- Ocean and sea labels +- Capital cities only + +**Zoom 5-8** (Country to State): + +- State/province boundaries +- Major cities +- Major highways +- Large water bodies + +**Zoom 9-11** (Metro Area): + +- City boundaries +- Neighborhoods +- All highways and major roads +- Parks and landmarks + +**Zoom 12-15** (Neighborhood): + +- All streets +- Building footprints +- POIs (restaurants, shops) +- Street names + +**Zoom 16-22** (Street Level): + +- All detail +- House numbers +- Parking lots +- Fine-grained POIs + +### Color Palette Templates + +**Light Theme (Day/Professional):** + +```json +{ + "background": "#f5f5f5", + "water": "#a0c8f0", + "parks": "#d4e7c5", + "roads": "#ffffff", + "buildings": "#e0e0e0", + "text": "#333333" +} +``` + +**Dark Theme (Night Mode):** + +```json +{ + "background": "#1a1a1a", + "water": "#0d47a1", + "parks": "#2e7d32", + "roads": "#3a3a3a", + "buildings": "#2d2d2d", + "text": "#ffffff" +} +``` + +**High Contrast (Accessibility):** + +```json +{ + "background": "#000000", + "water": "#0066ff", + "parks": "#00ff00", + "roads": "#ffffff", + "buildings": "#808080", + "text": "#ffffff" +} +``` + +**Vintage/Retro:** + +```json +{ + "background": "#f4e8d0", + "water": "#b8d4d4", + "parks": "#c8d4a4", + "roads": "#d4c4a8", + "buildings": "#e4d4c4", + "text": "#4a3828" +} +``` + +## Common Mapping Scenarios + +### Scenario: Restaurant Finder App + +**Requirements:** + +- Restaurants must be highly visible +- Street context for navigation +- Muted background (food photos overlay) + +**Recommendations:** + +- Use bold, warm colors for restaurant markers (red, orange) +- Gray out background (low saturation) +- Keep street labels clear but not dominant +- High contrast for selected restaurant +- Mobile-optimized touch targets + +### Scenario: Real Estate Map + +**Requirements:** + +- Property boundaries clearly visible +- Neighborhood context +- Price differentiation + +**Recommendations:** + +- Use color scale for price ranges (green=affordable, red=expensive) +- Show parks and amenities prominently +- Include school zones if relevant +- Label neighborhoods clearly +- Show transit access + +### Scenario: Data Visualization Overlay + +**Requirements:** + +- Data layer is primary focus +- Base map provides context only +- Multiple data points may cluster + +**Recommendations:** + +- Monochromatic, low-contrast base map +- Use data-ink ratio principle (minimize non-data elements) +- Base map grayscale or single muted hue +- Remove unnecessary labels +- Consider using light base for dark data, vice versa + +### Scenario: Navigation/Routing + +**Requirements:** + +- Route must be unmissable +- Turn-by-turn clarity +- Current location always visible + +**Recommendations:** + +- Route in high-contrast color (blue or purple) +- Animate route line or use dashed pattern +- Large, clear turn indicators +- Dim unrelated features +- User location: pulsing blue dot +- Next turn: prominent arrow/icon + +## Performance Optimization + +**Style Performance:** + +- Minimize layer count (combine similar layers) +- Use expressions instead of multiple layers for variants +- Simplify complex geometries at lower zooms +- Use sprite sheets for repeated icons +- Leverage tileset simplification + +**Loading Speed:** + +- Preload critical zoom levels +- Use style optimization tools +- Minimize external resource calls +- Compress images in sprite sheets + +## Testing Your Design + +**Checklist:** + +- [ ] View at all relevant zoom levels +- [ ] Test in different lighting conditions +- [ ] Check on actual devices (mobile, desktop) +- [ ] Verify color accessibility (colorblind.org) +- [ ] Review with target users +- [ ] Test with real data density +- [ ] Check label collision/overlap +- [ ] Verify performance on slower devices + +## Common Mistakes to Avoid + +1. **Too many colors**: Stick to 5-7 main colors maximum +2. **Insufficient contrast**: Text must be readable +3. **Overcrowding**: Not everything needs a label +4. **Ignoring zoom levels**: Show appropriate detail for scale +5. **Poor label hierarchy**: Organize by importance +6. **Inconsistent styling**: Maintain visual consistency +7. **Neglecting performance**: Complex styles slow rendering +8. **Forgetting mobile**: Test on actual devices + +## When to Use This Skill + +Invoke this skill when: + +- Designing a new map style +- Choosing colors for map elements +- Making decisions about visual hierarchy +- Optimizing for specific use cases +- Troubleshooting visibility issues +- Ensuring accessibility +- Creating themed maps (dark mode, vintage, etc.) diff --git a/skills/mapbox-integration-patterns/SKILL.md b/skills/mapbox-integration-patterns/SKILL.md new file mode 100644 index 0000000..097512f --- /dev/null +++ b/skills/mapbox-integration-patterns/SKILL.md @@ -0,0 +1,969 @@ +--- +name: mapbox-integration-patterns +description: Official integration patterns for Mapbox GL JS across popular web frameworks. Covers setup, lifecycle management, token handling, search integration, and common pitfalls. Based on Mapbox's create-web-app scaffolding tool. +--- + +# Mapbox Integration Patterns Skill + +This skill provides official patterns for integrating Mapbox GL JS into web applications across different frameworks. These patterns are based on Mapbox's `create-web-app` scaffolding tool and represent production-ready best practices. + +## Version Requirements + +### Mapbox GL JS + +**Recommended:** v3.x (latest) + +- **Minimum:** v3.0.0 +- **Why v3.x:** Modern API, improved performance, active development +- **v2.x:** Still supported but deprecated patterns (see migration notes below) + +**Installing via npm (recommended for production):** + +```bash +npm install mapbox-gl@^3.0.0 # Installs latest v3.x +``` + +**CDN (for prototyping only):** + +```html + + + +``` + +⚠️ **Production apps should use npm, not CDN** - ensures consistent versions and offline builds. + +### Framework Requirements + +**React:** + +- Minimum: 19+ (current implementation in create-web-app) +- Recommended: Latest 19.x + +**Vue:** + +- Minimum: 3.x (Composition API recommended) +- Vue 2.x: Use Options API pattern (mounted/unmounted hooks) + +**Svelte:** + +- Minimum: 5+ (current implementation in create-web-app) +- Recommended: Latest 5.x + +**Angular:** + +- Minimum: 19+ (current implementation in create-web-app) +- Recommended: Latest 19.x + +**Next.js:** + +- Minimum: 13.x (App Router) +- Pages Router: 12.x+ + +### Mapbox Search JS + +**Required for search integration:** + +```bash +npm install @mapbox/search-js-react@^1.0.0 # React +npm install @mapbox/search-js-web@^1.0.0 # Other frameworks +``` + +### Version Migration Notes + +**Migrating from v2.x to v3.x:** + +- `accessToken` can now be passed to Map constructor (preferred) +- Improved TypeScript types +- Better tree-shaking support +- No breaking changes to core initialization patterns + +**Example:** + +```javascript +const token = import.meta.env.VITE_MAPBOX_ACCESS_TOKEN; // Use env vars in production + +// v2.x pattern (still works in v3.x) +mapboxgl.accessToken = token; +const map = new mapboxgl.Map({ container: '...' }); + +// v3.x pattern (preferred) +const map = new mapboxgl.Map({ + accessToken: token, + container: '...' +}); +``` + +## Core Principles + +**Every Mapbox GL JS integration must:** + +1. Initialize the map in the correct lifecycle hook +2. Store map instance in component state (not recreate on every render) +3. **Always call `map.remove()` on cleanup** to prevent memory leaks +4. Handle token management securely (environment variables) +5. Import CSS: `import 'mapbox-gl/dist/mapbox-gl.css'` + +## Framework-Specific Patterns + +### React Integration + +**Pattern: useRef + useEffect with cleanup** + +> **Note:** These examples use **Vite** (the bundler used in `create-web-app`). If using Create React App, replace `import.meta.env.VITE_MAPBOX_ACCESS_TOKEN` with `process.env.REACT_APP_MAPBOX_TOKEN`. See the [Token Management Patterns](#token-management-patterns) section for other bundlers. + +```jsx +import { useRef, useEffect } from 'react'; +import mapboxgl from 'mapbox-gl'; +import 'mapbox-gl/dist/mapbox-gl.css'; + +function MapComponent() { + const mapRef = useRef(null); // Store map instance + const mapContainerRef = useRef(null); // Store DOM reference + + useEffect(() => { + mapboxgl.accessToken = import.meta.env.VITE_MAPBOX_ACCESS_TOKEN; + + mapRef.current = new mapboxgl.Map({ + container: mapContainerRef.current, + center: [-71.05953, 42.3629], + zoom: 13 + }); + + // CRITICAL: Cleanup to prevent memory leaks + return () => { + mapRef.current.remove(); + }; + }, []); // Empty dependency array = run once on mount + + return
; +} +``` + +**Key points:** + +- Use `useRef` for both map instance and container +- Initialize in `useEffect` with empty deps `[]` +- **Always return cleanup function** that calls `map.remove()` +- Never initialize map in render (causes infinite loops) + +**React + Search JS:** + +```jsx +import { useRef, useEffect, useState } from 'react'; +import mapboxgl from 'mapbox-gl'; +import { SearchBox } from '@mapbox/search-js-react'; +import 'mapbox-gl/dist/mapbox-gl.css'; + +const accessToken = import.meta.env.VITE_MAPBOX_ACCESS_TOKEN; +const center = [-71.05953, 42.3629]; + +function MapWithSearch() { + const mapRef = useRef(null); + const mapContainerRef = useRef(null); + const [inputValue, setInputValue] = useState(''); + + useEffect(() => { + mapboxgl.accessToken = accessToken; + + mapRef.current = new mapboxgl.Map({ + container: mapContainerRef.current, + center: center, + zoom: 13 + }); + + return () => { + mapRef.current.remove(); + }; + }, []); + + return ( + <> +
+ setInputValue(d)} + marker + /> +
+
+ + ); +} +``` + +--- + +### Vue Integration + +**Pattern: mounted + unmounted lifecycle hooks** + +```vue + + + + + +``` + +**Key points:** + +- Initialize in `mounted()` hook +- Access container via `this.$refs.mapContainer` +- Store map as `this.map` +- **Always implement `unmounted()` hook** to call `map.remove()` + +--- + +### Svelte Integration + +**Pattern: onMount + onDestroy** + +```svelte + + +
+ + +``` + +**Key points:** + +- Use `onMount` for initialization +- Bind container with `bind:this={mapContainer}` +- **Always implement `onDestroy`** to call `map.remove()` +- Can pass `accessToken` directly to Map constructor in Svelte + +--- + +### Angular Integration + +**Pattern: ngOnInit + ngOnDestroy with SSR handling** + +```typescript +import { + Component, + ElementRef, + OnDestroy, + OnInit, + ViewChild, + inject +} from '@angular/core'; +import { isPlatformBrowser, CommonModule } from '@angular/common'; +import { PLATFORM_ID } from '@angular/core'; +import { environment } from '../../environments/environment'; + +@Component({ + selector: 'app-map', + standalone: true, + imports: [CommonModule], + templateUrl: './map.component.html', + styleUrls: ['./map.component.scss'] +}) +export class MapComponent implements OnInit, OnDestroy { + @ViewChild('mapContainer', { static: false }) + mapContainer!: ElementRef; + + private map: any; + private readonly platformId = inject(PLATFORM_ID); + + async ngOnInit(): Promise { + // IMPORTANT: Check if running in browser (not SSR) + if (!isPlatformBrowser(this.platformId)) { + return; + } + + try { + await this.initializeMap(); + } catch (error) { + console.error('Failed to initialize map:', error); + } + } + + private async initializeMap(): Promise { + // Dynamically import to avoid SSR issues + const mapboxgl = (await import('mapbox-gl')).default; + + this.map = new mapboxgl.Map({ + accessToken: environment.mapboxAccessToken, + container: this.mapContainer.nativeElement, + center: [-71.05953, 42.3629], + zoom: 13 + }); + + // Handle map errors + this.map.on('error', (e: any) => console.error('Map error:', e.error)); + } + + // CRITICAL: Clean up on component destroy + ngOnDestroy(): void { + if (this.map) { + this.map.remove(); + } + } +} +``` + +**Template (map.component.html):** + +```html +
+``` + +**Key points:** + +- Use `@ViewChild` to reference map container +- **Check `isPlatformBrowser` before initializing** (SSR support) +- **Dynamically import `mapbox-gl`** to avoid SSR issues +- Initialize in `ngOnInit()` lifecycle hook +- **Always implement `ngOnDestroy()`** to call `map.remove()` +- Handle errors with `map.on('error', ...)` + +--- + +### Vanilla JavaScript (with Vite) + +**Pattern: Module imports with initialization function** + +```javascript +import mapboxgl from 'mapbox-gl'; +import 'mapbox-gl/dist/mapbox-gl.css'; +import './main.css'; + +// Set access token +mapboxgl.accessToken = import.meta.env.VITE_MAPBOX_ACCESS_TOKEN; + +let map; + +/** + * Initialize the map + */ +function initMap() { + map = new mapboxgl.Map({ + container: 'map-container', + center: [-71.05953, 42.3629], + zoom: 13 + }); + + map.on('load', () => { + console.log('Map is loaded'); + }); +} + +// Initialize when script runs +initMap(); +``` + +**HTML:** + +```html +
+``` + +**Key points:** + +- Store map in module-scoped variable +- Initialize immediately or on DOMContentLoaded +- Listen for 'load' event for post-initialization actions + +--- + +### Vanilla JavaScript (No Bundler - CDN) + +**Pattern: Script tag with inline initialization** + +⚠️ **Note:** This pattern is for prototyping only. Production apps should use npm/bundler for version control and offline builds. + +```html + + + + + + Mapbox GL JS - No Bundler + + + + + + + + +
+ + + + + + + + +``` + +**Key points:** + +- ⚠️ **Prototyping only** - not recommended for production +- Replace `3.x.x` with specific version (e.g., `3.7.0`) from [Mapbox docs](https://docs.mapbox.com/mapbox-gl-js/) +- **Don't use `/latest/`** - always pin to specific version for consistency +- Initialize after script loads (bottom of body) +- For production: Use npm + bundler instead + +**Why not CDN for production?** + +- ❌ Network dependency (breaks offline) +- ❌ No version locking (CDN could change) +- ❌ Slower (no bundler optimization) +- ❌ No tree-shaking +- βœ… Use npm for production: `npm install mapbox-gl@^3.0.0` + +--- + +## Token Management Patterns + +### Environment Variables (Recommended) + +Different frameworks use different prefixes for client-side environment variables: + +| Framework/Bundler | Environment Variable | Access Pattern | +| -------------------- | ------------------------------- | ------------------------------------------ | +| **Vite** | `VITE_MAPBOX_ACCESS_TOKEN` | `import.meta.env.VITE_MAPBOX_ACCESS_TOKEN` | +| **Next.js** | `NEXT_PUBLIC_MAPBOX_TOKEN` | `process.env.NEXT_PUBLIC_MAPBOX_TOKEN` | +| **Create React App** | `REACT_APP_MAPBOX_TOKEN` | `process.env.REACT_APP_MAPBOX_TOKEN` | +| **Angular** | `environment.mapboxAccessToken` | Environment files (`environment.ts`) | + +**Vite .env file:** + +```bash +VITE_MAPBOX_ACCESS_TOKEN=pk.eyJ1... +``` + +**Next.js .env.local file:** + +```bash +NEXT_PUBLIC_MAPBOX_TOKEN=pk.eyJ1... +``` + +**Important:** + +- βœ… Always use environment variables for tokens +- βœ… Never commit `.env` files to version control +- βœ… Use public tokens (pk.\*) for client-side apps +- βœ… Add `.env` to `.gitignore` +- βœ… Provide `.env.example` template for team + +**.gitignore:** + +``` +.env +.env.local +.env.*.local +``` + +**.env.example:** + +```bash +VITE_MAPBOX_ACCESS_TOKEN=your_token_here +``` + +--- + +## Mapbox Search JS Integration + +### Search Box Component Pattern + +**Install dependency:** + +```bash +npm install @mapbox/search-js-react # React +npm install @mapbox/search-js-web # Vanilla/Vue/Svelte +``` + +**Note:** Both packages include `@mapbox/search-js-core` as a dependency. You only need to install `-core` directly if building a custom search UI. + +**React Search Pattern:** + +```jsx +import { SearchBox } from '@mapbox/search-js-react'; + +// Inside component: + setInputValue(value)} + proximity={centerCoordinates} // Bias results near center + marker // Show marker for selected result +/>; +``` + +**Key configuration options:** + +- `accessToken`: Your Mapbox public token +- `map`: Map instance (must be initialized first) +- `mapboxgl`: The mapboxgl library reference +- `proximity`: `[lng, lat]` to bias results geographically +- `marker`: Boolean to show/hide result marker +- `placeholder`: Search box placeholder text + +### Positioning Search Box + +**Absolute positioning (overlay):** + +```jsx +
+ +
+``` + +**Common positions:** + +- Top-right: `top: 10px, right: 10px` +- Top-left: `top: 10px, left: 10px` +- Bottom-left: `bottom: 10px, left: 10px` + +--- + +## Common Mistakes to Avoid + +### ❌ Mistake 1: Forgetting to call map.remove() + +```javascript +// BAD - Memory leak! +useEffect(() => { + const map = new mapboxgl.Map({ ... }) + // No cleanup function +}, []) +``` + +```javascript +// GOOD - Proper cleanup +useEffect(() => { + const map = new mapboxgl.Map({ ... }) + return () => map.remove() // βœ… Cleanup +}, []) +``` + +**Why:** Every Map instance creates WebGL contexts, event listeners, and DOM nodes. Without cleanup, these accumulate and cause memory leaks. + +--- + +### ❌ Mistake 2: Initializing map in render + +```javascript +// BAD - Infinite loop in React! +function MapComponent() { + const map = new mapboxgl.Map({ ... }) // Runs on every render + return
+} +``` + +```javascript +// GOOD - Initialize in effect +function MapComponent() { + useEffect(() => { + const map = new mapboxgl.Map({ ... }) + }, []) + return
+} +``` + +**Why:** React components re-render frequently. Creating a new map on every render causes infinite loops and crashes. + +--- + +### ❌ Mistake 3: Not storing map instance properly + +```javascript +// BAD - map variable lost between renders +function MapComponent() { + useEffect(() => { + let map = new mapboxgl.Map({ ... }) + // map variable is not accessible later + }, []) +} +``` + +```javascript +// GOOD - Store in useRef +function MapComponent() { + const mapRef = useRef() + useEffect(() => { + mapRef.current = new mapboxgl.Map({ ... }) + // mapRef.current accessible throughout component + }, []) +} +``` + +**Why:** You need to access the map instance for operations like adding layers, markers, or calling `remove()`. + +--- + +### ❌ Mistake 4: Wrong dependency array in useEffect + +```javascript +// BAD - Re-creates map on every render +useEffect(() => { + const map = new mapboxgl.Map({ ... }) + return () => map.remove() +}) // No dependency array + +// BAD - Re-creates map when props change +useEffect(() => { + const map = new mapboxgl.Map({ center: props.center, ... }) + return () => map.remove() +}, [props.center]) +``` + +```javascript +// GOOD - Initialize once +useEffect(() => { + const map = new mapboxgl.Map({ ... }) + return () => map.remove() +}, []) // Empty array = run once + +// GOOD - Update map property instead +useEffect(() => { + if (mapRef.current) { + mapRef.current.setCenter(props.center) + } +}, [props.center]) +``` + +**Why:** Map initialization is expensive. Initialize once, then use map methods to update properties. + +--- + +### ❌ Mistake 5: Hardcoding token in source code + +```javascript +// BAD - Token exposed in source code +mapboxgl.accessToken = 'pk.eyJ1IjoiZXhhbXBsZSIsImEiOiJjbGV4YW1wbGUifQ.example'; +``` + +```javascript +// GOOD - Use environment variable +mapboxgl.accessToken = import.meta.env.VITE_MAPBOX_ACCESS_TOKEN; +``` + +**Why:** Tokens in source code get committed to version control and exposed publicly. Always use environment variables. + +--- + +### ❌ Mistake 6: Not handling Angular SSR + +```typescript +// BAD - Crashes during server-side rendering +ngOnInit() { + import('mapbox-gl').then(mapboxgl => { + this.map = new mapboxgl.Map({ ... }) + }) +} +``` + +```typescript +// GOOD - Check platform first +ngOnInit() { + if (!isPlatformBrowser(this.platformId)) { + return // Skip map init during SSR + } + + import('mapbox-gl').then(mapboxgl => { + this.map = new mapboxgl.Map({ ... }) + }) +} +``` + +**Why:** Mapbox GL JS requires browser APIs (WebGL, Canvas). Angular Universal (SSR) will crash without platform check. + +--- + +### ❌ Mistake 7: Missing CSS import + +```javascript +// BAD - Map renders but looks broken +import mapboxgl from 'mapbox-gl'; +// Missing CSS import +``` + +```javascript +// GOOD - Import CSS for proper styling +import mapboxgl from 'mapbox-gl'; +import 'mapbox-gl/dist/mapbox-gl.css'; +``` + +**Why:** The CSS file contains critical styles for map controls, popups, and markers. Without it, the map appears broken. + +--- + +## Next.js Specific Patterns + +### App Router (Recommended) + +```typescript +'use client' // Mark as client component + +import { useRef, useEffect } from 'react' +import mapboxgl from 'mapbox-gl' +import 'mapbox-gl/dist/mapbox-gl.css' + +export default function Map() { + const mapRef = useRef() + const mapContainerRef = useRef(null) + + useEffect(() => { + if (!mapContainerRef.current) return + + mapboxgl.accessToken = process.env.NEXT_PUBLIC_MAPBOX_TOKEN! + + mapRef.current = new mapboxgl.Map({ + container: mapContainerRef.current, + center: [-71.05953, 42.36290], + zoom: 13 + }) + + return () => mapRef.current?.remove() + }, []) + + return
+} +``` + +**Key points:** + +- **Must use `'use client'` directive** (maps require browser APIs) +- Use `process.env.NEXT_PUBLIC_*` for environment variables +- Type `mapRef` properly with TypeScript + +### Pages Router (Legacy) + +```typescript +import dynamic from 'next/dynamic' + +// Dynamically import to disable SSR for map component +const Map = dynamic(() => import('../components/Map'), { + ssr: false, + loading: () =>

Loading map...

+}) + +export default function HomePage() { + return +} +``` + +**Key points:** + +- Use `dynamic` import with `ssr: false` +- Provide loading state +- Map component itself follows standard React pattern + +--- + +## Style Configuration + +### Default Center and Zoom Guidelines + +**Recommended defaults:** + +- **Center**: `[-71.05953, 42.36290]` (Boston, MA) - Mapbox HQ +- **Zoom**: `13` for city-level view + +**Zoom level guide:** + +- `0-2`: World view +- `3-5`: Continent/country +- `6-9`: Region/state +- `10-12`: City view +- `13-15`: Neighborhood +- `16-18`: Street level +- `19-22`: Building level + +**Customizing for user location:** + +```javascript +// Use browser geolocation +if (navigator.geolocation) { + navigator.geolocation.getCurrentPosition((position) => { + map.setCenter([position.coords.longitude, position.coords.latitude]); + map.setZoom(13); + }); +} +``` + +--- + +## Testing Patterns + +### Unit Testing Maps + +**Mock mapbox-gl:** + +```javascript +// vitest.config.js or jest.config.js +export default { + setupFiles: ['./test/setup.js'] +}; +``` + +```javascript +// test/setup.js +vi.mock('mapbox-gl', () => ({ + default: { + Map: vi.fn(() => ({ + on: vi.fn(), + remove: vi.fn(), + setCenter: vi.fn(), + setZoom: vi.fn() + })), + accessToken: '' + } +})); +``` + +**Why:** Mapbox GL JS requires WebGL and browser APIs that don't exist in test environments. Mock the library to test component logic. + +--- + +## When to Use This Skill + +Invoke this skill when: + +- Setting up Mapbox GL JS in a new project +- Integrating Mapbox into a specific framework +- Debugging map initialization issues +- Adding Mapbox Search functionality +- Implementing proper cleanup and lifecycle management +- Converting between frameworks (e.g., React to Vue) +- Reviewing code for Mapbox integration best practices + +## Related Skills + +- **mapbox-cartography**: Map design principles and styling +- **mapbox-token-security**: Token management and security +- **mapbox-style-patterns**: Common map style patterns + +## Resources + +- [Mapbox GL JS Documentation](https://docs.mapbox.com/mapbox-gl-js/) +- [Mapbox Search JS Documentation](https://docs.mapbox.com/mapbox-search-js/) +- [create-web-app GitHub](https://github.com/mapbox/create-web-app) diff --git a/skills/mapbox-style-patterns/SKILL.md b/skills/mapbox-style-patterns/SKILL.md new file mode 100644 index 0000000..fdae7b2 --- /dev/null +++ b/skills/mapbox-style-patterns/SKILL.md @@ -0,0 +1,902 @@ +--- +name: mapbox-style-patterns +description: Common style patterns, layer configurations, and recipes for typical mapping scenarios including restaurant finders, real estate, data visualization, navigation, and more. Use when implementing specific map use cases or looking for proven style patterns. +--- + +# Mapbox Style Patterns Skill + +This skill provides battle-tested style patterns and layer configurations for common mapping scenarios. + +## Pattern Library + +### Pattern 1: Restaurant/POI Finder + +**Use case:** Consumer app showing restaurants, cafes, bars, or other points of interest + +**Visual requirements:** + +- POIs must be immediately visible +- Street context for navigation +- Neutral background (photos/content overlay) +- Mobile-optimized + +**Recommended layers:** + +```json +{ + "layers": [ + { + "id": "background", + "type": "background", + "paint": { + "background-color": "#f5f5f5" + } + }, + { + "id": "water", + "type": "fill", + "source": "mapbox-streets", + "source-layer": "water", + "paint": { + "fill-color": "#d4e4f7", + "fill-opacity": 0.6 + } + }, + { + "id": "landuse-parks", + "type": "fill", + "source": "mapbox-streets", + "source-layer": "landuse", + "filter": ["==", "class", "park"], + "paint": { + "fill-color": "#e8f5e8", + "fill-opacity": 0.5 + } + }, + { + "id": "roads-minor", + "type": "line", + "source": "mapbox-streets", + "source-layer": "road", + "filter": ["in", "class", "street", "street_limited"], + "paint": { + "line-color": "#e0e0e0", + "line-width": { + "base": 1.5, + "stops": [ + [12, 0.5], + [15, 2], + [18, 6] + ] + } + } + }, + { + "id": "roads-major", + "type": "line", + "source": "mapbox-streets", + "source-layer": "road", + "filter": ["in", "class", "primary", "secondary", "tertiary"], + "paint": { + "line-color": "#ffffff", + "line-width": { + "base": 1.5, + "stops": [ + [10, 1], + [15, 4], + [18, 12] + ] + } + } + }, + { + "id": "restaurant-markers", + "type": "symbol", + "source": "restaurants", + "layout": { + "icon-image": "restaurant-15", + "icon-size": 1.5, + "icon-allow-overlap": false, + "text-field": ["get", "name"], + "text-offset": [0, 1.5], + "text-size": 12, + "text-allow-overlap": false + }, + "paint": { + "icon-color": "#FF6B35", + "text-color": "#333333", + "text-halo-color": "#ffffff", + "text-halo-width": 2 + } + } + ] +} +``` + +**Key features:** + +- Desaturated base map (doesn't compete with photos) +- High-contrast markers (#FF6B35 orange stands out) +- Clear road network (white on light gray) +- Parks visible but subtle +- Text halos for readability + +### Pattern 2: Real Estate Map + +**Use case:** Property search, neighborhood exploration, real estate listings + +**Visual requirements:** + +- Property boundaries clear +- Neighborhood context visible +- Amenities highlighted (schools, parks, transit) +- Price/property data display + +**Recommended layers:** + +```json +{ + "layers": [ + { + "id": "background", + "type": "background", + "paint": { + "background-color": "#fafafa" + } + }, + { + "id": "parks-green-spaces", + "type": "fill", + "source": "mapbox-streets", + "source-layer": "landuse", + "filter": ["in", "class", "park", "pitch", "playground"], + "paint": { + "fill-color": "#7cb342", + "fill-opacity": 0.3 + } + }, + { + "id": "water", + "type": "fill", + "source": "mapbox-streets", + "source-layer": "water", + "paint": { + "fill-color": "#42a5f5", + "fill-opacity": 0.4 + } + }, + { + "id": "roads", + "type": "line", + "source": "mapbox-streets", + "source-layer": "road", + "paint": { + "line-color": "#e0e0e0", + "line-width": { + "base": 1.2, + "stops": [ + [10, 0.5], + [15, 2], + [18, 6] + ] + } + } + }, + { + "id": "property-boundaries", + "type": "line", + "source": "properties", + "paint": { + "line-color": "#7e57c2", + "line-width": 2, + "line-opacity": 0.8 + } + }, + { + "id": "property-fills", + "type": "fill", + "source": "properties", + "paint": { + "fill-color": [ + "interpolate", + ["linear"], + ["get", "price"], + 200000, + "#4caf50", + 500000, + "#ffc107", + 1000000, + "#f44336" + ], + "fill-opacity": 0.3 + } + }, + { + "id": "school-icons", + "type": "symbol", + "source": "composite", + "source-layer": "poi_label", + "filter": ["==", "class", "school"], + "layout": { + "icon-image": "school-15", + "icon-size": 1.2 + }, + "paint": { + "icon-opacity": 0.8 + } + }, + { + "id": "transit-stops", + "type": "circle", + "source": "transit", + "paint": { + "circle-radius": 6, + "circle-color": "#2196f3", + "circle-stroke-color": "#ffffff", + "circle-stroke-width": 2 + } + } + ] +} +``` + +**Key features:** + +- Properties color-coded by price (greenβ†’yellowβ†’red) +- Parks prominently visible (important for home buyers) +- Schools and transit clearly marked +- Property boundaries visible +- Clean, professional aesthetic + +### Pattern 3: Data Visualization Base Map + +**Use case:** Choropleth maps, heatmaps, data overlays, analytics dashboards + +**Visual requirements:** + +- Minimal base map (data is the focus) +- Context without distraction +- Works with various data overlay colors +- High contrast optional for dark data + +**Recommended layers:** + +```json +{ + "layers": [ + { + "id": "background", + "type": "background", + "paint": { + "background-color": "#f0f0f0" + } + }, + { + "id": "water", + "type": "fill", + "source": "mapbox-streets", + "source-layer": "water", + "paint": { + "fill-color": "#d8d8d8", + "fill-opacity": 0.5 + } + }, + { + "id": "admin-boundaries", + "type": "line", + "source": "mapbox-streets", + "source-layer": "admin", + "filter": ["in", "admin_level", 0, 1, 2], + "paint": { + "line-color": "#999999", + "line-width": { + "base": 1, + "stops": [ + [0, 0.5], + [10, 1], + [15, 2] + ] + }, + "line-dasharray": [3, 2] + } + }, + { + "id": "roads-major-simplified", + "type": "line", + "source": "mapbox-streets", + "source-layer": "road", + "filter": ["in", "class", "motorway", "primary"], + "minzoom": 6, + "paint": { + "line-color": "#cccccc", + "line-width": { + "base": 1.2, + "stops": [ + [6, 0.5], + [10, 1], + [15, 2] + ] + }, + "line-opacity": 0.5 + } + }, + { + "id": "place-labels-major", + "type": "symbol", + "source": "mapbox-streets", + "source-layer": "place_label", + "filter": ["in", "type", "city", "capital"], + "layout": { + "text-field": ["get", "name"], + "text-size": { + "base": 1, + "stops": [ + [4, 10], + [10, 14] + ] + }, + "text-font": ["Open Sans Semibold"] + }, + "paint": { + "text-color": "#666666", + "text-halo-color": "#ffffff", + "text-halo-width": 2 + } + } + ] +} +``` + +**Key features:** + +- Grayscale palette (doesn't interfere with data colors) +- Minimal detail (roads, borders only) +- Major cities labeled for orientation +- Low opacity throughout +- Perfect for overlay data + +### Pattern 4: Navigation/Routing Map + +**Use case:** Turn-by-turn directions, route planning, delivery apps + +**Visual requirements:** + +- Route highly visible +- Current location always clear +- Turn points obvious +- Street names readable +- Performance optimized + +**Recommended layers:** + +```json +{ + "layers": [ + { + "id": "background", + "type": "background", + "paint": { + "background-color": "#ffffff" + } + }, + { + "id": "water", + "type": "fill", + "source": "mapbox-streets", + "source-layer": "water", + "paint": { + "fill-color": "#a8d8ea" + } + }, + { + "id": "landuse", + "type": "fill", + "source": "mapbox-streets", + "source-layer": "landuse", + "paint": { + "fill-color": [ + "match", + ["get", "class"], + "park", + "#d4edda", + "hospital", + "#f8d7da", + "school", + "#fff3cd", + "#e9ecef" + ], + "fill-opacity": 0.5 + } + }, + { + "id": "roads-background", + "type": "line", + "source": "mapbox-streets", + "source-layer": "road", + "paint": { + "line-color": "#333333", + "line-width": { + "base": 1.5, + "stops": [ + [10, 2], + [15, 8], + [18, 20] + ] + }, + "line-opacity": 0.3 + } + }, + { + "id": "roads-foreground", + "type": "line", + "source": "mapbox-streets", + "source-layer": "road", + "paint": { + "line-color": "#ffffff", + "line-width": { + "base": 1.5, + "stops": [ + [10, 1], + [15, 6], + [18, 16] + ] + } + } + }, + { + "id": "route-casing", + "type": "line", + "source": "route", + "paint": { + "line-color": "#0d47a1", + "line-width": { + "base": 1.5, + "stops": [ + [10, 8], + [15, 16], + [18, 32] + ] + }, + "line-opacity": 0.4 + } + }, + { + "id": "route-line", + "type": "line", + "source": "route", + "paint": { + "line-color": "#2196f3", + "line-width": { + "base": 1.5, + "stops": [ + [10, 6], + [15, 12], + [18, 24] + ] + } + } + }, + { + "id": "user-location", + "type": "circle", + "source": "user-location", + "paint": { + "circle-radius": 8, + "circle-color": "#2196f3", + "circle-stroke-color": "#ffffff", + "circle-stroke-width": 3 + } + }, + { + "id": "user-location-pulse", + "type": "circle", + "source": "user-location", + "paint": { + "circle-radius": { + "base": 1, + "stops": [ + [0, 16], + [1, 24] + ] + }, + "circle-color": "#2196f3", + "circle-opacity": { + "base": 1, + "stops": [ + [0, 0.4], + [1, 0] + ] + } + } + }, + { + "id": "turn-arrows", + "type": "symbol", + "source": "route-maneuvers", + "layout": { + "icon-image": ["get", "arrow-type"], + "icon-size": 1.5, + "icon-rotation-alignment": "map", + "icon-rotate": ["get", "bearing"] + } + } + ] +} +``` + +**Key features:** + +- Thick, high-contrast route (blue on white) +- Pulsing user location indicator +- Turn arrows at maneuver points +- Simplified background (focus on route) +- Color-coded land use for context + +### Pattern 5: Dark Mode / Night Theme + +**Use case:** Reduced eye strain, night use, modern aesthetic, battery saving (OLED) + +**Visual requirements:** + +- Dark background +- Reduced brightness +- Maintained contrast +- Readable text +- Comfortable viewing + +**Recommended layers:** + +```json +{ + "layers": [ + { + "id": "background", + "type": "background", + "paint": { + "background-color": "#0a0a0a" + } + }, + { + "id": "water", + "type": "fill", + "source": "mapbox-streets", + "source-layer": "water", + "paint": { + "fill-color": "#1a237e", + "fill-opacity": 0.5 + } + }, + { + "id": "parks", + "type": "fill", + "source": "mapbox-streets", + "source-layer": "landuse", + "filter": ["==", "class", "park"], + "paint": { + "fill-color": "#1b5e20", + "fill-opacity": 0.4 + } + }, + { + "id": "buildings", + "type": "fill", + "source": "mapbox-streets", + "source-layer": "building", + "paint": { + "fill-color": "#1a1a1a", + "fill-opacity": 0.8, + "fill-outline-color": "#2a2a2a" + } + }, + { + "id": "roads-minor", + "type": "line", + "source": "mapbox-streets", + "source-layer": "road", + "filter": ["in", "class", "street", "street_limited"], + "paint": { + "line-color": "#2a2a2a", + "line-width": { + "base": 1.5, + "stops": [ + [12, 0.5], + [15, 2], + [18, 6] + ] + } + } + }, + { + "id": "roads-major", + "type": "line", + "source": "mapbox-streets", + "source-layer": "road", + "filter": ["in", "class", "primary", "secondary", "motorway"], + "paint": { + "line-color": "#3a3a3a", + "line-width": { + "base": 1.5, + "stops": [ + [10, 1], + [15, 4], + [18, 12] + ] + } + } + }, + { + "id": "labels", + "type": "symbol", + "source": "mapbox-streets", + "source-layer": "place_label", + "layout": { + "text-field": ["get", "name"], + "text-size": 12 + }, + "paint": { + "text-color": "#e0e0e0", + "text-halo-color": "#0a0a0a", + "text-halo-width": 2 + } + } + ] +} +``` + +**Key features:** + +- Very dark background (#0a0a0a near-black) +- Subtle color differentiation (deep blues, greens) +- Light text (#e0e0e0) with dark halos +- Reduced opacity throughout +- Easy on eyes in low light + +## Pattern Selection Guide + +### Decision Tree + +**Question 1: What is the primary content?** + +- User-generated markers/pins β†’ **POI Finder Pattern** +- Property data/boundaries β†’ **Real Estate Pattern** +- Statistical/analytical data β†’ **Data Visualization Pattern** +- Routes/directions β†’ **Navigation Pattern** + +**Question 2: What is the viewing environment?** + +- Daytime/office β†’ Light theme +- Night/dark environment β†’ **Dark Mode Pattern** +- Variable β†’ Provide theme toggle + +**Question 3: What is the user's primary action?** + +- Browse/explore β†’ Focus on POIs, rich detail +- Navigate β†’ Focus on roads, route visibility +- Analyze data β†’ Minimize base map, maximize data +- Select location β†’ Clear boundaries, context + +**Question 4: What is the platform?** + +- Mobile β†’ Simplified, larger touch targets, less detail +- Desktop β†’ Can include more detail and complexity +- Both β†’ Design mobile-first, enhance for desktop + +## Layer Optimization Patterns + +### Performance Pattern: Simplified by Zoom + +```json +{ + "id": "roads", + "type": "line", + "source": "mapbox-streets", + "source-layer": "road", + "filter": [ + "step", + ["zoom"], + ["in", "class", "motorway", "trunk"], + 8, + ["in", "class", "motorway", "trunk", "primary"], + 12, + ["in", "class", "motorway", "trunk", "primary", "secondary"], + 14, + true + ], + "paint": { + "line-width": { + "base": 1.5, + "stops": [ + [4, 0.5], + [10, 1], + [15, 4], + [18, 12] + ] + } + } +} +``` + +### Expression Pattern: Data-Driven Styling + +```json +{ + "paint": { + "circle-radius": [ + "interpolate", + ["linear"], + ["get", "population"], + 0, + 3, + 1000, + 5, + 10000, + 8, + 100000, + 12, + 1000000, + 20 + ], + "circle-color": [ + "case", + ["<", ["get", "temperature"], 0], + "#2196f3", + ["<", ["get", "temperature"], 20], + "#4caf50", + ["<", ["get", "temperature"], 30], + "#ffc107", + "#f44336" + ] + } +} +``` + +### Clustering Pattern: Handle Dense POIs + +```json +{ + "id": "clusters", + "type": "circle", + "source": "pois", + "filter": ["has", "point_count"], + "paint": { + "circle-color": [ + "step", + ["get", "point_count"], + "#51bbd6", 10, + "#f1f075", 30, + "#f28cb1" + ], + "circle-radius": [ + "step", + ["get", "point_count"], + 15, 10, + 20, 30, + 25 + ] + } +}, +{ + "id": "cluster-count", + "type": "symbol", + "source": "pois", + "filter": ["has", "point_count"], + "layout": { + "text-field": ["get", "point_count_abbreviated"], + "text-size": 12 + } +} +``` + +## Common Modifications + +### Add 3D Buildings + +```json +{ + "id": "3d-buildings", + "type": "fill-extrusion", + "source": "composite", + "source-layer": "building", + "minzoom": 15, + "paint": { + "fill-extrusion-color": "#aaa", + "fill-extrusion-height": [ + "interpolate", + ["linear"], + ["zoom"], + 15, + 0, + 15.05, + ["get", "height"] + ], + "fill-extrusion-base": [ + "interpolate", + ["linear"], + ["zoom"], + 15, + 0, + 15.05, + ["get", "min_height"] + ], + "fill-extrusion-opacity": 0.6 + } +} +``` + +### Add Terrain/Hillshade + +```json +{ + "sources": { + "mapbox-dem": { + "type": "raster-dem", + "url": "mapbox://mapbox.mapbox-terrain-dem-v1" + } + }, + "layers": [ + { + "id": "hillshade", + "type": "hillshade", + "source": "mapbox-dem", + "paint": { + "hillshade-exaggeration": 0.5, + "hillshade-shadow-color": "#000000" + } + } + ], + "terrain": { + "source": "mapbox-dem", + "exaggeration": 1.5 + } +} +``` + +### Add Custom Markers + +```json +{ + "id": "custom-markers", + "type": "symbol", + "source": "markers", + "layout": { + "icon-image": "custom-marker", + "icon-size": 0.8, + "icon-anchor": "bottom", + "icon-allow-overlap": true, + "text-field": ["get", "name"], + "text-offset": [0, -2], + "text-anchor": "top", + "text-size": 12 + }, + "paint": { + "text-color": "#ffffff", + "text-halo-color": "#000000", + "text-halo-width": 2 + } +} +``` + +## Testing Patterns + +### Visual Regression Checklist + +- [ ] Test at zoom levels: 4, 8, 12, 16, 20 +- [ ] Verify on mobile (375px width) +- [ ] Verify on desktop (1920px width) +- [ ] Test with dense data +- [ ] Test with sparse data +- [ ] Check label collision +- [ ] Verify color contrast (WCAG) +- [ ] Test loading performance + +## When to Use This Skill + +Invoke this skill when: + +- Starting a new map style for a specific use case +- Looking for layer configuration examples +- Implementing common mapping patterns +- Optimizing existing styles +- Need proven recipes for typical scenarios +- Debugging style issues +- Learning Mapbox style best practices diff --git a/skills/mapbox-token-security/SKILL.md b/skills/mapbox-token-security/SKILL.md new file mode 100644 index 0000000..339b116 --- /dev/null +++ b/skills/mapbox-token-security/SKILL.md @@ -0,0 +1,500 @@ +--- +name: mapbox-token-security +description: Security best practices for Mapbox access tokens, including scope management, URL restrictions, rotation strategies, and protecting sensitive data. Use when creating, managing, or advising on Mapbox token security. +--- + +# Mapbox Token Security Skill + +This skill provides security expertise for managing Mapbox access tokens safely and effectively. + +## Token Types and When to Use Them + +### Public Tokens (pk.\*) + +**Characteristics:** + +- Can be safely exposed in client-side code +- Limited to specific public scopes only +- Can have URL restrictions +- Cannot access sensitive APIs + +**When to use:** + +- Client-side web applications +- Mobile apps +- Public-facing demos +- Embedded maps on websites + +**Allowed scopes:** + +- `styles:tiles` - Display style tiles (raster) +- `styles:read` - Read style specifications +- `fonts:read` - Access Mapbox fonts +- `datasets:read` - Read dataset data +- `vision:read` - Vision API access + +### Secret Tokens (sk.\*) + +**Characteristics:** + +- **NEVER expose in client-side code** +- Full API access with any scopes +- Server-side use only +- Can create/manage other tokens + +**When to use:** + +- Server-side applications +- Backend services +- CI/CD pipelines +- Administrative tasks +- Token management + +**Common scopes:** + +- `styles:write` - Create/modify styles +- `styles:list` - List all styles +- `tokens:read` - View token information +- `tokens:write` - Create/modify tokens +- User feedback management scopes + +### Temporary Tokens (tk.\*) + +**Characteristics:** + +- Short-lived (max 1 hour) +- Created by secret tokens +- Single-purpose use +- Automatically expire + +**When to use:** + +- One-time operations +- Temporary delegated access +- Short-lived demos +- Security-conscious workflows + +## Scope Management Best Practices + +### Principle of Least Privilege + +**Always grant the minimum scopes needed:** + +❌ **Bad:** + +```javascript +// Overly permissive - don't do this +{ + scopes: [ + 'styles:read', + 'styles:write', + 'styles:list', + 'styles:delete', + 'tokens:read', + 'tokens:write' + ]; +} +``` + +βœ… **Good:** + +```javascript +// Only what's needed for displaying a map +{ + scopes: ['styles:read', 'fonts:read']; +} +``` + +### Scope Combinations by Use Case + +**Public Map Display (client-side):** + +```json +{ + "scopes": ["styles:read", "fonts:read", "styles:tiles"], + "note": "Public token for map display", + "allowedUrls": ["https://myapp.com/*"] +} +``` + +**Style Management (server-side):** + +```json +{ + "scopes": ["styles:read", "styles:write", "styles:list"], + "note": "Backend style management - SECRET TOKEN" +} +``` + +**Token Administration (server-side):** + +```json +{ + "scopes": ["tokens:read", "tokens:write"], + "note": "Token management only - SECRET TOKEN" +} +``` + +**Read-Only Access:** + +```json +{ + "scopes": ["styles:list", "styles:read", "tokens:read"], + "note": "Auditing/monitoring - SECRET TOKEN" +} +``` + +## URL Restrictions + +### Why URL Restrictions Matter + +URL restrictions limit where a public token can be used, preventing unauthorized usage if the token is exposed. + +### Effective URL Patterns + +βœ… **Recommended patterns:** + +``` +https://myapp.com/* # Production domain +https://*.myapp.com/* # All subdomains +https://staging.myapp.com/* # Staging environment +http://localhost:* # Local development +``` + +❌ **Avoid these:** + +``` +* # No restriction (insecure) +http://* # Any HTTP site (insecure) +*.com/* # Too broad +``` + +### Multiple Environment Strategy + +Create separate tokens for each environment: + +```javascript +// Production +{ + note: "Production - myapp.com", + scopes: ["styles:read", "fonts:read"], + allowedUrls: ["https://myapp.com/*", "https://www.myapp.com/*"] +} + +// Staging +{ + note: "Staging - staging.myapp.com", + scopes: ["styles:read", "fonts:read"], + allowedUrls: ["https://staging.myapp.com/*"] +} + +// Development +{ + note: "Development - localhost", + scopes: ["styles:read", "fonts:read"], + allowedUrls: ["http://localhost:*", "http://127.0.0.1:*"] +} +``` + +## Token Storage and Handling + +### Server-Side (Secret Tokens) + +βœ… **DO:** + +- Store in environment variables +- Use secret management services (AWS Secrets Manager, HashiCorp Vault) +- Encrypt at rest +- Limit access via IAM policies +- Log token usage + +❌ **DON'T:** + +- Hardcode in source code +- Commit to version control +- Store in plaintext configuration files +- Share via email or Slack +- Reuse across multiple services + +**Example: Secure Environment Variable:** + +```bash +# .env (NEVER commit this file) +MAPBOX_SECRET_TOKEN=sk.ey... + +# .gitignore (ALWAYS include .env) +.env +.env.local +.env.*.local +``` + +### Client-Side (Public Tokens) + +βœ… **DO:** + +- Use public tokens only +- Apply URL restrictions +- Use different tokens per app +- Rotate periodically +- Monitor usage + +❌ **DON'T:** + +- Expose secret tokens +- Use tokens without URL restrictions +- Share tokens between unrelated apps +- Use tokens with excessive scopes + +**Example: Safe Client Usage:** + +```javascript +// Public token with URL restrictions - SAFE +const mapboxToken = 'pk.eyJ1IjoiZXhhbXBsZSIsImEiOiJjbGV4YW1wbGUifQ.example'; + +// This token is restricted to your domain +// and only has styles:read scope +mapboxgl.accessToken = mapboxToken; +``` + +## Token Rotation Strategy + +### When to Rotate Tokens + +**Mandatory rotation:** + +- Token exposed in public repository +- Team member leaves with token access +- Suspected compromise or breach +- Service decommissioning +- Compliance requirements + +**Scheduled rotation:** + +- Every 90 days (recommended for production) +- Every 30 days (high-security environments) +- After major deployments +- During security audits + +### Rotation Process + +**Zero-downtime rotation:** + +1. **Create new token** with same scopes +2. **Deploy new token** to canary/staging environment +3. **Verify functionality** with new token +4. **Gradually roll out** to production +5. **Monitor for issues** for 24-48 hours +6. **Revoke old token** after confirmation +7. **Update documentation** with rotation date + +**Emergency rotation:** + +1. **Immediately revoke** compromised token +2. **Create replacement** token +3. **Deploy emergency update** to all services +4. **Notify team** of incident +5. **Investigate** how compromise occurred +6. **Update procedures** to prevent recurrence + +## Monitoring and Auditing + +### Track Token Usage + +**Metrics to monitor:** + +- API request volume per token +- Geographic distribution of requests +- Error rates by token +- Unexpected spike patterns +- Requests from unauthorized domains + +**Alert on:** + +- Usage from unexpected IPs/regions +- Sudden traffic spikes (>200% normal) +- High error rates (>10%) +- Requests outside allowed URLs +- Off-hours access patterns + +### Regular Security Audits + +**Monthly checklist:** + +- [ ] Review all active tokens +- [ ] Verify token scopes are still appropriate +- [ ] Check for unused tokens (revoke if inactive >30 days) +- [ ] Confirm URL restrictions are current +- [ ] Review team member access +- [ ] Check for tokens in public repositories (GitHub scan) +- [ ] Verify documentation is up-to-date + +**Quarterly checklist:** + +- [ ] Rotate production tokens +- [ ] Full token inventory +- [ ] Access control review +- [ ] Update incident response procedures +- [ ] Security training for team + +## Common Security Mistakes + +### 1. Exposing Secret Tokens in Client Code + +❌ **CRITICAL ERROR:** + +```javascript +// NEVER DO THIS - Secret token in client code +const map = new mapboxgl.Map({ + accessToken: 'sk.eyJ1IjoiZXhhbXBsZSIsI...' // SECRET TOKEN +}); +``` + +βœ… **Correct:** + +```javascript +// Public token only in client code +const map = new mapboxgl.Map({ + accessToken: 'pk.eyJ1IjoiZXhhbXBsZSIsI...' // PUBLIC TOKEN +}); +``` + +### 2. Overly Permissive Scopes + +❌ **Too broad:** + +```json +{ + "scopes": ["styles:*", "tokens:*"] +} +``` + +βœ… **Specific:** + +```json +{ + "scopes": ["styles:read"] +} +``` + +### 3. Missing URL Restrictions + +❌ **No restrictions:** + +```json +{ + "scopes": ["styles:read"], + "allowedUrls": [] // Token works anywhere +} +``` + +βœ… **Domain restricted:** + +```json +{ + "scopes": ["styles:read"], + "allowedUrls": ["https://myapp.com/*"] +} +``` + +### 4. Long-Lived Tokens Without Rotation + +❌ **Never rotated:** + +``` +Token created: Jan 2020 +Last rotation: Never +Still in production: Yes +``` + +βœ… **Regular rotation:** + +``` +Token created: Dec 2024 +Last rotation: Dec 2024 +Next rotation: Mar 2025 +``` + +### 5. Tokens in Version Control + +❌ **Committed to Git:** + +```javascript +// config.js (committed to repo) +export const MAPBOX_TOKEN = 'sk.eyJ1IjoiZXhhbXBsZSI...'; +``` + +βœ… **Environment variables:** + +```javascript +// config.js +export const MAPBOX_TOKEN = process.env.MAPBOX_SECRET_TOKEN; +``` + +```bash +# .env (in .gitignore) +MAPBOX_SECRET_TOKEN=sk.eyJ1IjoiZXhhbXBsZSI... +``` + +## Incident Response Plan + +### If a Token is Compromised + +**Immediate actions (first 15 minutes):** + +1. **Revoke the token** via Mapbox dashboard or API +2. **Create replacement token** with different scopes/restrictions if needed +3. **Update all services** using the compromised token +4. **Notify team** via incident channel + +**Investigation (within 24 hours):** 5. **Review access logs** to understand exposure 6. **Check for unauthorized usage** in Mapbox dashboard 7. **Identify root cause** (how was it exposed?) 8. **Document incident** with timeline and impact + +**Prevention (within 1 week):** 9. **Update procedures** to prevent recurrence 10. **Implement additional safeguards** (CI checks, secret scanning) 11. **Train team** on lessons learned 12. **Update documentation** with new security measures + +## Best Practices Summary + +### Security Checklist + +**Token Creation:** + +- [ ] Use public tokens for client-side, secret for server-side +- [ ] Apply principle of least privilege for scopes +- [ ] Add URL restrictions to public tokens +- [ ] Use descriptive names/notes for token identification +- [ ] Document intended use and environment + +**Token Management:** + +- [ ] Store secret tokens in environment variables or secret managers +- [ ] Never commit tokens to version control +- [ ] Rotate tokens every 90 days (or per policy) +- [ ] Remove unused tokens promptly +- [ ] Separate tokens by environment (dev/staging/prod) + +**Monitoring:** + +- [ ] Track token usage patterns +- [ ] Set up alerts for unusual activity +- [ ] Regular security audits (monthly) +- [ ] Review team access quarterly +- [ ] Scan repositories for exposed tokens + +**Incident Response:** + +- [ ] Documented revocation procedure +- [ ] Emergency contact list +- [ ] Rotation process documented +- [ ] Post-incident review template +- [ ] Team training on security procedures + +## When to Use This Skill + +Invoke this skill when: + +- Creating new tokens +- Deciding between public vs secret tokens +- Setting up token restrictions +- Implementing token rotation +- Investigating security incidents +- Conducting security audits +- Training team on token security +- Reviewing code for token exposure