Skill: bigbubbaagent-bot/miro-sdk (version 1.0.0)

--- name: miro-sdk description: Complete Miro Web SDK reference for building web plugins and desktop applications. Covers setup, core APIs (boards, shapes, text, items, selections, events), authentication, real-time collaboration, examples in TypeScript/JavaScript, best practices, and error handling. --- # Miro Web SDK The Miro Web SDK enables you to build web plugins that extend Miro's functionality. Create custom tools, automate workflows, and integrate external data directly into Miro boards. ## Quick Start **Install SDK:** ```bash npm install @mirohq/websdk-cli @mirohq/miro-webplugin ``` **Create Plugin:** ```bash npm create @mirohq/websdk-cli my-plugin cd my-plugin npm start ``` **Hello World Plugin:** ```typescript import { Board } from '@mirohq/miro-webplugin'; miro.onReady(async () => { console.log('Plugin ready!'); // Get current board const board = await miro.board.getInfo(); console.log('Board name:', board.name); // Listen for shape creation miro.board.events.on('item:create', (event) => { console.log('New item:', event.data); }); }); ``` ## Core Capabilities ### 1. Board Interaction - Get board information (name, owner, size) - Listen to real-time events (item creation, updates, deletions) - Manage board selection and viewport - Access board history and undo/redo ### 2. Create Items - Shapes (rectangles, circles, diamonds, etc.) - Text and rich text formatting - Images and embeds - Sticky notes and cards - Frames and groups ### 3. Manipulate Items - Move, resize, rotate items - Change styling (colors, opacity, fonts) - Update content and properties - Apply connectors between shapes - Delete items ### 4. Selection & Interaction - Get selected items - Listen to selection changes - Programmatically select items - Multi-select operations ### 5. User Interaction - Viewport and zoom control - Notification system - Modal dialogs - Context menus - Keyboard shortcuts ### 6. Collaboration Features - Real-time sync with other users - User presence indicators - Shared selections - Live editing ### 7. Data Management - Metadata and custom properties - Board-level storage - Plugin configuration - Event tracking ### 8. Advanced Features - Batch operations (bulk create/update) - Search and filtering - Viewport navigation - Animation support - Performance optimization ## SDK Platforms ### Web Plugin Browser-based plugin running inside Miro app **Install:** ```bash npm install @mirohq/miro-webplugin ``` **Use:** ```typescript import { Board, Ui } from '@mirohq/miro-webplugin'; // Access board const board = await miro.board.getInfo(); // Create UI miro.ui.openPanel({ url: 'panel.html' }); ``` ### Desktop App Standalone desktop application using Electron **Install:** ```bash npm install @mirohq/miro-desktop ``` **Supported Platforms:** - macOS - Windows - Linux ## Architecture ### Plugin Structure ``` my-plugin/ ├── src/ │ ├── index.ts # Main plugin code │ ├── panel.html # UI panel │ ├── panel.ts # Panel logic │ └── styles.css # Styling ├── manifest.json # Plugin metadata ├── package.json └── tsconfig.json ``` ### Event Flow ``` User Action ↓ Browser/Electron ↓ Miro SDK ↓ Event Handler ↓ API Call ↓ Board Update ↓ Real-time Sync ``` ## Authentication ### Web Plugin Auth Uses implicit flow - user already logged into Miro ```typescript miro.onReady(async () => { // User automatically authenticated const user = await miro.currentUser.get(); console.log('Current user:', user.name); }); ``` ### Scopes Define what your plugin can do: ```json { "requiredScopes": [ "board:read", "board:write", "identity:read" ] } ``` **Available Scopes:** - `board:read` - Read board data - `board:write` - Create/edit items - `board:history` - Access history - `identity:read` - Get current user info ## Reference Files See detailed documentation: - `references/setup-installation.md` - Project setup and installation - `references/core-apis.md` - Complete SDK API reference - `references/authentication.md` - Auth patterns and scopes - `references/examples.md` - TypeScript/JavaScript code examples - `references/best-practices.md` - Performance, security, reliability - `references/error-handling.md` - Error types and handling strategies ## API Overview ### Core Objects ```typescript // Board miro.board.getInfo() miro.board.getAllItems() miro.board.createShape(shape) miro.board.getSelection() miro.board.events // Items item.move(deltaX, deltaY) item.resize(width, height) item.update(updates) item.delete() // User miro.currentUser.get() // UI miro.ui.openPanel() miro.ui.openModal() miro.ui.notifyWithButton() // Viewport miro.board.viewport.zoomIn() miro.board.viewport.scrollTo() ``` ## Events ```typescript // Item events miro.board.events.on('item:create', (event) => {}) miro.board.events.on('item:update', (event) => {}) miro.board.events.on('item:delete', (event) => {}) // Selection events miro.board.events.on('selection:change', (event) => {}) // User events miro.board.events.on('user:join', (event) => {}) miro.board.events.on('user:leave', (event) => {}) ``` ## Version History **Latest:** 2.0+ **Support:** Web plugins, Desktop apps, Collaborative features ## Getting Help - **Documentation:** https://developers.miro.com/docs - **API Reference:** https://developers.miro.com/reference - **GitHub:** https://github.com/miroapp/api-clients - **Community:** https://community.miro.com - **Support:** https://support.miro.com ## Tutorials - Build your first plugin - Create interactive shapes - Real-time collaboration - Data integration - Performance optimization - Testing and debugging

path: references/authentication.md
content: # Miro Web SDK - Authentication

## Web Plugin Authentication

Web plugins use implicit authentication - no login flow needed. User already logged into Miro.

### Check if Ready

```typescript
miro.onReady(() => {
  // Plugin is ready
  // User is authenticated
});
```

### Get Current User

```typescript
const user = await miro.currentUser.get();
console.log(user.name, user.email);
```

## Scopes

Define what your plugin can do:

```json
{
  "requiredScopes": [
    "board:read",
    "board:write",
    "identity:read"
  ]
}
```

