# Dhwani Plugin Development Guide (AGENTS.md)

This document is a comprehensive developer reference guide for any human or AI coding agent to understand, build, and package plugins for the **Dhwani Note Taking** editor.

---

## Reference Examples

Complete source code and pre-built ZIP packages for reference plugins are available in the [examples/](examples) directory:
* **[badge_plugin/](examples/badge_plugin)**: A badge plugin demonstrating a custom Tiptap inline node view, color/style toolbar customization with color pickers, and various badge styles (Solid, Outline, Soft, Dash).
* **[carousel_plugin/](examples/carousel_plugin)**: An image carousel plugin demonstrating attachment saving, file uploads, and ProseMirror attribute serialization.
* **[example_plugin_for_testing/](examples/example_plugin_for_testing)**: A callout block plugin demonstrating custom Tiptap nodes, editable nested text blocks, and slash commands.
* **[search_link_plugin/](examples/search_link_plugin)**: A search and link plugin demonstrating global note searches, custom React-based interactive search cards, keyboard navigation, and inserting page links using the `dhwani://` protocol.
* **[task_manager_plugin/](examples/task_manager_plugin)**: A Todoist-like task board interact plugin demonstrating custom HTML canvases, SQLite project/task database integrations, and deep-linking into the host app.
* **[weather_plugin/](examples/weather_plugin)**: A live weather forecast card demonstrating external API calls (`makeHttpCall`) with query parameters and timeout configurations.
* **[notebook_explorer_plugin/](examples/notebook_explorer_plugin)**: A custom board node plugin displaying workspace notebooks, enabling inline notebook creation, and listing page titles/types inside clicked notebooks.
* **[database_analytics_plugin/](examples/database_analytics_plugin)**: A custom board node plugin visualizing workspace size, density, and page formats using animated Chart.js graphs.
* **[workflow_step_plugin/](examples/workflow_step_plugin)**: An automated action block plugin styled with a clean SaaS workflow card aesthetic, permitting inline brand switches (Slack, Webhook, Email, Stripe) and live test runs.
* **[weather_board_node_plugin/](examples/weather_board_node_plugin)**: A weather forecast card plugin querying meteorological APIs, responding to user settings (default city), and dynamically syncing its colors to match DaisyUI application themes in real-time.
* **Pre-built Zips**: Complete, ready-to-install packages are available at [dhwani-badge-plugin.zip](examples/dhwani-badge-plugin.zip), [daisyui-carousel-plugin.zip](examples/daisyui-carousel-plugin.zip), [tip-callout-plugin.zip](examples/tip-callout-plugin.zip), [search-link-plugin.zip](examples/search-link-plugin.zip), [task-manager-plugin.zip](examples/task-manager-plugin.zip), [weather-plugin.zip](examples/weather-plugin.zip), [notebook-explorer-plugin.zip](examples/notebook-explorer-plugin.zip), [database-analytics-plugin.zip](examples/database-analytics-plugin.zip), [workflow-step-plugin.zip](examples/workflow-step-plugin.zip), and [weather-board-node-plugin.zip](examples/weather-board-node-plugin.zip).

---

## 1. Plugin Architecture Overview

Dhwani's plugins allow developers to inject custom Tiptap Node/Mark views and dropdown Slash Commands into the rich-text page editor.

---

## 2. Directory Layout of a Plugin

Every plugin must be structured inside a ZIP archive containing the following layout:

```
my-custom-plugin/
├── application.json             ← Plugin metadata manifest (with JSON schema)
├── package.json                 ← Node package dependencies config (dev typings)
├── zip-plugin.sh                ← Helper shell script to bundle the ZIP
├── assets/
│   ├── icon.svg                 ← Main plugin icon shown in the Settings list
│   └── custom-node.svg          ← Icon displayed in the dropdown Slash Command
└── dist/
    └── custom-node.js           ← Compiled self-invoking JS bundle containing the React component
```

---

## 3. The Manifest Configuration (`application.json`)

To enable autocomplete and validation in code editors (like VS Code), include the `$schema` reference pointing to the public unpkg CDN.

```json
{
  "$schema": "https://unpkg.com/@dhwaniapp/plugin-sdk@latest/plugin-manifest.schema.json",
  "display_name": "My Custom Node",
  "system_name": "my.custom.node.plugin",
  "version": "1.0.0",
  "description": "Inserts a custom styling container in your notes.",
  "author": "Developer Name",
  "license": "MIT",
  "permissions_required": [
    "use_page_extention",
    "use_interact"
  ],
  "icon_in_svg": "assets/icon.svg",
  "documentation_md_file": "doc/readme.md",
  "extentions": [
    {
      "type": "page",
      "extention_file": "dist/custom-node.js",
      "extention_svg_icon": "assets/custom-node.svg",
      "extention_display_name": "My Custom Block"
    }
  ],
  "interacts": [
    {
      "interact_html_file": "dist/custom-canvas.html",
      "interact_svg_icon": "assets/custom-canvas.svg",
      "interact_display_name": "My Custom Canvas"
    }
  ],
  "board_nodes": [
    {
      "node_html_file": "dist/my-node.html",
      "node_svg_icon": "assets/my-node.svg",
      "node_display_name": "Custom Board Node"
    }
  ],
  "settings": [
    {
      "id": "api_key",
      "type": "string",
      "display_name": "API Key",
      "details": "The API Key for accessing third-party services.",
      "default_value": ""
    },
    {
      "id": "enable_feature",
      "type": "boolean",
      "display_name": "Enable Extra Feature",
      "details": "Toggle to enable or disable advanced features.",
      "default_value": true
    }
  ]
}
```

