Claude
Skills
Sign in
โ† Back

Zimlet Modern Development

Included with Lifetime
$97 forever

This skill should be used when the user asks about "modern zimlet", "Preact zimlet", "React zimlet", "zimlet GraphQL", "zimlet slots", "zimlet CLI", "Zimbra X zimlet", "zm-x-web", "zimlet-cli", or mentions developing zimlets for Zimbra Modern Web Client. Covers component-based zimlet development.

Design

What this skill does


# Zimlet Modern Development

Guide for developing zimlets for the Zimbra Modern Web Client using Preact, GraphQL, and the zimlet-cli toolchain.

> **๐Ÿ’ก Essential Tip:** Add `?zimletSlots=show` to your Zimbra URL to visualize all available slot locations in the UI!

## Architecture Overview

Modern zimlets are Preact-based web components:

```
my-zimlet/
โ”œโ”€โ”€ package.json
โ”œโ”€โ”€ zimlet.json           # Zimlet manifest (v2 schema)
โ”œโ”€โ”€ src/
โ”‚   โ”œโ”€โ”€ index.js          # Entry point
โ”‚   โ”œโ”€โ”€ components/       # Preact components
โ”‚   โ””โ”€โ”€ graphql/          # GraphQL queries
โ”œโ”€โ”€ public/
โ”‚   โ””โ”€โ”€ icon.png
โ””โ”€โ”€ dist/                 # Built zimlet
```

### Key Technologies

- **Preact** - React-compatible lightweight framework
- **GraphQL** - Data fetching from Zimbra
- **zimlet-cli** - Build and development tooling
- **Slot system** - UI injection points

## Project Setup

### Using zimlet-cli

```bash
# Install CLI
npm install -g @zimbra/zimlet-cli

# Create new zimlet
zimlet create my-zimlet
cd my-zimlet

# Development server
zimlet watch

# Build for production
zimlet build

# Package for deployment
zimlet package
```

### zimlet.json Manifest

```json
{
  "name": "my-zimlet",
  "version": "1.0.0",
  "description": "My Modern Zimlet",
  "label": "My Zimlet",
  "icon": "icon.png",
  "host": "https://mail.domain.com",
  "slots": {
    "menu": true,
    "routes": true,
    "compose-attachment-buttons": true
  }
}
```

### package.json

```json
{
  "name": "my-zimlet",
  "version": "1.0.0",
  "main": "src/index.js",
  "scripts": {
    "build": "zimlet build",
    "watch": "zimlet watch",
    "package": "zimlet package"
  },
  "dependencies": {
    "@zimbra-client/components": "^1.0.0",
    "@zimbra-client/graphql": "^1.0.0",
    "preact": "^10.0.0"
  }
}
```

## Entry Point and Slots

### src/index.js

```javascript
import { createElement } from 'preact';
import { useCallback, useState } from 'preact/hooks';
import { MenuItem } from '@zimbra-client/components';

// Register zimlet with all slot handlers
export default function Zimlet(context) {
    const { plugins } = context;
    const exports = {};

    // Menu slot - add item to hamburger menu
    exports.menu = {
        handler: function MenuHandler(menu, context) {
            return [
                <MenuItem
                    icon="fa fa-star"
                    onClick={() => context.openSidebar('my-zimlet-panel')}
                >
                    My Zimlet
                </MenuItem>
            ];
        }
    };

    // Routes slot - add custom routes
    exports.routes = {
        handler: function RouteHandler() {
            return [
                {
                    path: '/my-zimlet',
                    component: () => import('./components/MainView')
                }
            ];
        }
    };

    // Sidebar slot
    exports.sidebars = {
        handler: function SidebarHandler() {
            return {
                'my-zimlet-panel': () => import('./components/Sidebar')
            };
        }
    };

    // Compose attachment buttons
    exports['compose-attachment-buttons'] = {
        handler: function AttachmentHandler(props) {
            return [
                <button onClick={() => props.onAttach({ type: 'my-service' })}>
                    Attach from My Service
                </button>
            ];
        }
    };

    // Register with plugin system
    plugins.register('my-zimlet', exports);
}
```