### Available Scopes

| Scope | Permission |
|-------|-----------|
| `board:read` | Read board data and items |
| `board:write` | Create, edit, delete items |
| `board:history` | Access undo/redo history |
| `identity:read` | Get current user info |
| `identity:write` | Update user profile |

### Scope Dialog

When plugin installs, user sees scope permissions:

```
"My Plugin" is requesting access to:
✓ Read your boards
✓ Create and edit items
✓ Your profile information
```

User must accept to continue.

## Authorization Flow

```
1. User opens plugin
    ↓
2. Browser checks Miro auth
    ↓
3. If not logged in → Miro login
    ↓
4. Check plugin scopes
    ↓
5. If first time → Show permissions dialog
    ↓
6. User accepts
    ↓
7. Plugin loads with access
```

## Handling Unauthorized

```typescript
miro.onReady(async () => {
  try {
    const board = await miro.board.getInfo();
  } catch (error) {
    if (error.code === 'FORBIDDEN') {
      // Insufficient permissions
      console.log('Need board:read scope');
    }
  }
});
```

## Token Management

**SDK handles tokens automatically** - no manual management needed.

### Behind the Scenes

```
miro.board.getInfo()
  ↓
SDK gets access token
  ↓
API request with token
  ↓
Response
```

## Offline Support

Plugin still works when user goes offline:

```typescript
miro.onReady(() => {
  // Can read local cache
  const items = miro.board.getAllItems(); // From cache
  
  // Write operations queue for sync
  miro.board.createShape({...}); // Queued
  
  // When back online, syncs automatically
});
```

## Multi-User Auth

Each user has own context:

```typescript
const user = await miro.currentUser.get();
// Returns logged-in user only
// No access to other users' credentials
```

## Security Practices

### DO

- ✅ Request minimal required scopes
- ✅ Handle auth errors gracefully
- ✅ Validate user permissions
- ✅ Use HTTPS for plugin URL
- ✅ Store secure data server-side

### DON'T

- ❌ Don't store tokens in localStorage
- ❌ Don't request unnecessary scopes
- ❌ Don't expose user data publicly
- ❌ Don't use HTTP (use HTTPS only)
- ❌ Don't hardcode credentials

## Testing Auth

### Test with Developer Team

1. Create Developer team (sandbox)
2. Install plugin in Developer team
3. Test with multiple users if needed
4. 5 collaborator limit for testing

### Permissions Testing

```typescript
async function testScopes() {
  try {
    // Will fail if scope not granted
    await miro.board.createShape({
      x: 0,
      y: 0,
      width: 100,
      height: 100,
      shape: 'rectangle'
    });
  } catch (error) {
    if (error.code === 'FORBIDDEN') {
      console.log('Need board:write scope');
    }
  }
}
```

## Marketplace Auth

For published apps on Miro Marketplace:

1. OAuth consent screen required
2. User reviews scopes
3. Token flow same as development
4. No additional setup needed

## Troubleshooting

### Plugin says "Not Authorized"
- Check scopes in manifest.json
- Try reinstalling plugin
- Check user has board access

### Can't read board data
- Verify `board:read` scope
- Check if board exists
- User might not have access

### Scope not appearing
- Update manifest.json
- Reinstall plugin
- Clear browser cache

### User profile access denied
- Check `identity:read` scope
- May be privacy setting issue



path: references/best-practices.md
content: # Miro Web SDK - Best Practices

## Performance

### 1. Cache getAllItems()

```typescript
// SLOW - Multiple calls
const items1 = await miro.board.getAllItems();
const items2 = await miro.board.getAllItems();

// FAST - Cache result
let itemsCache = null;

async function getItems() {
  if (!itemsCache) {
    itemsCache = await miro.board.getAllItems();
  }
  return itemsCache;
}
```

### 2. Lazy Load Large Datasets

```typescript
// SLOW - Load everything
const allItems = await miro.board.getAllItems();

// FAST - Load as needed
async function* loadItemsLazy(pageSize = 100) {
  let offset = 0;
  let hasMore = true;
  
  while (hasMore) {
    const items = await miro.board.getAllItems();
    // Process items in batches
    yield items.slice(offset, offset + pageSize);
    
    offset += pageSize;
    hasMore = offset < items.length;
  }
}
```

### 3. Batch Operations

```typescript
// SLOW - Multiple API calls
for (const task of tasks) {
  await miro.board.createShape({...});
}

// FAST - Single batch operation
const shapes = await Promise.all(
  tasks.map(task => 
    miro.board.createShape({...})
  )
);
```

### 4. Event Listener Cleanup

```typescript
// DON'T - Memory leak
miro.board.events.on('item:create', handler);

// DO - Clean up
const handler = (event) => {...};
miro.board.events.on('item:create', handler);

// Later:
miro.board.events.off('item:create', handler);
```

### 5. Minimize Viewport Operations

```typescript
// SLOW - Update viewport frequently
for (let i = 0; i < 100; i++) {
  await miro.board.viewport.zoomIn();
}

// FAST - Single viewport operation
await miro.board.viewport.setViewport({
  zoomLevel: 3
});
```

## Security

### 1. Validate User Input

```typescript
// Validate before creating items
function validateShapeData(data) {
  if (!data.content || typeof data.content !== 'string') {
    throw new Error('Invalid content');
  }
  
  if (data.width < 10 || data.width > 10000) {
    throw new Error('Invalid width');
  }
  
  return true;
}

// Use in creation
const data = getUserInput();
validateShapeData(data);
await miro.board.createShape(data);
```

### 2. Check Permissions

```typescript
async function ensurePermission(scope) {
  try {
    // Attempt operation that requires scope
    await testOperation(scope);
    return true;
  } catch (error) {
    if (error.code === 'FORBIDDEN') {
      miro.ui.notify(`Missing ${scope} permission`);
      return false;
    }
    throw error;
  }
}
```

### 3. Sanitize External Data