* **`permissions_required`:** Must include `"use_page_extention"` for Tiptap editor nodes, `"use_interact"` for interactive canvases, or `"use_board_nodes"` for custom board nodes. The following system permissions are available for request in Dhwani:
  - `"use_page_extention"`: Add custom page views and Tiptap editor nodes.
  - `"use_interact"`: Add custom interactive canvases (Interact views).
  - `"use_board_nodes"`: Add custom interactive board nodes to the visual canvas.
  - `"save_attachment"`: Save images and files as page attachments.
  - `"delete_attachment"`: Delete saved page attachments.
  - `"make_http_calls"`: Make external HTTP requests to outside services.
  - **Note & Notebook Operations**:
    - `"get_all_notes"`: Retrieve all notebooks.
    - `"get_note_by_id"`: Retrieve details of a specific notebook.
    - `"create_note"`: Create new notebooks.
    - `"update_note"`: Update notebook title and description.
    - `"update_note_icon"`: Update the icon of a notebook.
    - `"search_note_by_title"`: Search notebooks by title.
    - `"delete_note_by_id"`: Delete notebooks.
  - **Content & Page Hierarchy Operations**:
    - `"create_content"`: Create new pages, chats, or boards.
    - `"update_content"`: Rename pages, chats, or boards.
    - `"delete_content"`: Delete pages, chats, or boards.
    - `"get_content_tree"`: Retrieve the sidebar page hierarchy of a notebook.
    - `"search_content_by_title"`: Search pages by title within a notebook.
    - `"search_content_globally"`: Search all pages globally.
    - `"get_content_by_id"`: Retrieve metadata of a specific page or board.
    - `"get_all_contents_for_note"`: Retrieve all pages and boards in a notebook.
  - **Content Body & Storage Operations**:
    - `"get_content_body"`: Read the body contents of pages or boards.
    - `"create_or_update_content_body"`: Write or update the body contents of pages or boards.
    - `"is_content_body_exists"`: Check if a page has a body payload.
  - **Custom Interact Key-Value Data Operations**:
    - `"get_interact_data"`: Read custom key-value settings or progress data.
    - `"set_interact_data"`: Save custom key-value settings or progress data.
    - `"delete_interact_data"`: Delete custom key-value data.
    - `"get_all_interact_data"`: Retrieve all custom key-value data for a page.
  - **Discussion Comments Operations**:
    - `"get_comments_by_content_id"`: Retrieve discussion comments for a page.
    - `"create_comment"`: Post new discussion comments.
  - **Projects & Timelines Operations**:
    - `"get_all_projects"`: Retrieve all projects.
    - `"get_project_by_id"`: Retrieve details of a specific project.
    - `"create_project"`: Create new projects.
    - `"get_tasks_by_project_id"`: Retrieve tasks associated with a project.
    - `"create_task"`: Create tasks under a project.
    - `"update_task_status"`: Update the status of a task.
    - `"create_task_content_link"`: Link a task to a notebook page or board.

* **`settings` (Optional):** An array of user-configurable settings that Dhwani will render in the plugin's settings card. Each setting object supports the following fields:
  - `id`: Unique identifier for the setting (e.g., `"api_key"`).
  - `type`: Input control type. One of: `"string"`, `"number"`, `"boolean"`, `"select"`, or `"multiselect"`.
  - `display_name`: User-facing label.
  - `details` (Optional): Explanatory sub-text shown below the setting.
  - `default_value` (Optional): The default value (string, number, boolean, or array).
  - `options` (Optional, for `"select"` and `"multiselect"`): An array of `{ "value": string, "label": string }` objects representing the selectable choices.

* **`documentation_md_file` (Optional):** Relative path to the plugin's markdown documentation file inside the ZIP package (e.g. `"doc/readme.md"`). When specified, a **Documentation** button will be rendered next to the plugin options in the settings list. Clicking this button dynamically loads and displays the markdown file using the built-in `MarkdownViewer` dialog modal.

* **`board_nodes` (Optional):** An array of interactive custom board shape nodes injected into the React Flow canvas. Each item requires:
  - `node_html_file`: Relative path to the node's HTML view inside the ZIP (e.g. `"dist/my-node.html"`).
  - `node_svg_icon`: Relative path to the node's custom icon shown in the Board toolbar (e.g. `"assets/my-node.svg"`).
  - `node_display_name`: User-facing name shown in the toolbar selection drop-down.

---

## 4. Development & SDK Autocomplete

Install the official SDK package locally in your workspace to get complete types and intellisense during development:

```bash
npm install --save-dev @dhwaniapp/plugin-sdk
```

In your source JavaScript file, add this reference directive at the absolute top of the file:
```javascript
/// <reference types="@dhwaniapp/plugin-sdk" />
```

---

## 5. Exposing Editor SDKs at Runtime

During runtime execution inside Dhwani, the following global dependencies are exposed in the window:
* **`window.React`**: React v18 engine.
* **`window.ReactDOM`**: ReactDOM v18 engine.
* **`window.DhwaniEditorSDK`**: Core Tiptap and React Tiptap utilities. Exposes:
  - `Node`: Base Tiptap Node view class.
  - `Mark`: Base Tiptap Mark class.
  - `Extension`: Base Tiptap Extension class.
  - `mergeAttributes`: Method for combining HTML element attributes.
  - `ReactNodeViewRenderer`: Utility to map React components to Tiptap nodes.
  - `NodeViewWrapper`: Structural React element wrapper.
  - `NodeViewContent`: Editable placeholder for child text in custom views.
* **`window.Dhwani`**: Registry provider. Exposes:
  - `registerPageExtension(config)`: Registers the extension inside the editor workspace. **Returns a scoped `DhwaniSDKInstance`** pre-bound to the calling plugin's ID.

> [!WARNING]
> The host application automatically deletes the slash (`/`) and query text in the editor before executing your slash command. Therefore, your plugin's `command` callback should **NOT** call `editor.chain().deleteRange(range)`. Doing so will result in a double-deletion and trigger a `RangeError`.
* **`window.DhwaniSDK`**: Global namespace for permission-guarded APIs (prefer using the scoped instance returned by `registerPageExtension`).