## Available Slots

Modern Web Client provides these injection points:

| Slot | Location | Use Case |
|------|----------|----------|
| `menu` | Hamburger menu | Add navigation items |
| `routes` | App routing | Custom views/pages |
| `sidebars` | Side panels | Contextual panels |
| `compose-attachment-buttons` | Compose toolbar | Custom attachments |
| `mail-action-menu` | Email context menu | Message actions |
| `calendar-action-menu` | Calendar context menu | Event actions |
| `contact-action-menu` | Contact context menu | Contact actions |
| `composer-toolbar` | Rich editor toolbar | Formatting tools |
| `search-bar` | Search area | Search enhancements |

### Slot Handler Patterns

```javascript
// Simple handler returning components
exports['slot-name'] = {
    handler: function(props, context) {
        return <Component {...props} />;
    }
};

// Async component loading
exports['slot-name'] = {
    handler: function() {
        return import('./components/MyComponent').then(m => m.default);
    }
};

// Conditional rendering
exports['slot-name'] = {
    handler: function(props, context) {
        if (!context.account.features.includes('myFeature')) {
            return null;
        }
        return <Component />;
    }
};
```

## Preact Components

### Basic Component

```javascript
// src/components/MainView.js
import { createElement } from 'preact';
import { useState, useEffect } from 'preact/hooks';
import { useGraphQL } from '@zimbra-client/graphql';

export default function MainView({ context }) {
    const [data, setData] = useState(null);

    useEffect(() => {
        // Load data on mount
        fetchData();
    }, []);

    return (
        <div class="my-zimlet-main">
            <h1>My Zimlet</h1>
            {data ? (
                <DataList items={data} />
            ) : (
                <Loading />
            )}
        </div>
    );
}
```

### Using Zimbra Components

```javascript
import {
    Button,
    Card,
    Dialog,
    Icon,
    MenuItem,
    Sidebar,
    TextInput
} from '@zimbra-client/components';

function MyDialog({ visible, onClose }) {
    return (
        <Dialog
            visible={visible}
            title="My Dialog"
            onClose={onClose}
            buttons={[
                { label: 'Cancel', onClick: onClose },
                { label: 'Save', primary: true, onClick: handleSave }
            ]}
        >
            <TextInput
                label="Name"
                value={name}
                onChange={e => setName(e.target.value)}
            />
        </Dialog>
    );
}
```

## GraphQL Integration

### Basic Query

```javascript
import { useQuery } from '@zimbra-client/graphql';
import gql from 'graphql-tag';

const GET_FOLDERS = gql`
    query GetFolders {
        getFolder {
            folders {
                id
                name
                unread
                view
            }
        }
    }
`;

function FolderList() {
    const { data, loading, error } = useQuery(GET_FOLDERS);

    if (loading) return <Loading />;
    if (error) return <Error message={error.message} />;

    return (
        <ul>
            {data.getFolder.folders.map(folder => (
                <li key={folder.id}>
                    {folder.name} ({folder.unread})
                </li>
            ))}
        </ul>
    );
}
```

### Search Messages

```javascript
const SEARCH_MESSAGES = gql`
    query SearchMessages($query: String!, $limit: Int) {
        search(query: $query, types: MESSAGE, limit: $limit) {
            messages {
                id
                subject
                date
                from {
                    address
                    name
                }
            }
        }
    }
`;

function MessageSearch({ searchTerm }) {
    const { data } = useQuery(SEARCH_MESSAGES, {
        variables: { query: searchTerm, limit: 50 }
    });

    return (
        <MessageList messages={data?.search?.messages || []} />
    );
}
```

### Mutations

```javascript
const SEND_MESSAGE = gql`
    mutation SendMessage($message: SendMessageInput!) {
        sendMessage(message: $message) {
            id
        }
    }
`;

function ComposeForm() {
    const [sendMessage] = useMutation(SEND_MESSAGE);

    const handleSend = async () => {
        await sendMessage({
            variables: {
                message: {
                    to: [{ address: '[email protected]' }],
                    subject: 'Hello',
                    text: 'Message body'
                }
      

Related in Design