```typescript
// Don't trust external APIs
async function importFromApi(url) {
  try {
    const response = await fetch(url);
    const data = await response.json();
    
    // Validate structure
    if (!Array.isArray(data.items)) {
      throw new Error('Invalid data format');
    }
    
    // Sanitize content
    for (const item of data.items) {
      item.content = sanitizeHtml(item.content);
    }
    
    return data;
  } catch (error) {
    console.error('Import failed:', error);
    miro.ui.notify('Import failed', 'error');
  }
}
```

### 4. Secure Storage

```typescript
// Store sensitive data server-side
async function saveUserData(data) {
  // Don't store in localStorage
  // Send to secure backend
  const response = await fetch('/api/user-data', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(data)
  });
  
  return response.json();
}
```

## User Experience

### 1. Provide Feedback

```typescript
// Show progress
miro.ui.notify('Processing...');

try {
  const items = await heavyOperation();
  miro.ui.notify('Done!');
} catch (error) {
  miro.ui.notify('Failed: ' + error.message, 'error');
}
```

### 2. Graceful Degradation

```typescript
async function createShapeWithFallback(data) {
  try {
    // Try with full styling
    return await miro.board.createShape({
      ...data,
      style: {
        fillColor: data.color,
        fontSize: 14,
        fontFamily: 'Arial'
      }
    });
  } catch (error) {
    // Fallback: minimal styling
    console.warn('Detailed styling failed, using defaults');
    return await miro.board.createShape({
      ...data,
      style: { fillColor: '#CCCCCC' }
    });
  }
}
```

### 3. Undo/Redo Support

```typescript
// Track changes for undo
class ChangeHistory {
  constructor() {
    this.history = [];
    this.currentIndex = -1;
  }
  
  push(change) {
    // Remove any redo history
    this.history = this.history.slice(0, this.currentIndex + 1);
    this.history.push(change);
    this.currentIndex++;
  }
  
  undo() {
    if (this.currentIndex > 0) {
      this.currentIndex--;
      return this.history[this.currentIndex];
    }
  }
  
  redo() {
    if (this.currentIndex < this.history.length - 1) {
      this.currentIndex++;
      return this.history[this.currentIndex];
    }
  }
}
```

## Code Organization

### 1. Modular Structure

```typescript
// helpers/board.ts
export async function getShapes() {
  return (await miro.board.getAllItems())
    .filter(item => item.type === 'SHAPE');
}

// helpers/ui.ts
export function showSuccess(message) {
  miro.ui.notify(message);
}

// index.ts
import { getShapes } from './helpers/board';
import { showSuccess } from './helpers/ui';

miro.onReady(async () => {
  const shapes = await getShapes();
  showSuccess(`Found ${shapes.length} shapes`);
});
```

### 2. Type Safety

```typescript
// types.ts
interface Shape {
  id: string;
  type: 'SHAPE';
  x: number;
  y: number;
  content: string;
}

// Use in code
async function processShapes(shapes: Shape[]) {
  for (const shape of shapes) {
    console.log(shape.content);
  }
}
```

### 3. Error Handling Hierarchy

```typescript
async function operation() {
  try {
    await miro.board.createShape({...});
  } catch (error) {
    if (error.code === 'FORBIDDEN') {
      handlePermissionError();
    } else if (error.code === 'INVALID_ARGUMENT') {
      handleValidationError(error);
    } else if (error.code === 'NETWORK_ERROR') {
      handleNetworkError();
    } else {
      handleUnknownError(error);
    }
  }
}
```

## Testing

### 1. Test with Developer Team

```typescript
// Use Developer team (sandbox)
// Limited to 3 boards, 5 users
// "Developer team" watermark visible
```

### 2. Mock API for Unit Tests

```typescript
// Mock SDK
const mockMiro = {
  board: {
    getInfo: jest.fn().mockResolvedValue({
      id: 'test-board',
      name: 'Test Board'
    }),
    createShape: jest.fn().mockResolvedValue({
      id: 'shape-1'
    })
  }
};

// Test code
test('creates shape', async () => {
  const shape = await mockMiro.board.createShape({});
  expect(shape.id).toBe('shape-1');
});
```

### 3. Integration Testing

```typescript
// Test with real board
describe('Board Integration', () => {
  it('creates and deletes shape', async () => {
    const shape = await miro.board.createShape({
      x: 0, y: 0, width: 100, height: 100, shape: 'rectangle'
    });
    
    expect(shape.id).toBeDefined();
    
    await shape.delete();
    
    const items = await miro.board.getAllItems();
    expect(items.find(i => i.id === shape.id)).toBeUndefined();
  });
});
```

## Monitoring & Logging

### 1. Error Tracking

```typescript
function setupErrorTracking() {
  window.addEventListener('error', (event) => {
    // Send to monitoring service
    fetch('/api/errors', {
      method: 'POST',
      body: JSON.stringify({
        message: event.message,
        stack: event.error?.stack,
        timestamp: new Date()
      })
    });
  });
}

// Initialize on startup
miro.onReady(() => {
  setupErrorTracking();
});
```

### 2. Performance Monitoring

```typescript
async function measurePerformance(fn) {
  const start = performance.now();
  
  try {
    const result = await fn();
    const duration = performance.now() - start;
    
    console.log(`Operation took ${duration.toFixed(2)}ms`);
    
    if (duration > 1000) {
      console.warn('Slow operation detected');
    }
    
    return result;
  } catch (error) {
    console.error('Operation failed:', error);
    throw error;
  }
}

// Usage
await measurePerformance(() => miro.board.getAllItems());
```

## Deployment

### 1. Environment Configuration

```typescript
const config = {
  development: {
    apiUrl: 'http://localhost:3000'
  },
  production: {
    apiUrl: 'https://api.example.com'
  }
};

const env = process.env.NODE_ENV || 'development';
export const API_URL = config[env].apiUrl;
```

### 2. Feature Flags