### Scoped `DhwaniSDKInstance` Interface
When a plugin registers itself, `window.Dhwani.registerPageExtension` returns a scoped SDK object pre-bound with the plugin's ID:
```typescript
interface DhwaniHttpResponse {
    status: number;
    statusText: string;
    headers: Record<string, string>;
    data: any;
}

interface DhwaniSDKInstance {
    api: {
        /**
         * Saves a file as a page attachment.
         * Requires the 'save_attachment' permission.
         */
        saveAttachment(
            contentId: string,
            buffer: ArrayBuffer,
            fileName: string
        ): Promise<{
            id: string;
            file_path: string;
            title: string;
            content_id: string;
        }>;

        /**
         * Deletes a saved page attachment.
         * Requires the 'delete_attachment' permission.
         */
        deleteAttachment(id: string): Promise<void>;

        /**
         * Retrieves the ID of the currently active page/content.
         * Does not require any permissions.
         */
        getCurrentContentId(): string | null;

        /**
         * Retrieves the ID of the currently active notebook.
         * Does not require any permissions.
         */
        getCurrentNoteId(): string | null;

        /**
         * Generates an HTML anchor link pointing to a project timeline.
         * Does not require any permissions.
         */
        getHTMLLinkForProjectId(title: string, projectId: string): string;

        /**
         * Generates an HTML anchor link pointing to a notebook.
         * Does not require any permissions.
         */
        getHTMLLinkForNoteId(title: string, noteId: string): string;

        /**
         * Generates an HTML anchor link pointing to a specific page or content within a notebook.
         * Does not require any permissions.
         */
        getHTMLLinkForNoteIdContentId(title: string, noteId: string, contentId: string): string;

        // ---- Note & Notebook Operations ----
        getAllNotes(): Promise<Array<{ id: string; title: string; description: string; icon: string; createdAt: string; updatedAt: string }>>;
        getNoteById(noteId: string): Promise<{ id: string; title: string; description: string; icon: string; createdAt: string; updatedAt: string } | null>;
        createNote(title: string, description: string): Promise<{ id: string; title: string; description: string; icon: string; createdAt: string; updatedAt: string }>;
        updateNote(id: string, title: string, description: string): Promise<void>;
        updateNoteIcon(id: string, icon: string): Promise<void>;
        searchNoteByTitle(title: string): Promise<Array<{ id: string; title: string; description: string; icon: string; createdAt: string; updatedAt: string }>>;
        deleteNoteById(noteId: string): Promise<void>;

        // ---- Content & Page Hierarchy Operations ----
        createContent(contentType: "page" | "chat" | "board" | "interact", title: string, noteId: string, parentContentId: string | null): Promise<{ id: string; noteId: string; title: string; type: "page"|"chat"|"board"|"interact"; parentId?: string; createdAt: string; updatedAt: string }>;
        updateContent(contentId: string, title: string): Promise<void>;
        deleteContent(contentId: string, noteId: string): Promise<void>;
        getContentTree(noteId: string): Promise<Array<any>>;
        searchContentByTitle(noteId: string, title: string): Promise<Array<{ id: string; type: string; title: string }>>;
        searchContentGlobally(title: string): Promise<Array<{ id: string; type: string; title: string; noteId: string; noteTitle: string }>>;
        getContentById(contentId: string): Promise<{ id: string; noteId: string; title: string; type: "page"|"chat"|"board"|"interact"; parentId?: string; createdAt: string; updatedAt: string } | null>;
        getAllContentsForNote(noteId: string): Promise<Array<{ id: string; noteId: string; title: string; type: "page"|"chat"|"board"|"interact"; parentId?: string; createdAt: string; updatedAt: string }>>;

        // ---- Content Body & Storage Operations ----
        getContentBody(contentId: string, type: "text" | "markdown" | "json" | "html"): Promise<{ contentId: string; type: string; body: string } | null>;
        createOrUpdateContentBody(contentId: string, type: "text" | "markdown" | "json" | "html", body: string): Promise<{ contentId: string; type: string; body: string }>;
        isContentBodyExists(contentId: string, type: "text" | "markdown" | "json" | "html"): Promise<boolean>;

        // ---- Custom Interact Key-Value Data Operations ----
        getInteractData(contentId: string, key: string): Promise<{ id: string; contentId: string; key: string; value: string | null } | null>;
        setInteractData(contentId: string, key: string, value: string): Promise<{ id: string; contentId: string; key: string; value: string }>;
        deleteInteractData(contentId: string, key: string): Promise<void>;
        getAllInteractData(contentId: string): Promise<Array<{ id: string; contentId: string; key: string; value: string }>>;

        // ---- Discussion Comments Operations ----
        getCommentsByContentId(contentId: string): Promise<Array<{ id: string; contentId: string; body: string; isResolved: boolean; creatorType: "user" | "ai"; parentCommentId?: string; createdAt: string }>>;
        createComment(contentId: string, body: string, creatorType: "user" | "ai", parentCommentId?: string | null): Promise<{ id: string; contentId: string; body: string; isResolved: boolean; creatorType: "user" | "ai"; parentCommentId?: string; createdAt: string }>;

        // ---- Projects & Timelines Operations ----
        getAllProjects(): Promise<Array<{ id: string; title: string; description: string; icon: string; createdAt: string; updatedAt: string }>>;
        getProjectById(projectId: string): Promise<{ id: string; title: string; description: string; icon: string; createdAt: string; updatedAt: string } | null>;
        createProject(title: string, description: string): Promise<{ id: string; title: string; description: string; icon: string; createdAt: string; updatedAt: string }>;
        getTasksByProjectId(projectId: string): Promise<Array<{ id: string; projectId: string; title: string; description: string; status: "NOT_STARTED"|"TODO"|"IN_PROGRESS"|"BLOCKED"|"DONE"; startDatetime?: string; endDatetime?: string }>>;
        createTask(projectId: string, title: string, description: string, status: "NOT_STARTED"|"TODO"|"IN_PROGRESS"|"BLOCKED"|"DONE", startDatetime?: string, endDatetime?: string): Promise<{ id: string; projectId: string; title: string; description: string; status: "NOT_STARTED"|"TODO"|"IN_PROGRESS"|"BLOCKED"|"DONE"; startDatetime?: string; endDatetime?: string }>;
        updateTaskStatus(id: string, status: "NOT_STARTED"|"TODO"|"IN_PROGRESS"|"BLOCKED"|"DONE"): Promise<void>;
        createTaskContentLink(taskId: string, contentId: string): Promise<void>;
        
        // ---- Plugin Settings Operations ----
        getSettings(): Promise<Record<string, any>>;
        setSettings(settingsValues: Record<string, any>): Promise<{ isSuccess: boolean; message: string }>;

        makeHttpCall(
            url: string,
            method: 'GET' | 'POST' | 'PUT' | 'DELETE' | string,
            options?: {
                headers?: Record<string, string>;
                body?: string;
                query?: Record<string, string>;
            },
            config?: {
                connectionTimeout?: number;
                requestTimeout?: number;
            }
        ): Promise<DhwaniHttpResponse | null>;
    };
}
```

### Managing Plugin Settings

Plugins can define settings in their `application.json` manifest. At runtime, the plugin can read and write these settings using the following APIs:

* **`sdk.api.getSettings()`**: Returns a promise resolving to a key-value object (`Record<string, any>`) of the plugin's settings. This automatically merges any user-saved overrides from the database with the default values specified in the `application.json` manifest.
* **`sdk.api.setSettings(settingsValues)`**: Saves a key-value object of settings to the database.

No special permissions are required for `getSettings` and `setSettings`; a plugin is always permitted to manage its own settings.

For example, to retrieve a configured API key:
```javascript
const settings = await sdk.api.getSettings();
const apiKey = settings.api_key;
```

### Custom Board Node State & Data Persistence

For plugins registering canvas widgets via `"board_nodes"`, the host application automatically injects two global variables directly into the iframe's `window` scope upon boot:

* **`window.__DhwaniNodeId`**: A string containing the unique ID assigned to this node instance on the React Flow canvas (e.g., `"f37d0fda-5b99-46a5-8bfe-b2cbaf7a3faf"`).
* **`window.__DhwaniNodeData`**: A JSON object containing the current persistent state data of this specific node instance (excluding the raw HTML source code of the plugin to prevent HTML parser collisions).