```typescript
const features = {
  newUI: process.env.FEATURE_NEW_UI === 'true',
  betaFeatures: process.env.FEATURE_BETA === 'true'
};

if (features.newUI) {
  loadNewUI();
} else {
  loadClassicUI();
}
```



path: references/core-apis.md
content: # Miro Web SDK - Core APIs Reference

## Board API

### Get Board Info

```typescript
const board = await miro.board.getInfo();
// Returns:
// {
//   id: "board-id",
//   name: "Board Name",
//   owner: { id: "user-id", email: "[REDACTED_SECRET]" },
//   createdAt: "2024-01-15T10:30:00Z",
//   updatedAt: "2024-01-20T14:45:00Z",
//   isPublic: false
// }
```

### Get All Items

```typescript
const items = await miro.board.getAllItems();
// Returns array of all board items

// With filter
const shapes = await miro.board.getAllItems({
  type: ['SHAPE']
});
```

### Get Items by ID

```typescript
const item = await miro.board.getItem('item-id');
```

### Get Selection

```typescript
const selected = await miro.board.getSelection();
// Returns array of selected items
```

### Set Selection

```typescript
await miro.board.select(['item-id-1', 'item-id-2']);
```

### Clear Selection

```typescript
await miro.board.deselect();
```

## Item Creation

### Create Shape

```typescript
const shape = await miro.board.createShape({
  x: 0,
  y: 0,
  width: 300,
  height: 200,
  shape: 'rectangle',
  style: {
    fillColor: '#FF0000',
    fillOpacity: 0.8,
    strokeColor: '#000000',
    strokeWidth: 2,
    fontSize: 14
  },
  content: 'My Shape'
});
```

**Shape Types:**
- `rectangle`, `circle`, `diamond`
- `triangle`, `pentagon`, `hexagon`
- `line`, `arrow`, `flowchart_*`

### Create Text

```typescript
const text = await miro.board.createText({
  x: 100,
  y: 100,
  content: 'Hello World',
  style: {
    color: '#000000',
    fontSize: 24,
    fontFamily: 'Arial',
    textAlign: 'center'
  }
});
```

### Create Sticky Note

```typescript
const sticky = await miro.board.createShape({
  x: 200,
  y: 200,
  shape: 'sticky',
  style: {
    fillColor: '#FFFF00'
  },
  content: 'To do: Review design'
});
```

### Create Image

```typescript
const image = await miro.board.createImage({
  x: 0,
  y: 0,
  width: 400,
  height: 300,
  url: 'https://example.com/image.jpg'
});
```

### Create Connector

```typescript
const connector = await miro.board.createConnector({
  startItem: sourceShape,
  endItem: targetShape,
  captions: ['label'],
  style: {
    strokeColor: '#0000FF',
    strokeWidth: 2
  }
});
```

## Item Manipulation

### Update Item

```typescript
await item.update({
  x: 100,
  y: 100,
  width: 400,
  height: 300,
  content: 'Updated content',
  style: {
    fillColor: '#00FF00'
  }
});
```

### Move Item

```typescript
await item.move(100, 50); // deltaX, deltaY
```

### Resize Item

```typescript
await item.resize(500, 400); // width, height
```

### Rotate Item

```typescript
await item.rotate(45); // degrees
```

### Delete Item

```typescript
await item.delete();
```

### Batch Delete

```typescript
const items = await miro.board.getAllItems();
await Promise.all(items.map(item => item.delete()));
```

## Item Properties

```typescript
// Read properties
console.log(item.id);
console.log(item.type); // 'SHAPE', 'TEXT', 'IMAGE', etc.
console.log(item.x, item.y); // Position
console.log(item.width, item.height); // Dimensions
console.log(item.rotation); // Rotation angle
console.log(item.content); // Text content
console.log(item.style); // Style properties
console.log(item.metadata); // Custom data

// Custom metadata
item.metadata.customField = 'value';
await item.update({ metadata: item.metadata });
```

## Events

### Listen to Item Events

```typescript
// Item created
miro.board.events.on('item:create', (event) => {
  console.log('New item:', event.data);
});

// Item updated
miro.board.events.on('item:update', (event) => {
  console.log('Updated:', event.data);
});

// Item deleted
miro.board.events.on('item:delete', (event) => {
  console.log('Deleted:', event.data.id);
});
```

### Selection Events

```typescript
miro.board.events.on('selection:change', (event) => {
  console.log('Selected:', event.data);
});
```

### User Presence

```typescript
miro.board.events.on('user:join', (event) => {
  console.log('User joined:', event.data.name);
});

miro.board.events.on('user:leave', (event) => {
  console.log('User left:', event.data.name);
});
```

### Remove Listeners

```typescript
const handler = (event) => console.log(event);
miro.board.events.on('item:create', handler);

// Later:
miro.board.events.off('item:create', handler);
```

## Viewport API

### Get Viewport

```typescript
const viewport = await miro.board.viewport.get();
// {
//   x, y: Position
//   width, height: Size
//   zoomLevel: number (0.1 - 5.0)
// }
```

### Set Viewport

```typescript
await miro.board.viewport.setViewport({
  x: 0,
  y: 0,
  zoomLevel: 1
});
```

### Zoom

```typescript
await miro.board.viewport.zoomIn(); // 1.5x
await miro.board.viewport.zoomOut(); // 0.7x
await miro.board.viewport.zoomToFit(items); // Fit items
```

### Scroll

```typescript
await miro.board.viewport.scrollTo({ x: 100, y: 100 });
```

## User API

### Get Current User

```typescript
const user = await miro.currentUser.get();
// {
//   id: "user-id",
//   name: "John Doe",
//   email: "[REDACTED_SECRET]",
//   picture: "https://..."
// }
```

## UI API

### Open Side Panel

```typescript
miro.ui.openPanel({
  url: 'panel.html',
  width: 300
});
```

### Open Modal