#### Updating Node State
To update the node's state, post a `DHWANI_UPDATE_NODE` message to the parent window. The parent application verifies the source origin and merges the changes into the canvas nodes array, triggering a debounced SQLite auto-save:

```javascript
// Within your board node's HTML page:
function saveNodeState(key, value) {
    if (!window.__DhwaniNodeData) window.__DhwaniNodeData = {};
    window.__DhwaniNodeData[key] = value;

    if (window.parent) {
        window.parent.postMessage({
            type: 'DHWANI_UPDATE_NODE',
            nodeId: window.__DhwaniNodeId || 'NODE_ID_PLACEHOLDER',
            data: {
                ...window.__DhwaniNodeData,
                [key]: value
            }
        }, '*');
    }
}
```

#### Security Validation
For security, the parent application validates that `event.source` strictly matches the iframe element of the corresponding node. Spoofing or manually passing incorrect `nodeId` values in messages targeting other nodes is blocked and ignored.

### Making External HTTP Requests

If your plugin needs to communicate with external APIs or services, you must request the `"make_http_calls"` permission in your `application.json`.

* **`sdk.api.makeHttpCall(url, method, options, config)`**:
  * `url`: The target endpoint URL.
  * `method`: The HTTP verb (e.g., `'GET'`, `'POST'`, `'PUT'`, `'DELETE'`).
  * `options` (Optional): An object containing:
    * `headers`: Custom headers (`Record<string, string>`).
    * `body`: Stringified request body (e.g. JSON).
    * `query`: Key-value query parameters (`Record<string, string>`).
  * `config` (Optional): Timeout configurations:
    * `connectionTimeout`: Time to establish connection in milliseconds. (Capped at 30,000ms max).
    * `requestTimeout`: Time to complete the request in milliseconds. (Capped at 60,000ms max).
  * **Returns**: A promise resolving to `{ status: number, statusText: string, headers: Record<string, string>, data: any }`. The `data` field contains parsed JSON if the response content-type is JSON, otherwise plain text.
```

---

## 6. Conforming to Themes, Fonts, and Typography

Your custom blocks **MUST** follow the user-selected styling options dynamically. Avoid static utility overrides (like `prose-sm` or hardcoded pixels/fonts) and instead read global CSS variables.

### Theme & Colors (DaisyUI)
Dhwani uses DaisyUI themes. **Whenever possible, you must use pre-existing DaisyUI components** (e.g., `alert`, `card`, `btn`, `badge`, `toggle`, etc.) to build your plugin interface rather than coding custom designs from scratch. This guarantees that your plugin matches the host application's style and automatically responds to theme changes.

Use native component classes (like `.alert`) and map backgrounds and text colors to semantic Tailwind custom variables:
* **Success block:** `backgroundColor: "var(--color-success)"`, `color: "var(--color-success-content)"`
* **Warning block:** `backgroundColor: "var(--color-warning)"`, `color: "var(--color-warning-content)"`
* **Info block:** `backgroundColor: "var(--color-info)"`, `color: "var(--color-info-content)"`
* **Error block:** `backgroundColor: "var(--color-error)"`, `color: "var(--color-error-content)"`

### Typography (Fonts and Sizes)
Inherit the user-selected editor typography by applying these styles to the `NodeViewContent` component:
* `fontFamily: "var(--editor-font-family, inherit)"`
* `fontSize: "var(--editor-font-size, inherit)"`
* `fontWeight: "var(--editor-font-weight, inherit)"`

### In-Depth Example: Designing an Adaptive Themed Block

To make your plugin look premium and feel native, it must handle dark/light mode switches and editor font resizing smoothly.

#### ❌ The Bad Way (Hardcoded Styles)
Avoid hardcoding specific pixel sizes or static Tailwind colors. This will break in dark mode or when the user changes font sizes:
```javascript
// Hardcoded blue background and static font size
React.createElement("div", {
    className: "bg-blue-100 text-blue-800 p-4 rounded-md font-sans text-[14px]"
});
```

####   The Good Way (Dynamic & Adaptive)
Use DaisyUI base classes combined with CSS custom variables:
```javascript
// Automatically matches theme colors and editor font settings
React.createElement("div", {
    className: "alert shadow-sm rounded-xl transition-colors duration-200",
    style: {
        backgroundColor: "var(--color-info)",
        color: "var(--color-info-content)",
        fontFamily: "var(--editor-font-family, inherit)",
        fontSize: "var(--editor-font-size, inherit)",
        fontWeight: "var(--editor-font-weight, inherit)",
    }
});
```

#### Complete Code Implementation: Adaptive Callout Block
Below is a complete implementation of a Tiptap Node View that dynamically adapts to both the active theme and the user's typography settings, allowing the user to cycle through different alert statuses (`info`, `success`, `warning`, `error`):

```javascript
/// <reference types="@dhwaniapp/plugin-sdk" />