```typescript
const result = await miro.ui.openModal({
  url: 'modal.html',
  width: 400,
  height: 300
});
```

### Notifications

```typescript
// Simple notification
miro.ui.notify('Task completed!');

// With button
miro.ui.notifyWithButton('Undo?', {
  action: {
    title: 'Undo',
    callback: () => {
      // Handle undo
    }
  }
});

// Error notification
miro.ui.notify('Something went wrong', 'error');
```

### Context Menu

```typescript
miro.board.ui.on('contextmenu', (event) => {
  event.preventDefault();
  // Show custom menu
});
```

## Storage API

### Get/Set Data

```typescript
// Board-level storage
const data = await miro.board.info.getAllMeta();
await miro.board.info.setMeta('key', { value: 'data' });

// User-level storage
const userMeta = await miro.currentUser.getMeta('key');
await miro.currentUser.setMeta('key', { value: 'data' });
```

## Keyboard Shortcuts

```typescript
miro.board.ui.on('keydown', (event) => {
  if (event.key === 'Enter' && event.ctrlKey) {
    // Ctrl+Enter pressed
    handleSubmit();
  }
});
```

## Search & Filter

```typescript
const items = await miro.board.getAllItems({
  type: ['SHAPE', 'TEXT'],
  query: 'Task' // Search content
});
```

## Batch Operations

```typescript
// Create multiple items efficiently
const newItems = await miro.board.batch(async () => {
  const shape1 = await miro.board.createShape({...});
  const shape2 = await miro.board.createShape({...});
  return [shape1, shape2];
});
```

## Performance Tips

- Cache getAllItems() results
- Use event listeners instead of polling
- Batch item creation
- Limit event listener scope
- Clean up listeners when done



path: references/error-handling.md
content: # Miro Web SDK - Error Handling

## Error Types

### Permission Errors

**Code:** `FORBIDDEN`

```typescript
try {
  await miro.board.createShape({...});
} catch (error) {
  if (error.code === 'FORBIDDEN') {
    // User doesn't have required scope
    miro.ui.notify('Missing board:write permission');
  }
}
```

**Solutions:**
- Check scopes in manifest.json
- Reinstall plugin to request permissions
- User needs to accept scope dialog

### Validation Errors

**Code:** `INVALID_ARGUMENT`

```typescript
try {
  await miro.board.createShape({
    x: 'invalid', // Should be number
    y: 0,
    width: 100,
    height: 100
  });
} catch (error) {
  if (error.code === 'INVALID_ARGUMENT') {
    console.error('Invalid shape data:', error.message);
  }
}
```

**Common causes:**
- Wrong data type
- Missing required fields
- Out of range values

### Item Not Found

**Code:** `NOT_FOUND`

```typescript
try {
  await miro.board.getItem('non-existent-id');
} catch (error) {
  if (error.code === 'NOT_FOUND') {
    console.log('Item was deleted or ID is wrong');
  }
}
```

**Solutions:**
- Verify item ID is correct
- Item may have been deleted
- Check board.getAllItems() for valid IDs

### Network Errors

**Code:** `NETWORK_ERROR`

```typescript
try {
  const items = await miro.board.getAllItems();
} catch (error) {
  if (error.code === 'NETWORK_ERROR') {
    miro.ui.notify('Connection lost. Please try again.');
  }
}
```

**Handling:**
- Implement retry logic
- Show offline message
- Queue operations for later

### Rate Limit

**Code:** `RATE_LIMIT`

```typescript
try {
  // Make many API calls
  for (let i = 0; i < 1000; i++) {
    await miro.board.createShape({...});
  }
} catch (error) {
  if (error.code === 'RATE_LIMIT') {
    console.log('Too many requests. Wait before retrying.');
  }
}
```

**Solutions:**
- Implement exponential backoff
- Batch operations
- Use Promise.all() for parallel requests

## Error Handling Patterns

### Basic Try-Catch

```typescript
async function createShape(data) {
  try {
    const shape = await miro.board.createShape(data);
    return shape;
  } catch (error) {
    console.error('Failed to create shape:', error);
    miro.ui.notify('Failed to create shape', 'error');
    throw error;
  }
}
```

### Retry with Backoff

```typescript
async function retryWithBackoff(fn, maxRetries = 3) {
  let lastError;
  
  for (let attempt = 0; attempt < maxRetries; attempt++) {
    try {
      return await fn();
    } catch (error) {
      lastError = error;
      
      // Only retry on network/rate limit errors
      if (error.code !== 'NETWORK_ERROR' && 
          error.code !== 'RATE_LIMIT') {
        throw error;
      }
      
      // Exponential backoff
      const delay = Math.pow(2, attempt) * 1000;
      await new Promise(r => setTimeout(r, delay));
    }
  }
  
  throw lastError;
}

// Usage
const shape = await retryWithBackoff(() => 
  miro.board.createShape({...})
);
```

### Error Recovery

```typescript
async function createShapeWithFallback(data) {
  try {
    // Try with full options
    return await miro.board.createShape({
      ...data,
      style: {
        fillColor: '#FF0000',
        fontSize: 16
      }
    });
  } catch (error) {
    if (error.code === 'INVALID_ARGUMENT') {
      // Fallback: minimal options
      try {
        return await miro.board.createShape({
          x: data.x || 0,
          y: data.y || 0,
          width: data.width || 100,
          height: data.height || 100,
          shape: 'rectangle'
        });
      } catch (fallbackError) {
        miro.ui.notify('Failed to create shape', 'error');
        throw fallbackError;
      }
    }
    throw error;
  }
}
```

### Validation Before API Call