(function() {
    const React = window.React;
    const { Node, ReactNodeViewRenderer, NodeViewWrapper, NodeViewContent } = window.DhwaniEditorSDK;

    function AdaptiveCalloutComponent(props) {
        const { node, updateAttributes } = props;
        const { type = "info" } = node.attrs;

        const themeStyles = {
            info: {
                bg: "var(--color-info)",
                text: "var(--color-info-content)",
                icon: "m11.25 11.25.041-.02a.75.75 0 1 1 1.084 1.085l-.042.022m0 0a.75.75 0 1 1-1.084-1.085l.042-.022ZM12 21.75a9.75 9.75 0 1 0 0-19.5 9.75 9.75 0 0 0 0 19.5Z"
            },
            success: {
                bg: "var(--color-success)",
                text: "var(--color-success-content)",
                icon: "M9 12.75 11.25 15 15 9.75M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"
            },
            warning: {
                bg: "var(--color-warning)",
                text: "var(--color-warning-content)",
                icon: "M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126ZM12 15.75h.007v.008H12v-.008Z"
            },
            error: {
                bg: "var(--color-error)",
                text: "var(--color-error-content)",
                icon: "m9.75 9.75 4.5 4.5m0-4.5-4.5 4.5M21 12a9 9 0 1 1-18 0 9 9 0 0 1 18 0Z"
            }
        };

        const currentStyle = themeStyles[type] || themeStyles.info;

        return React.createElement(
            NodeViewWrapper,
            { 
                className: "alert shadow-sm my-4 flex items-start gap-3 border border-base-content/10 transition-colors duration-200",
                style: {
                    backgroundColor: currentStyle.bg,
                    color: currentStyle.text,
                    borderRadius: "0.75rem",
                    padding: "1rem"
                }
            },
            React.createElement(
                "div",
                { className: "shrink-0 select-none pt-0.5" },
                React.createElement(
                    "svg",
                    {
                        xmlns: "http://www.w3.org/2000/svg",
                        fill: "none",
                        viewBox: "0 0 24 24",
                        strokeWidth: "2",
                        stroke: "currentColor",
                        className: "w-5 h-5"
                    },
                    React.createElement("path", {
                        strokeLinecap: "round",
                        strokeLinejoin: "round",
                        d: currentStyle.icon
                    })
                )
            ),
            React.createElement(
                NodeViewContent,
                { 
                    className: "outline-none flex-1 leading-relaxed",
                    style: {
                        fontFamily: "var(--editor-font-family, inherit)",
                        fontSize: "var(--editor-font-size, inherit)",
                        fontWeight: "var(--editor-font-weight, inherit)"
                    }
                }
            ),
            React.createElement(
                "button",
                {
                    className: "btn btn-xs btn-ghost opacity-40 hover:opacity-100 self-center font-sans",
                    onClick: () => {
                        const types = ["info", "success", "warning", "error"];
                        const nextType = types[(types.indexOf(type) + 1) % types.length];
                        updateAttributes({ type: nextType });
                    }
                },
                "Cycle Type"
            )
        );
    }

    const AdaptiveCalloutNode = Node.create({
        name: 'adaptiveCallout',
        group: 'block',
        content: 'inline*',
        defining: true,

        addAttributes() {
            return {
                type: { default: 'info' }
            };
        },

        parseHTML() {
            return [{ 
                tag: 'div[data-type="adaptive-callout"]',
                getAttrs: element => ({
                    type: element.getAttribute('data-callout-type') || 'info'
                })
            }];
        },

        renderHTML({ HTMLAttributes }) {
            return [
                'div', 
                { 
                    'data-type': 'adaptive-callout', 
                    'data-callout-type': HTMLAttributes.type || 'info',
                    ...HTMLAttributes 
                }, 
                0
            ];
        },

        addNodeView() {
            return ReactNodeViewRenderer(AdaptiveCalloutComponent);
        }
    });

    window.Dhwani.registerPageExtension({
        name: "adaptiveCallout",
        tiptapExtension: AdaptiveCalloutNode,
        slashCommand: {
            title: "Adaptive Callout",
            description: "Insert a themed, resizable alert box",
            icon: `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="m11.25 11.25.041-.02a.75.75 0 1 1 1.084 1.085l-.042.022m0 0a.75.75 0 1 1-1.084-1.085l.042-.022ZM12 21.75a9.75 9.75 0 1 0 0-19.5 9.75 9.75 0 0 0 0 19.5Z" /></svg>`,
            command: (editor) => {
                editor.chain().focus().insertContent({ type: "adaptiveCallout" }).run();
            }
        }
    });
})();
```

---

## 7. Rich Media & Attachment APIs

If your plugin needs to save local files (like images, PDFs, or audio), you must request the `"save_attachment"` and/or `"delete_attachment"` permissions in your `application.json`.

### Saving Attachments
Call the scoped `sdk.api.saveAttachment(contentId, buffer, fileName)` API. It will write the file to the local user directory (`userData/attachments/`) and return an attachment object. 
* **Protocol prefix:** The returned `file_path` is automatically prefixed with `dhwani-attachment://` (e.g. `dhwani-attachment://[uuid].png`).
* **Rendering:** Simply render `<img>` or other media elements using this URL. Electron automatically resolves it at the protocol level.