```typescript
interface ShapeData {
  x: number;
  y: number;
  width: number;
  height: number;
  shape: string;
  content?: string;
}

function validateShape(data: any): data is ShapeData {
  if (typeof data.x !== 'number' || data.x < -50000 || data.x > 50000) {
    throw new Error('Invalid x coordinate');
  }
  
  if (typeof data.y !== 'number' || data.y < -50000 || data.y > 50000) {
    throw new Error('Invalid y coordinate');
  }
  
  if (typeof data.width !== 'number' || data.width < 10) {
    throw new Error('Invalid width');
  }
  
  if (typeof data.height !== 'number' || data.height < 10) {
    throw new Error('Invalid height');
  }
  
  const validShapes = ['rectangle', 'circle', 'diamond', 'triangle'];
  if (!validShapes.includes(data.shape)) {
    throw new Error('Invalid shape type');
  }
  
  return true;
}

async function createShape(data: any) {
  try {
    validateShape(data);
    return await miro.board.createShape(data);
  } catch (error) {
    miro.ui.notify('Invalid shape data: ' + error.message, 'error');
    throw error;
  }
}
```

### Error Context Tracking

```typescript
class ErrorContext {
  private context: Record<string, any> = {};
  
  set(key: string, value: any) {
    this.context[key] = value;
  }
  
  get() {
    return { ...this.context };
  }
  
  async run<T>(fn: () => Promise<T>, contextData: Record<string, any>) {
    const previous = this.context;
    this.context = { ...previous, ...contextData };
    
    try {
      return await fn();
    } catch (error) {
      console.error('Error with context:', {
        error: error.message,
        context: this.context
      });
      throw error;
    } finally {
      this.context = previous;
    }
  }
}

const errorContext = new ErrorContext();

// Usage
await errorContext.run(
  () => miro.board.createShape({...}),
  { userId: 'user-123', boardId: 'board-456' }
);
```

## Specific Error Scenarios

### User Offline

```typescript
function setupOfflineHandling() {
  const queue = [];
  
  window.addEventListener('offline', () => {
    miro.ui.notify('You are offline');
  });
  
  window.addEventListener('online', async () => {
    miro.ui.notify('Back online! Syncing...');
    
    while (queue.length > 0) {
      const operation = queue.shift();
      try {
        await operation();
      } catch (error) {
        queue.unshift(operation); // Re-queue
        break;
      }
    }
  });
}
```

### Concurrent Modifications

```typescript
async function handleConcurrentEdit() {
  try {
    await item.update({ content: 'New content' });
  } catch (error) {
    if (error.code === 'CONFLICT') {
      // Item was modified by someone else
      miro.ui.notifyWithButton(
        'Item was modified by another user',
        {
          action: {
            title: 'Reload',
            callback: async () => {
              const updated = await miro.board.getItem(item.id);
              console.log('Reloaded:', updated);
            }
          }
        }
      );
    }
  }
}
```

### Selection Changed

```typescript
async function deleteSelected() {
  const original = await miro.board.getSelection();
  
  if (original.length === 0) {
    miro.ui.notify('Nothing selected');
    return;
  }
  
  for (const item of original) {
    try {
      await item.delete();
    } catch (error) {
      if (error.code === 'NOT_FOUND') {
        // Item already deleted
        console.log('Item already deleted:', item.id);
      } else {
        throw error;
      }
    }
  }
}
```

## Monitoring & Debugging

### Error Logging

```typescript
class ErrorLogger {
  private logs: any[] = [];
  
  log(error: Error, context?: Record<string, any>) {
    this.logs.push({
      message: error.message,
      code: error.code,
      stack: error.stack,
      context,
      timestamp: new Date()
    });
    
    // Send to server
    this.flushIfNeeded();
  }
  
  private async flushIfNeeded() {
    if (this.logs.length > 10) {
      const logs = this.logs.splice(0, 10);
      await fetch('/api/logs', {
        method: 'POST',
        body: JSON.stringify(logs)
      }).catch(console.error);
    }
  }
}

const errorLogger = new ErrorLogger();

// Usage
try {
  // Operation
} catch (error) {
  errorLogger.log(error, { userId: 'user-123' });
}
```

### Error Boundaries

```typescript
// React-like error boundary for Miro SDK
class ErrorBoundary {
  constructor(private onError?: (error: Error) => void) {}
  
  async wrap<T>(fn: () => Promise<T>): Promise<T | null> {
    try {
      return await fn();
    } catch (error) {
      console.error('Error boundary caught:', error);
      
      if (this.onError) {
        this.onError(error as Error);
      }
      
      miro.ui.notify('An error occurred. Please try again.', 'error');
      return null;
    }
  }
}

const boundary = new ErrorBoundary(error => {
  console.error('Global error:', error);
});

// Usage
await boundary.wrap(() => miro.board.createShape({...}));
```



path: references/examples.md
content: # Miro Web SDK - Code Examples

## TypeScript/JavaScript Setup

### Basic Plugin

```typescript
import { Board } from '@mirohq/miro-webplugin';

miro.onReady(async () => {
  console.log('Plugin is ready');
  
  // Get current board info
  const board = await miro.board.getInfo();
  console.log('Current board:', board.name);
});
```

### Create Shape

```typescript
const shape = await miro.board.createShape({
  x: 0,
  y: 0,
  width: 300,
  height: 200,
  shape: 'rectangle',
  style: {
    fillColor: '#FF6B6B',
    fillOpacity: 0.8,
    strokeColor: '#000000',
    strokeWidth: 2,
    fontSize: 16
  },
  content: 'Hello Miro!'
});

console.log('Created shape:', shape.id);
```

### Create Multiple Shapes

```typescript
async function createTaskBoard() {
  const tasks = [
    'Design',
    'Development', 
    'Testing',
    'Deployment'
  ];
  
  let x = 0;
  const shapes = [];
  
  for (const task of tasks) {
    const shape = await miro.board.createShape({
      x: x,
      y: 0,
      width: 200,
      height: 150,
      shape: 'rectangle',
      style: { fillColor: '#4A90E2' },
      content: task
    });
    
    shapes.push(shape);
    x += 250; // Space them out
  }
  
  return shapes;
}

await createTaskBoard();
```

### Listen to Events

```typescript
miro.board.events.on('item:create', (event) => {
  console.log('New item created:', event.data);
});

miro.board.events.on('item:update', (event) => {
  console.log('Item updated:', event.data);
});

miro.board.events.on('selection:change', (event) => {
  console.log('Selection changed:', event.data);
});
```

### Process Selected Items

```typescript
async function processSelection() {
  const selected = await miro.board.getSelection();
  
  for (const item of selected) {
    if (item.type === 'SHAPE') {
      // Change color
      await item.update({
        style: {
          fillColor: '#00FF00'
        }
      });
      
      // Move down
      await item.move(0, 100);
    }
  }
}
```

### Create Side Panel

**main.ts:**
```typescript
async function openPanel() {
  miro.ui.openPanel({
    url: 'panel.html',
    width: 300
  });
}

// Button listener
document.getElementById('open-panel').addEventListener('click', openPanel);
```

**panel.html:**
```html
<div id="panel">
  <h2>Task Creator</h2>
  <input id="task-input" type="text" placeholder="Task name">
  <button id="create-btn">Create Task</button>
</div>

<script>
  document.getElementById('create-btn').addEventListener('click', async () => {
    const input = document.getElementById('task-input');
    const taskName = input.value;
    
    // Create shape in main board
    await miro.board.createShape({
      x: 0,
      y: 0,
      width: 200,
      height: 100,
      shape: 'sticky',
      content: taskName
    });
    
    input.value = '';
  });
</script>
```

### Real-time Collaboration

```typescript
miro.board.events.on('user:join', (event) => {
  console.log(`${event.data.name} joined the board`);
  
  // Show notification
  miro.ui.notify(`Welcome ${event.data.name}!`);
});

miro.board.events.on('item:create', (event) => {
  // Show who created what
  const creator = event.data.createdBy?.name || 'Unknown';
  console.log(`${creator} created ${event.data.type}`);
});
```

### Search and Filter

```typescript
async function findAllTasks() {
  const items = await miro.board.getAllItems();
  
  const tasks = items.filter(item => 
    item.type === 'SHAPE' && 
    item.content?.includes('TODO')
  );
  
  console.log('Found tasks:', tasks.length);
  return tasks;
}

const tasks = await findAllTasks();
await miro.board.select(tasks.map(t => t.id));
```

### Export Board Data

```typescript
async function exportBoardData() {
  const items = await miro.board.getAllItems();
  
  const data = items.map(item => ({
    id: item.id,
    type: item.type,
    content: item.content,
    position: { x: item.x, y: item.y },
    size: { width: item.width, height: item.height }
  }));
  
  // Download as JSON
  const json = JSON.stringify(data, null, 2);
  const blob = new Blob([json], { type: 'application/json' });
  const url = URL.createObjectURL(blob);
  
  const a = document.createElement('a');
  a.href = url;
  a.download = 'board-export.json';
  a.click();
}
```

### Custom Menu

```typescript
document.addEventListener('contextmenu', (event) => {
  event.preventDefault();
  
  // Show custom context menu
  const menu = document.createElement('div');
  menu.className = 'custom-menu';
  menu.style.position = 'absolute';
  menu.style.left = event.clientX + 'px';
  menu.style.top = event.clientY + 'px';
  
  menu.innerHTML = `
    <div class="menu-item" data-action="copy">Copy</div>
    <div class="menu-item" data-action="paste">Paste</div>
    <div class="menu-item" data-action="delete">Delete</div>
  `;
  
  document.body.appendChild(menu);
  
  menu.addEventListener('click', (e) => {
    const action = e.target.dataset.action;
    handleMenuAction(action);
  });
});
```

### Data Persistence

```typescript
async function savePluginData() {
  const board = await miro.board.getInfo();
  
  const data = {
    timestamp: new Date(),
    boardId: board.id,
    settings: {
      theme: 'dark',
      autoSave: true
    }
  };
  
  // Store in board metadata
  await miro.board.info.setMeta('plugin-settings', data);
}

async function loadPluginData() {
  const data = await miro.board.info.getMeta('plugin-settings');
  console.log('Loaded settings:', data);
  return data;
}
```

### Error Handling

```typescript
async function safeCreateShape(shapeData) {
  try {
    const shape = await miro.board.createShape(shapeData);
    miro.ui.notify('Shape created successfully!');
    return shape;
  } catch (error) {
    if (error.code === 'INVALID_ARGUMENT') {
      miro.ui.notify('Invalid shape data', 'error');
    } else if (error.code === 'FORBIDDEN') {
      miro.ui.notify('You don\'t have permission', 'error');
    } else {
      miro.ui.notify('Failed to create shape', 'error');
      console.error(error);
    }
  }
}
```

### Keyboard Shortcuts

```typescript
document.addEventListener('keydown', (event) => {
  // Ctrl+S or Cmd+S
  if ((event.ctrlKey || event.metaKey) && event.key === 's') {
    event.preventDefault();
    saveBoard();
  }
  
  // Delete key
  if (event.key === 'Delete') {
    deleteSelected();
  }
});

async function deleteSelected() {
  const selected = await miro.board.getSelection();
  await Promise.all(selected.map(item => item.delete()));
}
```

### Batch Operations

```typescript
async function createWorkflow() {
  // Create multiple connected shapes
  const shapes = [];
  const positions = [
    { x: 0, y: 0, label: 'Start' },
    { x: 300, y: 0, label: 'Process' },
    { x: 600, y: 0, label: 'End' }
  ];
  
  for (const pos of positions) {
    const shape = await miro.board.createShape({
      x: pos.x,
      y: pos.y,
      width: 150,
      height: 100,
      shape: 'rectangle',
      content: pos.label
    });
    shapes.push(shape);
  }
  
  // Connect shapes
  for (let i = 0; i < shapes.length - 1; i++) {
    await miro.board.createConnector({
      startItem: shapes[i],
      endItem: shapes[i + 1]
    });
  }
}
```

### Performance Optimization