### Deleting Attachments
To clean up files when they are removed by the user, extract the attachment ID from the URL and call `sdk.api.deleteAttachment(id)`.

### Attribute JSON Serialization (ProseMirror / Tiptap)
If you store complex attributes (like arrays of objects) on a leaf node, you must define `parseHTML` and `renderHTML` for those attributes so they serialize correctly as JSON strings in the Markdown body:
```javascript
addAttributes() {
    return {
        images: {
            default: [],
            parseHTML: element => {
                const val = element.getAttribute('images');
                if (!val) return [];
                try {
                    return JSON.parse(val);
                } catch (e) {
                    return val.split(',').filter(Boolean).map(url => ({ url }));
                }
            },
            renderHTML: attributes => {
                return {
                    images: JSON.stringify(attributes.images)
                };
            }
        }
    };
}
```

---

## 8. Step-by-Step Code Examples

### Example 1: Basic Info Callout (Editable Text Block)
Below is the code template for an info callout plugin (`dist/custom-node.js`).

```javascript
/// <reference types="@dhwaniapp/plugin-sdk" />

(function() {
    const React = window.React;
    const { Node, ReactNodeViewRenderer, NodeViewWrapper, NodeViewContent } = window.DhwaniEditorSDK;

    function InfoCalloutComponent() {
        return React.createElement(
            NodeViewWrapper,
            { 
                className: "alert shadow-sm my-4 text-left flex items-center gap-3",
                style: {
                    backgroundColor: "var(--color-info)",
                    color: "var(--color-info-content)",
                    borderRadius: "0.75rem"
                }
            },
            React.createElement(
                "div",
                { className: "shrink-0 select-none flex items-center justify-center" },
                React.createElement(
                    "svg",
                    {
                        xmlns: "http://www.w3.org/2000/svg",
                        fill: "none",
                        viewBox: "0 0 24 24",
                        strokeWidth: "2",
                        stroke: "currentColor",
                        className: "w-5 h-5"
                    },
                    React.createElement("path", {
                        strokeLinecap: "round",
                        strokeLinejoin: "round",
                        d: "m11.25 11.25.041-.02a.75.75 0 1 1 1.084 1.085l-.042.022m0 0a.75.75 0 1 1-1.084-1.085l.042-.022ZM12 21.75a9.75 9.75 0 1 0 0-19.5 9.75 9.75 0 0 0 0 19.5Z"
                    })
                )
            ),
            React.createElement(
                NodeViewContent,
                { 
                    className: "outline-none flex-1 font-medium",
                    style: {
                        fontFamily: "var(--editor-font-family, inherit)",
                        fontSize: "var(--editor-font-size, inherit)",
                        fontWeight: "var(--editor-font-weight, inherit)"
                    }
                }
            )
        );
    }

    const InfoCalloutNode = Node.create({
        name: 'infoCallout',
        group: 'block',
        content: 'inline*',
        defining: true,

        parseHTML() {
            return [{ tag: 'div[data-type="info-callout"]' }];
        },

        renderHTML({ HTMLAttributes }) {
            return ['div', { 'data-type': 'info-callout', ...HTMLAttributes }, 0];
        },

        addNodeView() {
            return ReactNodeViewRenderer(InfoCalloutComponent);
        }
    });

    window.Dhwani.registerPageExtension({
        name: "infoCallout",
        tiptapExtension: InfoCalloutNode,
        slashCommand: {
            title: "Info Callout",
            description: "Insert a blue info card",
            icon: `<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor"><path stroke-linecap="round" stroke-linejoin="round" d="m11.25 11.25.041-.02a.75.75 0 1 1 1.084 1.085l-.042.022m0 0a.75.75 0 1 1-1.084-1.085l.042-.022ZM12 21.75a9.75 9.75 0 1 0 0-19.5 9.75 9.75 0 0 0 0 19.5Z" /></svg>`,
            command: (editor) => {
                editor
                    .chain()
                    .focus()
                    .insertContent({ type: "infoCallout" })
                    .run();
            }
        }
    });
})();
```

### Example 2: Interactive Media Block (Image Carousel with Resizing)
This shows how to write a leaf node (`atom: true`) that interacts with the scoped `sdk` to manage file attachments and drag-and-drop.

```javascript
/// <reference types="@dhwaniapp/plugin-sdk" />

(function() {
    const React = window.React;
    const { Node, ReactNodeViewRenderer, NodeViewWrapper } = window.DhwaniEditorSDK;
    const { useState } = React;
    let sdk; // closure variable populated on registration

    function CarouselComponent(props) {
        const { node, updateAttributes } = props;
        const { images = [] } = node.attrs;
        const contentId = sdk ? sdk.api.getCurrentContentId() : "default-id";

        const uploadFiles = async (files) => {
            const imageFiles = Array.from(files).filter(f => f.type.startsWith('image/'));
            const newImages = [...images];
            
            for (const file of imageFiles) {
                const buffer = await file.arrayBuffer();
                // Calls the permission-guarded attachment API
                const attachment = await sdk.api.saveAttachment(contentId, buffer, file.name);
                newImages.push({
                    url: attachment.file_path, // automatically contains "dhwani-attachment://" prefix
                    width: 560
                });
            }
            updateAttributes({ images: newImages });
        };

        // Render Carousel list with drag-and-drop listeners
        return React.createElement(
            NodeViewWrapper,
            { className: "carousel flex flex-row w-full overflow-x-auto rounded-xl gap-4 py-3 bg-base-200" },
            images.map((img, index) => {
                return React.createElement(
                    "div",
                    { 
                        key: index,
                        className: "carousel-item relative shrink-0",
                        style: { width: (img.width || 560) + "px", height: "360px" }
                    },
                    React.createElement("img", { src: img.url, className: "w-full h-full object-cover" })
                );
            })
        );
    }

    const CarouselNode = Node.create({
        name: "carousel",
        group: "block",
        atom: true, // leaf node, must NOT have a content hole (0) in renderHTML

        addAttributes() {
            return {
                images: { default: [] }
            };
        },

        renderHTML({ HTMLAttributes }) {
            // No content hole (0) since it is an atom node
            return ["div", window.DhwaniEditorSDK.mergeAttributes(HTMLAttributes, { "data-type": "carousel" })];
        },

        addNodeView() {
            return ReactNodeViewRenderer(CarouselComponent);
        }
    });

    // Capture the returned pre-bound SDK instance
    sdk = window.Dhwani.registerPageExtension({
        name: "carousel",
        tiptapExtension: CarouselNode,
        slashCommand: {
            title: "Carousel",
            description: "Insert an image carousel",
            icon: "<svg>...</svg>",
            command: (editor) => {
                editor.chain().focus().insertContent({ type: "carousel" }).run();
            }
        }
    });
})();
```

---

## 9. Zipping the Plugin for Distribution

When distributing your plugin package, **DO NOT** package the heavy `node_modules` folder or developer lockfiles. 

Create a shell script named `zip-plugin.sh` in your plugin's root:

```bash
#!/bin/bash
OUTPUT_ZIP="../../../my-plugin.zip"

echo "📦 Bundling plugin..."
zip -r "$OUTPUT_ZIP" . -x "node_modules/*" "package-lock.json" "zip-plugin.sh" "*.log" ".DS_Store"

if [ $? -eq 0 ]; then
  echo "✅ Successfully packaged plugin to $OUTPUT_ZIP"
else
  echo "❌ Failed to package plugin"
  exit 1
fi
```
*(Make the script executable: `chmod +x zip-plugin.sh`, then run it `./zip-plugin.sh` to package).*

---

## 10. Building Interact Canvas Plugins

Interact Canvas Plugins allow you to package HTML/CSS/JS applications (like custom dashboards, quizzes, or visual simulations) that render inside a sandboxed iframe when the user selects them from the **Interact** page dropdown.

### Directory Layout

```
my-interact-plugin/
├── application.json             ← Manifest requesting "use_interact"
├── assets/
│   ├── icon.svg                 ← Main plugin icon
│   └── custom-canvas.svg        ← Icon shown in the Interact dropdown
└── dist/
    └── custom-canvas.html       ← The HTML page loaded in the iframe
```

### Manifest Setup

Request the `"use_interact"` permission and register the HTML file in the `interacts` array:

```json
{
  "$schema": "https://unpkg.com/@dhwaniapp/plugin-sdk@latest/plugin-manifest.schema.json",
  "display_name": "Interact Demo",
  "system_name": "interact.demo.plugin",
  "version": "1.0.0",
  "description": "An interactive quiz playground.",
  "author": "Developer Name",
  "license": "MIT",
  "permissions_required": [
    "use_interact",
    "get_all_notes"
  ],
  "icon_in_svg": "assets/icon.svg",
  "interacts": [
    {
      "interact_html_file": "dist/custom-canvas.html",
      "interact_svg_icon": "assets/custom-canvas.svg",
      "interact_display_name": "Interactive Quiz"
    }
  ]
}
```

### Accessing the Scoped API

Dhwani automatically injects a scoped, permission-checked `window.api` instance directly into the iframe's global context. It matches the same signature as the main window's `window.api`, but it is restricted to the permissions declared in `permissions_required`.

In your `custom-canvas.html` (or `.htm`), you can call the API directly:

```html
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Interactive Quiz</title>
</head>
<body>
    <div id="quiz-container">
        <h2>My Interactive Quiz</h2>
        <button onclick="loadNotebooks()">Load My Notebooks</button>
        <ul id="notes-list"></ul>
    </div>

    <script>
        async function loadNotebooks() {
            try {
                // Calls the permission-guarded API securely
                const notes = await window.api.getAllNotes();
                const list = document.getElementById("notes-list");
                list.innerHTML = notes.map(n => `<li>${n.title}</li>`).join("");
            } catch (err) {
                console.error("Failed to load notes:", err);
                alert("Permission Denied: " + err.message);
            }
        }
    </script>
</body>
</html>
```

### Complete Example Reference
For a complete, fully featured task board implementation that handles SQLite projects, tasks, and page-linking, refer to the [task_manager_plugin](examples/task_manager_plugin) directory.