```typescript
// Cache board data
let cachedItems = null;

async function getItemsCached() {
  if (!cachedItems) {
    cachedItems = await miro.board.getAllItems();
  }
  return cachedItems;
}

// Invalidate cache on changes
miro.board.events.on('item:create', () => {
  cachedItems = null;
});

miro.board.events.on('item:update', () => {
  cachedItems = null;
});

miro.board.events.on('item:delete', () => {
  cachedItems = null;
});
```



path: references/setup-installation.md
content: # Miro Web SDK - Setup & Installation

## Prerequisites

- Node.js 14+ and npm/yarn
- A Miro account
- A Developer team (sandbox environment)
- Basic TypeScript/JavaScript knowledge

## Step 1: Create Developer Team

**Purpose:** Isolated sandbox for testing without affecting production boards

1. Go to https://miro.com/app/settings/user-profile
2. Click "Create Developer team"
3. Name it (e.g., "My Plugin Dev")
4. You're now in sandbox mode

**Limitations:**
- 3 boards maximum
- 5 collaborators maximum
- "Developer team" watermark on boards

## Step 2: Create App

1. Visit https://miro.com/app/settings/user-profile/apps
2. Click "Create new app"
3. Select "Web plugin"
4. Name your app
5. Accept terms and create

**You'll get:**
- App ID
- Redirect URI
- Scopes configuration

## Step 3: Install SDK CLI

```bash
npm create @mirohq/websdk-cli my-plugin
cd my-plugin
npm install
```

**What gets created:**
- `manifest.json` - App metadata
- `src/index.ts` - Main plugin code
- `src/panel.html` - UI panel
- `package.json` - Dependencies
- `tsconfig.json` - TypeScript config

## Step 4: Configure App

**manifest.json:**
```json
{
  "name": "My Plugin",
  "version": "1.0.0",
  "description": "My awesome plugin",
  "minWallOrigin": "localhost:3000",
  "appUrl": "http://localhost:3000/index.html",
  "requiredScopes": [
    "board:read",
    "board:write",
    "identity:read"
  ],
  "sdkVersion": ">=2.0.0"
}
```

**Key Settings:**
- `name` - Display name
- `appUrl` - Entry point for plugin
- `requiredScopes` - Permissions needed
- `minWallOrigin` - Trusted origins (dev: localhost)

## Step 5: Start Development Server

```bash
npm start
```

**Output:**
```
Webpack 5.x.x compiled with X warning(s)
Plugin started at http://localhost:3000
```

Visit http://localhost:3000 in browser to test.

## Step 6: Install in Miro

1. Open Miro app
2. Go to "Settings" → "Apps & integrations"
3. Click "Install app from URL"
4. Paste: `http://localhost:3000`
5. Click Install

**In Developer team only** - won't install in production teams yet.

## Directory Structure

```
my-plugin/
├── src/
│   ├── index.ts              # Main plugin entry
│   ├── index.html            # HTML template
│   ├── panel.ts              # Side panel logic
│   ├── panel.html            # Panel UI
│   └── styles.css            # CSS styles
├── public/
│   └── (static assets)
├── dist/
│   └── (built files)
├── manifest.json             # Plugin config
├── package.json              # Dependencies
├── tsconfig.json             # TypeScript config
└── webpack.config.js         # Build config
```

## Building for Production

```bash
npm run build
```

**Creates:**
- `dist/index.html` - Bundled plugin
- Ready to deploy to web server

**Configuration:**
```json
{
  "appUrl": "https://my-plugin.example.com"
}
```

## Deploying Plugin

### Option 1: Vercel (Recommended)

```bash
npm install -g vercel
vercel
```

**Updates manifest:**
```json
{
  "appUrl": "https://my-plugin.vercel.app"
}
```

### Option 2: GitHub Pages

```bash
npm run build
# Push dist/ to GitHub Pages
```

### Option 3: Docker

```dockerfile
FROM node:18-alpine
WORKDIR /app
COPY . .
RUN npm install && npm run build
EXPOSE 3000
CMD ["npm", "start"]
```

### Option 4: AWS S3 + CloudFront

```bash
npm run build
aws s3 sync dist/ s3://my-bucket/
```

## Installing in Production

Once tested in Developer team:

1. Update `manifest.json` with production URL
2. Submit to Miro Marketplace (optional)
3. Or distribute app URL directly

**Direct Distribution:**
```
https://miro.com/app/install-app/?install_source=custom_direct_install
```

## Environment Variables

**Development (.env.local):**
```
VITE_APP_ID=your-app-id
VITE_MIRO_VERSION=2.0.0
DEBUG=true
```

**Production (.env.production):**
```
VITE_APP_ID=your-app-id
VITE_MIRO_VERSION=2.0.0
DEBUG=false
API_URL=https://api.example.com
```

**Usage in Code:**
```typescript
const appId = import.meta.env.VITE_APP_ID;
const isDebug = import.meta.env.DEBUG === 'true';
```

## SDK Versions

| Version | Status | Node | Features |
|---------|--------|------|----------|
| 2.x | Current | 14+ | Latest features, types |
| 1.x | Legacy | 12+ | Deprecated, use 2.x |

**Upgrade from 1.x to 2.x:**
```bash
npm update @mirohq/miro-webplugin@2
```

## Troubleshooting

### "Plugin not loading"
- Check `localhost:3000` is accessible
- Verify manifest.json syntax
- Check browser console for errors

### "CORS errors"
- Add domain to `minWallOrigin`
- Enable CORS on API endpoints
- Check redirect URLs

### "Scopes not working"
- Verify scopes in manifest.json
- User must accept scope permissions
- Try uninstalling and reinstalling app

### "Build fails"
```bash
# Clear cache
rm -rf node_modules dist
npm install
npm run build
```

## Next Steps

1. Follow tutorials in references
2. Build your first shape
3. Add event listeners
4. Create UI panel
5. Test with multiple users
6. Deploy to production



Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (HIGH)