API: Menubar - Drowbe/coffee-pub-blacksmith GitHub Wiki

Blacksmith Menubar API Documentation

Audience: Developers integrating with Blacksmith and leveraging the exposed API.

Overview

The Blacksmith Menubar API allows external modules to register custom tools with the Blacksmith menubar system. This provides a unified interface for adding functionality to the global menubar that appears above the FoundryVTT interface.

Getting Started

1. Access the API

// Get the Blacksmith module API
const blacksmith = game.modules.get('coffee-pub-blacksmith')?.api;

// Check if API is available
if (blacksmith?.registerMenubarTool) {
    // API is ready to use
} else {
    // Wait for API to load
    Hooks.once('ready', () => {
        // API should be available now
    });
}

2. Register a Tool

Best Practice: Always explicitly set all optional parameters for clarity and consistency. This makes your code more maintainable and follows the recommended approach.

// Register a custom tool with all parameters explicitly set
const success = blacksmith.registerMenubarTool('my-custom-tool', {
    // Required parameters
    icon: "fa-solid fa-dice-d20",      // FontAwesome icon class
    name: "my-custom-tool",             // Tool name (used for data-tool attribute)
    onClick: () => {                    // Click handler function
        // Your tool logic here
        console.log("My custom tool clicked!");
    },
    
    // Optional parameters - recommended to set explicitly
    title: "My Custom Tool",            // Optional: Tooltip text and label (can be a function for dynamic content, defaults to name if omitted)
    tooltip: "My Custom Tool",          // Optional: explicit tooltip (defaults to title if omitted)
    zone: "left",                       // Optional: "left", "middle", "right" (default: "left")
    group: "general",                   // Optional: group name (default: "general")
    groupOrder: 999,                    // Optional: group order (default: 999, appears last)
    order: 5,                           // Optional: order within group (default: 999)
    moduleId: "my-module",              // Optional: module identifier (default: "unknown")
    gmOnly: false,                      // Optional: GM-only visibility (default: false)
    leaderOnly: false,                  // Optional: leader-only visibility (default: false)
    visible: true,                      // Optional: visibility (can be function) (default: true)
    toggleable: false,                  // Optional: toggleable behavior (default: false)
    active: false,                      // Optional: initial active state for toggleable tools (default: false)
    iconColor: null,                    // Optional: icon color - any valid CSS color (default: null)
    buttonNormalTint: null,             // Optional: normal button background color - any valid CSS color (default: null)
    buttonSelectedTint: null            // Optional: selected button background color - any valid CSS color (default: null)
});

if (success) {
    console.log("Tool registered successfully!");
} else {
    console.log("Failed to register tool");
}

Note: While many parameters are optional, explicitly setting them ensures your code is clear, maintainable, and follows best practices. The system will work with just the required parameters, but for production code, we recommend setting all optional parameters explicitly.

API Reference

Notification System

Notifications appear in a dedicated notification area within the middle zone of the menubar. They are separate from the zone-based tool system and do not require zone specification.

Visual Layout:

[LEFT ZONE TOOLS] [MIDDLE ZONE TOOLS] [NOTIFICATION AREA] [RIGHT ZONE TOOLS]

addNotification(text, icon, duration, moduleId)

Add a notification to the menubar.

Parameters:

  • text (string): The notification text to display
  • icon (string, optional): FontAwesome icon class (default: "fas fa-info-circle")
  • duration (number, optional): Duration in seconds, 0 = until manually removed (default: 5)
  • moduleId (string, optional): The module ID adding the notification (default: "blacksmith-core")

Returns: string - The notification ID for later removal

Note: Notifications do not use zones. They appear in a dedicated notification area within the middle zone of the menubar, separate from the zone-based tool system.

Example:

// Add a temporary notification
const notificationId = game.modules.get('coffee-pub-blacksmith').api.addNotification(
    "New message received",
    "fas fa-envelope",
    5,
    "my-module"
);

// Add a persistent notification
const persistentId = game.modules.get('coffee-pub-blacksmith').api.addNotification(
    "Important system update available",
    "fas fa-exclamation-triangle",
    0, // 0 = until manually removed
    "my-module"
);

updateNotification(notificationId, updates)

Update an existing notification.

Parameters:

  • notificationId (string): The notification ID to update
  • updates (Object): Object containing fields to update
    • text (string, optional): New notification text
    • icon (string, optional): New FontAwesome icon class
    • duration (number, optional): New duration in seconds (0 = persistent)

Returns: boolean - True if notification was updated, false if not found

Example:

// Update notification text and icon
blacksmith.updateNotification(notificationId, {
    text: "Processing complete!",
    icon: "fas fa-check-circle"
});

// Change notification to auto-remove after 3 seconds
blacksmith.updateNotification(notificationId, {
    duration: 3
});

// Make notification persistent again
blacksmith.updateNotification(notificationId, {
    duration: 0
});

removeNotification(notificationId)

Remove a specific notification from the menubar.

Parameters:

  • notificationId (string): The notification ID to remove

Returns: boolean - True if notification was removed, false if not found

Example:

game.modules.get('coffee-pub-blacksmith').api.removeNotification(notificationId);

clearNotificationsByModule(moduleId)

Remove all notifications from a specific module.

Parameters:

  • moduleId (string): The module ID to clear notifications for

Returns: number - Number of notifications removed

Example:

const removedCount = game.modules.get('coffee-pub-blacksmith').api.clearNotificationsByModule('my-module');
console.log(`Removed ${removedCount} notifications`);

getActiveNotifications()

Get all currently active notifications.

Returns: Array - Array of notification objects

Example:

const notifications = game.modules.get('coffee-pub-blacksmith').api.getActiveNotifications();
console.log(`Currently ${notifications.length} active notifications`);

clearAllNotifications()

Clear all notifications from the menubar.

Returns: number - Number of notifications removed

Example:

const removedCount = game.modules.get('coffee-pub-blacksmith').api.clearAllNotifications();
console.log(`Cleared ${removedCount} notifications`);

getNotificationIdsByModule(moduleId)

Get all notification IDs for a specific module.

Parameters:

  • moduleId (string): The module ID to get notification IDs for

Returns: Array - Array of notification IDs

Example:

const myNotificationIds = game.modules.get('coffee-pub-blacksmith').api.getNotificationIdsByModule('my-module');
console.log(`My module has ${myNotificationIds.length} active notifications`);

Tool Registration

registerMenubarTool(toolId, toolData)

Registers a new tool with the Blacksmith menubar system.

Parameters:

  • toolId (string): Unique identifier for the tool
  • toolData (Object): Tool configuration object

Returns: boolean - Success status

Tool Data Properties:

  • icon (string, required): FontAwesome icon class (e.g., "fa-solid fa-dice-d20")
  • name (string, required): Tool name (used for data-tool attribute)
  • title (string|Function, optional): Tooltip text and label displayed on hover. Can be a function that returns a string for dynamic tooltips. Defaults to name if omitted. Can be an empty string or null for icon-only buttons.
  • tooltip (string|Function, optional): Alternative tooltip text. If provided, overrides title for tooltip display. Can be a function that returns a string for dynamic tooltips.
  • onClick (Function, required): Function to execute when tool is clicked
  • zone (string, optional): Zone placement - "left", "middle", "right" (default: "left")
  • group (string, optional): Group name for organizing tools within a zone. Tools in the same group appear together. If not specified, defaults to "general". Groups are separated by visual dividers.
  • groupOrder (number, optional): Order for the group within the zone. Lower numbers appear first. Blacksmith-defined groups take precedence over other modules. If not specified, defaults to 999 (appears last). Minimum value is 1.
  • order (number, optional): Order within the group and module. Lower numbers appear first. Blacksmith tools take precedence over other modules within the same group. Default: 999.
  • moduleId (string, optional): Module identifier (default: "unknown")
  • gmOnly (boolean, optional): Whether tool is GM-only. Affects visibility only; tool is still added to the specified group. Default: false.
  • leaderOnly (boolean, optional): Whether tool is leader-only. Affects visibility only; tool is still added to the specified group. Default: false.
  • visible (boolean|Function, optional): Whether tool is visible. Can be a function that returns a boolean for dynamic visibility. Default: true.
  • toggleable (boolean, optional): Whether tool can be toggled on/off (default: false)
  • active (boolean, optional): Initial active state for toggleable tools (default: false)
  • iconColor (string, optional): Icon color. Can be any valid CSS color (e.g., '#ff0000', 'rgba(255, 0, 0, 0.8)', 'red'). If omitted, uses default icon color.
  • buttonNormalTint (string, optional): Background color for the button in normal state. Can be any valid CSS color (e.g., 'rgba(255, 107, 53, 0.2)', '#ff6b35', 'red'). If omitted, uses default button background color.
  • buttonSelectedTint (string, optional): Background color for the button when active/selected (for toggleable tools). Can be any valid CSS color (e.g., 'rgba(255, 107, 53, 0.4)', '#ff6b35', 'red'). If omitted, uses default active button background color.
  • contextMenuItems (Array | Function, optional): Right-click context menu. If provided, right-clicking the tool shows a menu instead of the browser default. Can be an array of { name, icon, description?, onClick, submenu? }, or a function (toolId, tool) => array for dynamic items (e.g. list that depends on current state). Icon can be a Font Awesome class string (e.g. 'fa-solid fa-hand') or HTML (e.g. '<i class="fa-solid fa-hand"></i>'). submenu is an array of { name, icon, description?, onClick } to render a flyout. Example: Broadcast View Mode tool uses this for mode selection (Manual, GM View, Combat, Spectator, Map View, Mirror/Follow).

updateMenubarToolActive(toolId, active)

Updates the active state of a toggleable tool.

Parameters:

  • toolId (string): Unique identifier for the tool
  • active (boolean): The active state to set

Returns: boolean - Success status

Note: Only works for tools registered with toggleable: true.

Example:

// Update a toggleable tool's active state programmatically
blacksmith.updateMenubarToolActive('my-toggle-tool', true);

unregisterMenubarTool(toolId)

Removes a tool from the Blacksmith menubar system.

Parameters:

  • toolId (string): Unique identifier for the tool

Returns: boolean - Success status

Tool Querying

getRegisteredMenubarTools()

Gets all registered tools.

Returns: Map - Map of all registered tools (toolId -> toolData)

getMenubarToolsByModule(moduleId)

Gets all tools registered by a specific module.

Parameters:

  • moduleId (string): Module identifier

Returns: Array - Array of tools registered by the module

isMenubarToolRegistered(toolId)

Checks if a tool is registered.

Parameters:

  • toolId (string): Unique identifier for the tool

Returns: boolean - Whether the tool is registered

getMenubarToolsByZone()

Gets all tools organized by their zones, groups, and modules.

Returns: Object - Object with zones containing groups, which contain module arrays with tools

{
    left: {
        "combat": [
            { moduleId: "blacksmith-core", tools: [/* tool objects */], isBlacksmith: true },
            { moduleId: "other-module", tools: [/* tool objects */], isBlacksmith: false }
        ],
        "general": [
            { moduleId: "blacksmith-core", tools: [/* tool objects */], isBlacksmith: true }
        ]
    },
    middle: {
        "utility": [
            { moduleId: "blacksmith-core", tools: [/* tool objects */], isBlacksmith: true }
        ],
        "general": [...]
    },
    right: {
        "general": [...]
    }
}

Structure:

  • Zone (left, middle, right): Top-level organization
  • Group (e.g., "combat", "utility", "general"): Groups within each zone, sorted by groupOrder
  • Module Array: Array of objects, each containing:
    • moduleId (string): The module identifier
    • tools (Array): Array of tool objects for this module
    • isBlacksmith (boolean): Whether this module is Blacksmith
  • Tools within each module are sorted by their order value
  • Modules within a group are sorted with Blacksmith first, then by minimum tool order value

Note: Only visible tools are included in the returned structure. Tools filtered by gmOnly, leaderOnly, or visible are excluded.

Menubar Control API

renderMenubar(immediate)

Request a re-render of the menubar. Use this when your module's settings or state change and the menubar should reflect those changes (e.g. tool visibility, secondary bar state).

Parameters:

  • immediate (boolean, optional): If true, re-render immediately. If false (default), debounces the render by ~50ms to batch rapid changes.

Returns: Promise<void>

Example:

const blacksmith = game.modules.get('coffee-pub-blacksmith')?.api;
if (blacksmith?.renderMenubar) {
    blacksmith.renderMenubar(); // Debounced
    // or
    blacksmith.renderMenubar(true); // Immediate
}

registerMenubarVisibilityOverride(moduleId, callback)

Register a callback that can hide the menubar for specific users. Used by modules (e.g. Herald) that designate a "broadcast/cameraman" user who should see a clean, UI-free view.

Parameters:

  • moduleId (string): Your module identifier (e.g. 'coffee-pub-herald')
  • callback (function): Called with (user) and returns { hide: true } to hide the menubar for that user, or { hide: false } (or omit hide) to allow the menubar

Example:

const blacksmith = game.modules.get('coffee-pub-blacksmith')?.api;
blacksmith?.registerMenubarVisibilityOverride('my-module', (user) => {
    const shouldHide = /* your logic */;
    return { hide: shouldHide };
});

unregisterMenubarVisibilityOverride(moduleId)

Unregister a visibility override (e.g. on module unload).

Parameters:

  • moduleId (string): The same module ID used when registering

Example:

blacksmith?.unregisterMenubarVisibilityOverride('my-module');

Menubar Zones

The menubar system organizes tools into three predefined zones:

Zone Layout:

  • left - Action tools (movement, interface, voting, skill checks)
  • middle - General tools and utilities (supports grouping with visual dividers)
  • right - Informational tools (leader display, timer)

Zone Guidelines:

  • left: Primary action tools that users interact with frequently
  • middle: Secondary tools and utilities (supports grouping system)
  • right: Read-only information displays and status indicators

Zone Organization:

  • Tools within each zone can be organized into groups using the group parameter
  • Groups are separated by visual dividers in the menubar
  • The grouping system is most commonly used in the middle zone, but works in all zones
  • Groups are sorted by groupOrder, with lower numbers appearing first

Visibility System

The menubar uses a three-tier visibility system:

User Types:

  • GM: Sees all tools (including GM tools and leader tools)
  • LEADER: Sees all tools except GM tools, plus leader tools
  • PLAYER: Sees all tools except GM tools and leader tools

Tool Properties:

  • gmOnly: true: Only visible to Game Masters
  • leaderOnly: true: Visible to party leaders and GMs
  • Default: Visible to all users

Dynamic Visibility:

// Tool with dynamic visibility
blacksmith.registerMenubarTool('my-conditional-tool', {
    icon: "fa-solid fa-eye",
    name: "my-conditional-tool",
    title: "Conditional Tool",
    tooltip: "Conditional Tool",
    zone: "left",
    group: "general",
    groupOrder: 999,
    order: 20,
    moduleId: "my-module",
    gmOnly: false,
    leaderOnly: false,
    visible: () => {
        // Only show if certain conditions are met
        return game.user.isGM || game.settings.get('my-module', 'enableFeature');
    },
    toggleable: false,
    active: false,
    iconColor: null,
    buttonNormalTint: null,
    buttonSelectedTint: null,
    onClick: () => {
        // Tool logic
    }
});

Grouping System

The menubar uses a tiered grouping system to organize tools logically:

Organization Hierarchy

Tools are organized in the following hierarchy:

  1. Zone (left, middle, right)
  2. Group (e.g., "combat", "utility", "party", "general")
  3. Module (tools from the same module are clustered together)
  4. Order (within the module, by order value)

Group Organization

  • Groups are separated by visual dividers in the menubar
  • Groups are sorted by groupOrder (lower numbers appear first)
  • Within a group, tools are organized by module:
    • Blacksmith tools appear first (if any)
    • Other modules follow, sorted by the minimum order value of their tools
  • Within a module, tools are sorted by order value

Blacksmith-Defined Groups

Blacksmith defines the following groups with their order priorities:

  • "combat" (order: 1) - Combat-related tools
  • "utility" (order: 2) - Utility tools
  • "party" (order: 3) - Party management tools
  • "general" (order: 999) - Default group, always appears last

Group Priority Rules

  1. Blacksmith groups take precedence: If Blacksmith defines a group order, it overrides any other module's definition
  2. "general" group is always last: The default group always has order 999 and appears after all other groups
  3. Dynamic groups: Modules can create new groups by specifying a group name. If the group doesn't exist, it's created automatically
  4. Auto-assignment: If a module specifies a groupOrder >= 999, it's automatically assigned to the first available slot below 999

Visibility and Groups

  • gmOnly and leaderOnly affect visibility only - tools are still added to their specified group
  • Hidden tools don't affect group ordering or module clustering

Ordering Guidelines

Group Ordering

  • Lower groupOrder values appear first within each zone
  • Recommended groupOrder ranges:
    • 1-10: Core/primary groups (combat, utility, party)
    • 11-50: Secondary groups
    • 51-100: Utility groups
    • 101-998: Custom groups
    • 999: Default "general" group (always last)

Tool Ordering Within Groups

  • Lower order values appear first within each module
  • Blacksmith tools take precedence over other modules within the same group
  • Recommended order ranges:
    • 1-10: Core/primary tools
    • 11-50: Secondary tools
    • 51-100: Utility tools
    • 101+: Optional/advanced tools

Example Usage

Notification Management

Complete Module Notification Management

class MyModule {
    constructor() {
        this.notificationIds = new Set(); // Track our notification IDs
        this.moduleId = 'my-module';
        this.blacksmith = null;
    }

    async initialize() {
        // Wait for Blacksmith API to be ready
        this.blacksmith = game.modules.get('coffee-pub-blacksmith')?.api;
        if (!this.blacksmith) {
            console.error('Blacksmith API not available');
            return;
        }
    }

    // Add a notification and track the ID
    addNotification(text, icon = "fas fa-info", duration = 5) {
        if (!this.blacksmith) return null;

        const notificationId = this.blacksmith.addNotification(text, icon, duration, this.moduleId);
        if (notificationId) {
            this.notificationIds.add(notificationId);
        }
        return notificationId;
    }

    // Update a specific notification
    updateNotification(notificationId, newText, newIcon) {
        if (!this.blacksmith) return false;

        return this.blacksmith.updateNotification(notificationId, {
            text: newText,
            icon: newIcon
        });
    }

    // Remove a specific notification
    removeNotification(notificationId) {
        if (!this.blacksmith) return false;

        const success = this.blacksmith.removeNotification(notificationId);
        if (success) {
            this.notificationIds.delete(notificationId);
        }
        return success;
    }

    // Clean up all our notifications when module is disabled
    cleanup() {
        if (this.blacksmith) {
            this.blacksmith.clearNotificationsByModule(this.moduleId);
            this.notificationIds.clear();
        }
    }

    // Get all our current notification IDs
    getMyNotificationIds() {
        if (!this.blacksmith) return [];
        return this.blacksmith.getNotificationIdsByModule(this.moduleId);
    }

    // Example: Show a progress notification that updates
    async showProgressNotification() {
        const notificationId = this.addNotification(
            "Starting process...", 
            "fas fa-spinner fa-spin", 
            0 // Persistent
        );

        // Simulate progress updates
        setTimeout(() => {
            this.updateNotification(notificationId, {
                text: "Processing... 50%",
                icon: "fas fa-spinner fa-spin"
            });
        }, 2000);

        setTimeout(() => {
            this.updateNotification(notificationId, {
                text: "Processing complete!",
                icon: "fas fa-check-circle",
                duration: 3 // Auto-remove after 3 seconds
            });
        }, 4000);
    }
}

// Register cleanup when module is disabled
Hooks.once('disableModule', (moduleId) => {
    if (moduleId === 'my-module') {
        myModuleInstance.cleanup();
    }
});

Simple Notification Examples

const blacksmith = game.modules.get('coffee-pub-blacksmith')?.api;

// Add a temporary notification
const tempId = blacksmith.addNotification(
    "Task completed!", 
    "fas fa-check", 
    5, 
    "my-module"
);

// Add a persistent notification
const persistentId = blacksmith.addNotification(
    "System is processing...", 
    "fas fa-spinner fa-spin", 
    0, // 0 = persistent
    "my-module"
);

// Update the persistent notification when done
blacksmith.updateNotification(persistentId, {
    text: "Processing complete!",
    icon: "fas fa-check-circle",
    duration: 3 // Now auto-remove after 3 seconds
});

// Or remove it manually
blacksmith.removeNotification(persistentId);

Basic Tool Registration

Best Practice: Always explicitly set all optional parameters for clarity and consistency.

// Register a simple utility tool (left zone)
blacksmith.registerMenubarTool('my-utility', {
    icon: "fa-solid fa-calculator",
    name: "my-utility",
    title: "My Utility Tool",
    tooltip: "My Utility Tool",  // Optional: explicit tooltip (defaults to title)
    zone: "left",
    group: "general",             // Optional: group name (default: "general")
    groupOrder: 999,              // Optional: group order (default: 999)
    order: 10,                     // Optional: order within group (default: 999)
    moduleId: "my-module",        // Optional: module identifier (default: "unknown")
    gmOnly: false,                // Optional: GM-only visibility (default: false)
    leaderOnly: false,            // Optional: leader-only visibility (default: false)
    visible: true,                // Optional: visibility (default: true)
    toggleable: false,            // Optional: toggleable behavior (default: false)
    active: false,                // Optional: initial active state (default: false)
    iconColor: null,              // Optional: icon color (default: null)
    buttonNormalTint: null,      // Optional: normal button background color (default: null)
    buttonSelectedTint: null,     // Optional: selected button background color (default: null)
    onClick: () => {
        // Your utility logic
        ui.notifications.info("Utility tool activated!");
    }
});

GM-Only Tool

// Register a GM-only admin tool
blacksmith.registerMenubarTool('my-admin-tool', {
    icon: "fa-solid fa-cog",
    name: "my-admin-tool",
    title: "Admin Tool",
    tooltip: "Admin Tool",
    zone: "middle",
    group: "general",
    groupOrder: 999,
    order: 5,
    moduleId: "my-module",
    gmOnly: true,                 // Only visible to GMs
    leaderOnly: false,
    visible: true,
    toggleable: false,
    active: false,
    iconColor: null,
    buttonNormalTint: null,
    buttonSelectedTint: null,
    onClick: () => {
        // Admin functionality
        console.log("Admin tool used by GM");
    }
});

Leader Tool

// Register a leader-only tool
blacksmith.registerMenubarTool('my-leader-tool', {
    icon: "fa-solid fa-crown",
    name: "my-leader-tool",
    title: "Leader Tool",
    tooltip: "Leader Tool",
    zone: "left",
    group: "general",
    groupOrder: 999,
    order: 1,
    moduleId: "my-module",
    gmOnly: false,
    leaderOnly: true,             // Visible to leaders and GMs
    visible: true,
    toggleable: false,
    active: false,
    iconColor: null,
    buttonNormalTint: null,
    buttonSelectedTint: null,
    onClick: () => {
        // Leader functionality
        ui.notifications.info("Leader tool activated!");
    }
});

Right Zone Information Tool

// Register an informational tool for the right zone
blacksmith.registerMenubarTool('my-status-tool', {
    icon: "fa-solid fa-info-circle",
    name: "my-status-tool",
    title: "Status Information",
    tooltip: "Status Information",
    zone: "right",
    group: "general",
    groupOrder: 999,
    order: 10,
    moduleId: "my-module",
    gmOnly: false,
    leaderOnly: false,
    visible: true,
    toggleable: false,
    active: false,
    iconColor: null,
    buttonNormalTint: null,
    buttonSelectedTint: null,
    onClick: () => {
        // Display status information
        ui.notifications.info("Status: All systems operational");
    }
});

Toggleable Tool

// Register a toggleable tool that shows active/inactive state
blacksmith.registerMenubarTool('my-toggle-tool', {
    icon: "fa-solid fa-toggle-on",
    name: "my-toggle-tool",
    title: "Toggle Feature",
    tooltip: "Toggle Feature",
    zone: "left",
    group: "general",
    groupOrder: 999,
    order: 15,
    moduleId: "my-module",
    gmOnly: false,
    leaderOnly: false,
    visible: true,
    toggleable: true,              // Enable toggleable behavior
    active: false,                 // Initial state
    iconColor: null,
    buttonNormalTint: null,
    buttonSelectedTint: null,
    onClick: () => {
        // The active state is automatically toggled by the system
        // Check the current state if needed
        const tool = blacksmith.getRegisteredMenubarTools().get('my-toggle-tool');
        if (tool.active) {
            console.log("Feature is now active");
        } else {
            console.log("Feature is now inactive");
        }
    }
});

// Update active state programmatically
blacksmith.updateMenubarToolActive('my-toggle-tool', true);

Tool with Custom Colors

// Register a tool with custom icon and button colors
blacksmith.registerMenubarTool('my-colored-tool', {
    icon: "fa-solid fa-palette",
    name: "my-colored-tool",
    title: "Colored Tool",
    tooltip: "Colored Tool",
    zone: "middle",
    group: "utility",
    groupOrder: 2,
    order: 10,
    moduleId: "my-module",
    gmOnly: false,
    leaderOnly: false,
    visible: true,
    toggleable: false,
    active: false,
    iconColor: "#ff6b35",                              // Orange icon
    buttonNormalTint: "rgba(255, 107, 53, 0.2)",      // Light orange background
    buttonSelectedTint: "rgba(255, 107, 53, 0.4)",    // Darker orange when active
    onClick: () => {
        ui.notifications.info("Colored tool activated!");
    }
});

Tool in Custom Group

// Register a tool in a custom group (creates the group automatically)
blacksmith.registerMenubarTool('my-custom-tool', {
    icon: "fa-solid fa-star",
    name: "my-custom-tool",
    title: "Custom Group Tool",
    tooltip: "Custom Group Tool",
    zone: "middle",
    group: "my-custom-group",      // Creates new group automatically
    groupOrder: 50,                // Appears after utility (2) but before general (999)
    order: 1,
    moduleId: "my-module",
    gmOnly: false,
    leaderOnly: false,
    visible: true,
    toggleable: false,
    active: false,
    iconColor: null,
    buttonNormalTint: null,
    buttonSelectedTint: null,
    onClick: () => {
        ui.notifications.info("Custom group tool activated!");
    }
});

Module Cleanup

// Unregister all tools when module is disabled
Hooks.once('disableModule', (moduleId) => {
    if (moduleId === 'my-module') {
        const tools = blacksmith.getMenubarToolsByModule('my-module');
        tools.forEach(tool => {
            blacksmith.unregisterMenubarTool(tool.toolId);
        });
    }
});

Error Handling

The API includes robust error handling:

  • Invalid tool data: Returns false and logs error
  • Duplicate tool IDs: Returns false (tools must be unique)
  • Missing required properties: Returns false and logs error
  • API not available: Check for API availability before use

⚠️ Critical: Function Dependencies and Scope

The Problem: Module Scope Isolation

When you register a tool with the menubar, the onClick function gets executed in the Blacksmith menubar's context, not your module's context. This means your function loses access to your module's imports and variables.

❌ Common Error Pattern:

// In your-module.js
import { MyManager } from './manager-my.js';

const myFunction = () => {
    MyManager.doSomething(); // ❌ ReferenceError: MyManager is not defined
};

// Register with menubar
blacksmith.registerMenubarTool('my-tool', {
    onClick: myFunction // ❌ Function loses access to MyManager
});

Error: ReferenceError: MyManager is not defined

✅ Solution: Self-Contained Functions

Make your onClick functions completely self-contained by importing all dependencies:

// In your-module.js
import { MyManager } from './manager-my.js';

// ✅ Self-contained function with all dependencies
const myFunction = () => {
    try {
        if (!MyManager) {
            throw new Error('MyManager not available');
        }
        MyManager.doSomething();
    } catch (error) {
        console.error('My Module | Error in tool:', error);
    }
};

// Register with menubar
blacksmith.registerMenubarTool('my-tool', {
    onClick: myFunction // ✅ Function has access to all its dependencies
});

Alternative Solutions:

1. Bound Context Functions

// Bind the function to maintain its original context
blacksmith.registerMenubarTool('my-tool', {
    onClick: myFunction.bind(this) // Maintains original context
});

2. Module API Access

// Access your module's API instead of direct imports
const myFunction = () => {
    const myAPI = game.modules.get('my-module')?.api;
    myAPI.MyManager?.doSomething();
};

3. Wrapper Function

// Create a wrapper that handles the context
const createMyHandler = () => {
    return () => {
        // This closure maintains access to your module's scope
        MyManager.doSomething();
    };
};

blacksmith.registerMenubarTool('my-tool', {
    onClick: createMyHandler()
});

🎯 Recommended Approach:

Use self-contained functions (Solution 1) because they:

  • Are explicit about dependencies
  • Work regardless of execution context
  • Are easier to debug
  • Are more reusable

📋 Checklist for onClick Functions:

  • All required imports are included in the same file
  • Function is self-contained (no external dependencies)
  • Error handling is included
  • Function works when called from any context
  • All variables and functions are properly scoped

Best Practices

  1. Explicit Parameter Setting: Always explicitly set all optional parameters when registering tools. This ensures clarity, consistency, and makes your code more maintainable:

    // ✅ Good: Explicit parameters
    blacksmith.registerMenubarTool('my-tool', {
        icon: "fa-solid fa-icon",
        name: "my-tool",
        title: "My Tool",
        tooltip: "My Tool",
        zone: "left",
        group: "general",
        groupOrder: 999,
        order: 10,
        moduleId: "my-module",
        gmOnly: false,
        leaderOnly: false,
        visible: true,
        toggleable: false,
        active: false,
        iconColor: null,
        buttonNormalTint: null,
        buttonSelectedTint: null,
        onClick: () => {}
    });
    
    // ❌ Avoid: Relying on defaults (less clear)
    blacksmith.registerMenubarTool('my-tool', {
        icon: "fa-solid fa-icon",
        name: "my-tool",
        title: "My Tool",
        onClick: () => {}
    });
  2. Self-Contained Functions: Make onClick functions completely self-contained with all dependencies imported

  3. Unique Tool IDs: Use descriptive, unique tool identifiers

  4. Proper Zone Selection: Choose the most appropriate zone for your tool:

    • left: Primary action tools
    • middle: General tools and utilities
    • right: Informational/read-only tools
  5. Group Organization: Use appropriate groups to organize related tools:

    • Use existing Blacksmith groups ("combat", "utility", "party") when appropriate
    • Create custom groups for module-specific tool collections
    • Use groupOrder to control group positioning
  6. Consistent Ordering: Use consistent order values within your module:

    • Lower numbers appear first
    • Reserve ranges for different tool types
  7. Color Format Flexibility: When using iconColor, buttonNormalTint, or buttonSelectedTint, you can use any valid CSS color format:

    • Hex: '#ff6b35'
    • RGB/RGBA: 'rgba(255, 107, 53, 0.2)'
    • Named colors: 'red'
    • HSL: 'hsl(15, 100%, 60%)'
  8. Module Cleanup: Unregister tools when your module is disabled

  9. Error Handling: Always check return values and handle errors gracefully

  10. API Availability: Check if the API is available before using it

  11. Scope Awareness: Understand that onClick functions execute in Blacksmith's context, not your module's context

Troubleshooting

Tool Not Appearing

  • Check if tool is registered: blacksmith.isMenubarToolRegistered('tool-id')
  • Verify visibility settings (gmOnly, leaderOnly, visible function)
  • Check console for error messages
  • Ensure API is loaded: blacksmith?.registerMenubarTool

API Not Available

  • Use correct API path: blacksmith.registerMenubarTool() not blacksmith.api.registerMenubarTool()
  • Wait for ready hook, not blacksmithUpdated hook
  • Check if module is active: game.modules.get('coffee-pub-blacksmith')?.active

Tool in Wrong Zone

  • Verify zone property is set correctly
  • Check zone spelling (must match "left", "middle", or "right" exactly)
  • Default zone is "left" if not specified

Tool Click Errors (ReferenceError)

  • Error: ReferenceError: SomeClass is not defined
  • Cause: onClick function loses access to module's imports when executed
  • Solution: Make onClick function self-contained with all dependencies imported
  • Check: Ensure all required classes/functions are imported in the same file as the onClick function

Tool Not Clickable

  • Verify onClick function is provided and valid
  • Check for JavaScript errors in onClick function
  • Ensure tool is not disabled by visibility logic

Support

For issues or questions about the Blacksmith Menubar API:

  1. Check this documentation first
  2. Review console logs for error messages
  3. Test with a simple tool registration
  4. Contact the Blacksmith development team

Secondary Bar API

The menubar supports secondary bars - additional toolbars that appear below the main menubar, similar to tabs. Only one secondary bar can be open at a time. When you open a new secondary bar, the existing one automatically closes.

Secondary bar methods at a glance:

Method Purpose
registerSecondaryBarType(typeId, config) Define a bar type (height, persistence, groups). Call this first.
registerSecondaryBarItem(barTypeId, itemId, itemData) Add a button or info item to a bar (zone, group, icon/onClick or label/value).
registerSecondaryBarTool(barTypeId, toolId) Optional. Link a menubar tool to this bar so the menubar syncs the tool's active state when the bar opens/closes.
unregisterSecondaryBarItem(barTypeId, itemId) Remove an item from a bar.
openSecondaryBar(typeId, options) / closeSecondaryBar() / toggleSecondaryBar(typeId, options) Show, hide, or toggle a secondary bar (e.g. from a menubar tool's onClick).
updateSecondaryBarItemActive(barTypeId, itemId, active) Set which button is active on the bar (e.g. radio-style mode buttons).
updateSecondaryBarItemInfo(barTypeId, itemId, updates) Update the value/label of an info item (for dynamic display without re-registering).
getSecondaryBarItems(barTypeId) Get the list of items for a bar.
updateSecondaryBar(data) Update data for an already-open bar (e.g. custom template content).

Important: Always use the default tool system unless your use case absolutely requires a custom template. The default tool system is simpler, faster to implement, timing-safe, and sufficient for most toolbars. Only use custom templates for complex UIs that cannot be achieved with simple button-based tools.

Common Use Cases:

  • Combat tracker (built-in, uses custom template for complex portraits/health rings)
  • Drawing tools (Cartographer, uses default tool system)
  • Specialized toolbars for specific activities

Secondary Bar Behavior

  • Tab-like behavior: Only one secondary bar can be open at a time
  • Automatic switching: Opening a new secondary bar closes the currently open one
  • Persistence modes: 'manual' (user closes) or 'auto' (auto-closes after delay)
  • Height customization: Each bar type can have its own height

Default Bar Zones and Item Kinds

The default tool system (no custom template) supports:

  • Zones: Each item can specify a zone: 'left', 'middle', or 'right'. Items without a zone default to 'middle'. The bar renders three regions: left-aligned, center, and right-aligned. This matches the main menubar’s left/middle/right layout so you can build encounter-style bars (info on the sides, actions in the center) without a custom template.
  • Item kinds:
    • Button (default): Clickable item with icon or image and onClick. Same as before.
    • Info: Display-only item with label and/or value. No onClick. Use updateSecondaryBarItemInfo(barTypeId, itemId, { value, label }) to update the displayed text so the bar can show dynamic content (e.g. Party CR, Difficulty) without re-registering.

Registering a Secondary Bar Type

Before you can open a secondary bar, you must register its type. The system supports two approaches:

  1. Default Tool System (recommended - use this unless custom template is absolutely necessary) - Register individual tools/items
  2. Custom Template (only when default system cannot meet your needs) - Provide a custom Handlebars template

Bar type modes are mutually exclusive: Each bar type is either fully default (zones + buttons/info) or fully custom (your template replaces the entire bar). You cannot use a custom template for only some zones; it is all-or-nothing per bar type.

Choose the default tool system unless you need:

  • Complex nested HTML structures
  • Custom graphics/SVG elements (like health rings)
  • Dynamic data-driven layouts that buttons can't handle
  • Highly specialized UI components

Use default tool system for:

  • Simple button-based toolbars
  • Drawing tools, filters, toggles
  • Any toolbar that can be represented as a row of buttons/controls
const blacksmith = game.modules.get('coffee-pub-blacksmith')?.api;

// Option 1: Register a bar type using the default tool system (no template needed)
const success = await blacksmith.registerSecondaryBarType('cartographer', {
    height: 60,                    // Height in pixels
    persistence: 'manual',         // 'manual' or 'auto'
    autoCloseDelay: 10000,         // Auto-close delay in ms (if persistence is 'auto')
    groups: {                      // Optional: Group configuration
        'line-size': {
            mode: 'switch',        // 'switch' or 'default'
            order: 10
        },
        'colors': {
            mode: 'switch',
            order: 20
        },
        'tools': {
            mode: 'default',       // Default mode allows mixed behaviors
            order: 0
        }
    }
    // No templatePath = uses default tool system
});

// Option 2: Register a bar type with a custom template (for complex UIs)
const success = blacksmith.registerSecondaryBarType('combat', {
    height: 60,
    persistence: 'manual',
    templatePath: 'modules/my-module/templates/menubar-combat.hbs'  // Custom template path
});

if (success) {
    console.log("Secondary bar type registered!");
}

Parameters:

  • typeId (string, required): Unique identifier for the bar type (e.g., 'cartographer', 'combat')
  • config (Object, required): Configuration object
    • height (number, optional): Height in pixels (default: 50)
    • persistence (string, optional): 'manual' or 'auto' (default: 'manual')
    • autoCloseDelay (number, optional): Auto-close delay in milliseconds (default: 10000)
    • templatePath (string, optional): Path to custom Handlebars template partial. If not provided, uses the default tool system.
    • groupBannerEnabled (boolean, optional): Enable group banners above each button group (default: false)
    • groupBannerColor (string, optional): Background color for group banners - any valid CSS color (default: 'rgba(62, 62, 163, 0.9)')
    • groups (Object, optional): Group configuration object. Maps group IDs to group configs:
      • groupId (string): Unique identifier for the group (e.g., 'line-size', 'colors')
      • mode (string, optional): Group behavior mode - 'default' or 'switch' (default: 'default')
        • 'default': Independent buttons (can have toggleable buttons)
        • 'switch': Only one button active at a time, one always active
      • order (number, optional): Display order for the group (lower numbers appear first, default: 999)

Returns: Promise<boolean> - Success status (async method)

Note: This method is async because it loads custom templates. Use await or .then() when using custom templates.

Group Configuration:

  • Groups allow you to organize buttons into logical sections
  • Multiple modules can contribute groups to the same bar type (groups are merged)
  • Groups with different IDs will have dividers between them
  • The 'default' group always exists if no group is specified for an item
  • Group modes determine how buttons within that group behave (see Secondary Bar Groups section below)

Group Banners:

  • When groupBannerEnabled is true, a banner appears above each button group
  • The banner displays the group ID as text (e.g., "modes", "tools", "colors")
  • Banner height scales proportionally with the secondary bar height (20% of bar height, clamped 10-20px)
  • Banner text is automatically scaled to match (10% of bar height, clamped 8-14px)
  • Banner text color is always rgba(255, 255, 255, 0.9) for consistency
  • Button sizes automatically adjust when banners are enabled to fit in the remaining space
  • Use group banners to provide visual organization and labeling for button groups

Opening a Secondary Bar

// Open your secondary bar
const success = blacksmith.openSecondaryBar('cartographer', {
    data: {
        // Your custom data to pass to the template
        tools: ['pencil', 'eraser', 'line'],
        activeTool: 'pencil'
    },
    height: 60,                    // Optional: override registered height
    persistence: 'manual'          // Optional: override registered persistence
});

if (success) {
    console.log("Cartographer bar opened!");
}

Parameters:

  • typeId (string, required): The registered bar type ID
  • options (Object, optional): Options for the bar
    • data (Object, optional): Data to pass to the bar template
    • height (number, optional): Override the registered height
    • persistence (string, optional): Override the registered persistence mode

Returns: boolean - Success status

Note: Opening a secondary bar automatically closes any currently open secondary bar.

Closing a Secondary Bar

// Close the currently open secondary bar
const success = blacksmith.closeSecondaryBar();

if (success) {
    console.log("Secondary bar closed!");
}

Returns: boolean - Success status

Toggling a Secondary Bar

// Toggle the cartographer bar (opens if closed, closes if open)
const success = blacksmith.toggleSecondaryBar('cartographer', {
    data: {
        tools: ['pencil', 'eraser'],
        activeTool: 'pencil'
    }
});

Parameters:

  • typeId (string, required): The bar type to toggle
  • options (Object, optional): Options for the bar (same as openSecondaryBar)

Returns: boolean - Success status

Behavior:

  • If the bar is closed, it opens
  • If the bar is open and matches the type, it closes
  • If a different bar is open, it closes that bar and opens the requested one

Updating a Secondary Bar

Update the data of an already-open secondary bar without closing/reopening:

// Update the cartographer bar data
const success = blacksmith.updateSecondaryBar({
    activeTool: 'eraser',
    color: '#ff0000'
});

Parameters:

  • data (Object, required): New data to merge with existing data

Returns: boolean - Success status

Note: This only works if a secondary bar is currently open.

Secondary Bar Groups

Secondary bars support groups - logical collections of buttons that are visually separated by dividers. Groups support two behavior modes:

Group Modes

'default' mode (default):

  • Buttons are independent
  • Each button can be toggleable (use toggleable: true in item registration)
  • Buttons can perform actions independently
  • Use case: Tool buttons, action buttons, filters

'switch' mode:

  • Radio-button behavior: only one button active at a time
  • One button must always be active (first item becomes active by default)
  • Clicking a button automatically deactivates others in the group
  • Use case: Line size selection, color pickers, mode selection

Group Configuration

Groups are configured when registering the bar type:

await blacksmith.registerSecondaryBarType('cartographer', {
    groups: {
        'tools': {
            mode: 'default',    // Independent buttons
            order: 0            // Display order (lower = first)
        },
        'line-size': {
            mode: 'switch',     // Radio-button behavior
            order: 10
        }
    },
    groupBannerEnabled: true,   // Enable group banners (optional)
    groupBannerColor: 'rgba(62, 62, 163, 0.9)'  // Banner background color (optional)
});

Assigning Items to Groups

When registering items, specify the group parameter:

// Item in 'tools' group (default mode)
blacksmith.registerSecondaryBarItem('cartographer', 'pencil', {
    group: 'tools',
    toggleable: true,    // Can toggle on/off
    // ...
});

// Item in 'line-size' group (switch mode)
blacksmith.registerSecondaryBarItem('cartographer', 'small', {
    group: 'line-size',
    // toggleable not needed - switch mode handles it automatically
    // ...
});

Visual Layout

Groups are rendered with dividers between them:

[Group 1 Items] | [Group 2 Items] | [Group 3 Items]

Items within the same group appear together without dividers.

Multiple Module Support

Multiple modules can contribute groups to the same bar type. Groups are merged when the bar type is registered:

// Module A registers the bar type with groups
await blacksmith.registerSecondaryBarType('cartographer', {
    groups: {
        'tools': { mode: 'default', order: 0 }
    }
});

// Module B can add more groups (merged, not replaced)
await blacksmith.registerSecondaryBarType('cartographer', {
    groups: {
        'colors': { mode: 'switch', order: 20 }  // Added to existing groups
    }
});

Registering Secondary Bar Items (Default Tool System)

For simple toolbars, use the default tool registration system. This avoids needing to create custom templates:

// Register items for a secondary bar (default tool system)
blacksmith.registerSecondaryBarItem('cartographer', 'pencil-tool', {
    icon: 'fa-solid fa-pencil',
    label: 'Pencil',
    tooltip: 'Draw with pencil tool',
    group: 'tools',                  // Optional: Group ID (default: 'default')
    toggleable: false,               // Optional: Whether item can be toggled (default: false)
    active: false,                   // Optional: whether item is active/selected
    order: 10,                       // Optional: sort order within group
    onClick: (event) => {
        // Handle click
        console.log('Pencil tool clicked');
    }
});

blacksmith.registerSecondaryBarItem('cartographer', 'small-line', {
    icon: 'fa-solid fa-minus',
    label: 'Small',
    group: 'line-size',              // Part of the 'line-size' switch group
    order: 10,
    onClick: (event) => {
        // In switch groups, clicking automatically activates this item and deactivates others
        console.log('Small line size selected');
    }
});

blacksmith.registerSecondaryBarItem('cartographer', 'medium-line', {
    icon: 'fa-solid fa-equals',
    label: 'Medium',
    group: 'line-size',
    order: 20,
    onClick: (event) => {
        console.log('Medium line size selected');
    }
});

Parameters:

  • barTypeId (string, required): The bar type ID to register the item to
  • itemId (string, required): Unique identifier for the item
  • itemData (Object, required): Item configuration
    • kind (string, optional): 'button' (default), 'info', 'progressbar', or 'balancebar'. Buttons are clickable; info, progressbar, and balancebar items are display-only and can be updated with updateSecondaryBarItemInfo.
    • zone (string, optional): 'left', 'middle', or 'right'. Default: 'middle'. Only applies to the default tool system.
    • icon (string, required for buttons, optional for info): FontAwesome icon class (e.g., 'fa-solid fa-pencil', 'fas fa-eraser'). Info items can use icon for consistent styling with buttons.
    • label (string, optional): Text label. For buttons, shown next to the icon. For info items, use with or without value.
    • value (string, optional): For info items only. Display value (e.g. "2", "Medium"). Can be updated later with updateSecondaryBarItemInfo.
    • tooltip (string, optional): Tooltip text on hover. If omitted, uses label as tooltip.
    • group (string, optional): Group ID to place the item in. If not specified, uses 'default' group. Groups with different IDs will have dividers between them.
    • toggleable (boolean, optional): Whether item can be toggled on/off (buttons only; only applies to 'default' mode groups). In 'switch' mode groups, items are automatically managed.
    • active (boolean, optional): Whether button is active/selected. Adds active CSS class when true. For 'switch' mode groups, the first button is automatically made active if none is active.
    • order (number, optional): Sort order for displaying items within the group (lower numbers appear first). Items without order appear after items with order, sorted alphabetically by itemId.
    • iconColor (string, optional): Icon color. Can be any valid CSS color (e.g., '#ff0000', 'rgba(255, 0, 0, 0.8)', 'red'). If omitted, uses default icon color. Applies to both buttons and info items.
    • buttonColor (string, optional): Background color for the item. Can be any valid CSS color. If omitted, uses the default from --blacksmith-menubar-secondary-buttoncolor. Applies to both buttons and info items.
    • borderColor (string, optional): Border color. Applies to both buttons and info items.
    • onClick (Function, required for buttons): Click handler function (event) => {}. Receives the click event as parameter. Omit for info items.
    • Additional properties: Any other properties are preserved and passed through, but not used by the default template.

Info item example (display-only, e.g. encounter-style CR/difficulty):

blacksmith.registerSecondaryBarItem('my-encounter', 'party-cr', {
    kind: 'info',
    zone: 'left',
    icon: 'fas fa-helmet-battle',
    label: 'Party CR',
    value: '2',
    group: 'cr',
    order: 0
});
blacksmith.updateSecondaryBarItemInfo('my-encounter', 'party-cr', { value: '3' });  // update when assessment changes

Progressbar item (kind: 'progressbar'): A horizontal progress bar (0–100%). Required: width, borderColor, barColor, progressColor, percentProgress. Optional: title, icon, leftLabel, leftIcon, rightLabel, rightIcon, height. Height defaults to 40% of secondary bar height if omitted. Update with updateSecondaryBarItemInfo(barTypeId, itemId, { percentProgress, leftLabel, rightLabel, ... }).

blacksmith.registerSecondaryBarItem('my-bar', 'hp-bar', {
    kind: 'progressbar',
    zone: 'middle',
    width: 200,
    height: 14,
    borderColor: 'rgba(0,0,0,0.5)',
    barColor: '#2d5016',
    progressColor: '#4a7c23',
    percentProgress: 100,
    leftLabel: '130',
    rightLabel: '130',
    group: 'stats',
    order: 0
});
blacksmith.updateSecondaryBarItemInfo('my-bar', 'hp-bar', { percentProgress: 65, leftLabel: '85', rightLabel: '130' });

Balancebar item (kind: 'balancebar'): A horizontal bar with origin at 0 in the middle; range -100 to +100. A marker (circle) is positioned at the current value: left of center for negative, right for positive. Required: width, borderColor, barColorLeft, barColorRight, markerColor. Optional: percentProgress (default 0), title, icon, leftLabel, rightLabel (inside the bar), leftIcon (outside the bar on the left), rightIcon (outside the bar on the right), height. Optional callbacks: onClick(event) for left-click; contextMenuItems (array or (itemId, item) => array of { name, icon, onClick }) for right-click context menu. Update with updateSecondaryBarItemInfo(barTypeId, itemId, { percentProgress, leftLabel, rightLabel, leftIcon, rightIcon, title, icon, barColorLeft, barColorRight, markerColor, borderColor }).

blacksmith.registerSecondaryBarItem('my-bar', 'approval', {
    kind: 'balancebar',
    zone: 'middle',
    width: 240,
    height: 14,
    borderColor: 'rgba(0,0,0,0.5)',
    barColorLeft: '#4a1c1c',
    barColorRight: '#1c4a1c',
    markerColor: '#7c2323',
    percentProgress: -25,
    leftLabel: 'Disapprove',
    rightLabel: 'Approve',
    group: 'stats',
    order: 0,
    contextMenuItems: [
        { name: 'Set to 0', icon: 'fas fa-minus', onClick: async () => { await blacksmith.setPartyReputation(0); } },
        { name: 'Set to 50', icon: 'fas fa-equals', onClick: async () => { await blacksmith.setPartyReputation(50); } }
    ]
});
blacksmith.updateSecondaryBarItemInfo('my-bar', 'approval', { percentProgress: 50, leftLabel: 'Disapprove', rightLabel: 'Approve' });

Returns: boolean - Success status

Group Behavior:

  • Items are organized into groups specified by the group parameter
  • Groups are separated by visual dividers
  • Group behavior is controlled by the group's mode (set in registerSecondaryBarType):
    • 'default' mode: Independent buttons. Each button can be toggleable or perform actions independently.
    • 'switch' mode: Radio-button behavior. Only one button in the group can be active at a time, and one must always be active.

Example:

blacksmith.registerSecondaryBarItem('cartographer', 'pencil-tool', {
    icon: 'fa-solid fa-pencil',          // Required: FontAwesome icon
    label: 'Pencil',                      // Optional: Text label
    tooltip: 'Draw with pencil tool',     // Optional: Custom tooltip
    active: false,                        // Optional: Active state
    order: 10,                           // Optional: Display order
    iconColor: '#3498db',                // Optional: Icon color
    buttonColor: 'rgba(100, 150, 200, 0.3)',  // Optional: Custom button background color
    borderColor: 'rgba(100, 150, 200, 0.5)',  // Optional: Custom border color
    onClick: (event) => {                // Required: Click handler
        console.log('Pencil tool clicked');
        // Update active state
        blacksmith.updateSecondaryBar({
            activeTool: 'pencil'
        });
    }
});

Timing-Safe: You can register items before the bar type is registered. Items will be queued and applied when the bar type is registered.

Updating Secondary Bar Item Active State

// Update an item's active state programmatically
blacksmith.updateSecondaryBarItemActive('cartographer', 'pencil-tool', true);

Parameters:

  • barTypeId (string, required): The bar type ID
  • itemId (string, required): The item ID to update
  • active (boolean, required): The active state to set

Returns: boolean - Success status

Note:

  • For 'switch' mode groups, setting an item active will automatically deactivate other items in the same group.
  • You cannot deactivate all items in a 'switch' mode group (one must always be active).

Example:

// Activate a tool in a switch group
blacksmith.updateSecondaryBarItemActive('cartographer', 'medium-line', true);
// This automatically deactivates 'small-line' and 'large-line' in the same group

Updating Secondary Bar Info Items

Use this to update the displayed value and/or label of an info item, the progress and labels of a progressbar item, or the balance and labels of a balancebar item, without re-registering it. Typical for encounter-style bars (Party CR, Monster CR, Difficulty), HP/resource bars, or approval/balance bars.

blacksmith.updateSecondaryBarItemInfo('my-encounter', 'party-cr', { value: '4' });
blacksmith.updateSecondaryBarItemInfo('my-encounter', 'difficulty', { value: 'Deadly', label: '', iconColor: '#a02020', borderColor: null });

Parameters:

  • barTypeId (string, required): The bar type ID
  • itemId (string, required): The info item ID to update
  • updates (Object, required): At least one of:
    • Info items: value, label, borderColor, buttonColor, iconColor
    • Progressbar items: percentProgress (number 0–100), leftLabel, rightLabel, leftIcon, rightIcon, title, icon, barColor, progressColor, borderColor
    • Balancebar items: percentProgress (number -100 to +100), leftLabel, rightLabel, leftIcon, rightIcon, title, icon, barColorLeft, barColorRight, markerColor, borderColor

Returns: boolean - Success status

If the bar is currently open, it re-renders so the new value/label is visible immediately.

Reputation API (party bar)

Party reputation is stored in the world setting blacksmithPartyData, keyed by scene id. Each scene’s data lives under blacksmithPartyData.scenes[sceneId] and includes reputation (and optionally uuid, title for display); reputation is a subset of this structure so other party data can be added later. Party reputation is shown in the party bar’s Reputation balancebar (left zone). Right-click the bar to set the current scene’s value, send a current-reputation card, or change reputation by ±1 or ±5 (each change posts a New Reputation card). Scale labels and descriptions come from resources/reputation.json. The following are exposed on game.modules.get('coffee-pub-blacksmith').api:

getPartyReputation(scene?)

  • Returns the party reputation for a scene (-100 to +100). Omit scene to use the current canvas scene. Value is read from game.settings.get(MODULE.ID, 'blacksmithPartyData').scenes[sceneId].reputation.
  • Returns: number (0 if no scene or no value stored).

setPartyReputation(value, scene?)

  • Sets the party reputation for a scene. GM only. Value is clamped to -100..+100. Updates blacksmithPartyData.scenes[sceneId] (reputation, and optionally uuid/title for the scene).
  • Returns: Promise<boolean>true if set, false if not GM or no scene.

getReputationScaleEntry(value)

  • Returns the scale entry from reputation.json for a given value (label, description, effects). Useful for custom UI or macros.
  • Returns: Promise<{ key, label, min, max, description, effects? } | null>.

postCurrentReputationCard(api?)

  • Posts a Current Reputation chat card: scene name, current value, and scale label/description from the JSON. Uses the chat card theme API (default theme).
  • Returns: Promise<void>.

postNewReputationCard(change, previousValue, newValue, api?)

  • Posts a New Reputation chat card: scene name, change (e.g. +5, -1), previous → new value, and scale label/description. Call after updating reputation (e.g. from the context menu).
  • Returns: Promise<void>.
const api = game.modules.get('coffee-pub-blacksmith')?.api;
const current = api.getPartyReputation();
await api.setPartyReputation(50);
api.updateSecondaryBarItemInfo('party', 'reputation', { percentProgress: 50 });
await api.postCurrentReputationCard();
const scale = await api.getReputationScaleEntry(50);

Registering Secondary Bar Toggle Tool

If you create a main toolbar button to toggle your secondary bar, register it so the button's active state automatically syncs when the bar opens/closes:

// 1. Register your secondary bar type
await blacksmith.registerSecondaryBarType('cartographer', {
    height: 60,
    persistence: 'manual'
});

// 2. Register your toggle button
blacksmith.registerMenubarTool('cartographer-toggle', {
    icon: 'fa-solid fa-map',
    name: 'cartographer-toggle',
    title: 'Toggle Cartographer Tools',
    tooltip: 'Toggle Cartographer Tools',
    zone: 'left',
    group: 'general',
    groupOrder: 999,
    order: 20,
    moduleId: 'coffee-pub-cartographer',
    gmOnly: false,
    leaderOnly: false,
    visible: true,
    toggleable: true,  // Important: Make it toggleable for active state syncing
    active: false,
    iconColor: null,
    buttonNormalTint: null,
    buttonSelectedTint: null,
    onClick: () => {
        blacksmith.toggleSecondaryBar('cartographer');
    }
});

// 3. Register the mapping so button state syncs automatically
blacksmith.registerSecondaryBarTool('cartographer', 'cartographer-toggle');

Parameters:

  • barTypeId (string, required): The secondary bar type ID
  • toolId (string, required): The menubar tool ID that toggles this bar

Returns: No return value.

Note:

  • The tool must be registered with toggleable: true for the active state to work
  • When a different secondary bar opens, your button will automatically deactivate
  • When your secondary bar opens, your button will automatically activate
  • When your secondary bar closes, your button will automatically deactivate

Why use this? Without registration, you'd need to manually sync button states in your onClick handler. With registration, Blacksmith handles it automatically when bars open/close/switching.

Unregistering Secondary Bar Items

// Remove an item from a secondary bar
blacksmith.unregisterSecondaryBarItem('cartographer', 'pencil-tool');

Parameters:

  • barTypeId (string, required): The bar type ID
  • itemId (string, required): The item ID to remove

Returns: boolean - Success status

Note: If the removed item was active in a 'switch' mode group, the first remaining item in that group will automatically become active.

Getting Secondary Bar Items

// Get all registered items for a bar type
const items = blacksmith.getSecondaryBarItems('cartographer');
// Returns: Array of item data objects

Parameters:

  • barTypeId (string, required): The bar type ID

Returns: Array<Object> - Array of registered item data objects

Styling Secondary Bar Items

The default secondary bar items use CSS classes that you can override in your module's stylesheet:

CSS Classes:

  • .blacksmith-menubar-secondary .secondary-bar-item - Base item button
  • .blacksmith-menubar-secondary .secondary-bar-item.active - Active/selected item
  • .blacksmith-menubar-secondary .secondary-bar-item:hover - Hover state
  • .blacksmith-menubar-secondary .secondary-bar-item:active - Clicked/pressed state
  • .blacksmith-menubar-secondary .secondary-bar-item i - Icon element
  • .blacksmith-menubar-secondary .secondary-bar-item-label - Label text
  • .blacksmith-menubar-secondary .secondary-bar-toolbar - Container for all items

CSS Variables Available:

  • --blacksmith-menubar-fontcolor - Primary bar text color
  • --blacksmith-menubar-fontsize - Primary bar font size
  • --blacksmith-menubar-iconsize - Primary bar icon size
  • --blacksmith-menubar-secondary-height - Secondary bar height
  • --blacksmith-menubar-secondary-fontcolor - Secondary bar text color
  • --blacksmith-menubar-secondary-fontsize - Secondary bar font size
  • --blacksmith-menubar-secondary-iconsize - Secondary bar icon size
  • --blacksmith-menubar-secondary-buttoncolor - Default button background color (used when buttonColor not specified)
  • --blacksmith-menubar-secondary-bordercolor - Default border color (used when borderColor not specified)

Custom Styling Example:

You can customize styling in two ways:

  1. Per-item colors (recommended for individual button styling):
blacksmith.registerSecondaryBarItem('cartographer', 'pencil-tool', {
    icon: 'fa-solid fa-pencil',
    label: 'Pencil',
    buttonColor: 'rgba(100, 150, 200, 0.3)',  // Custom background
    borderColor: 'rgba(100, 150, 200, 0.5)',  // Custom border
    onClick: () => {}
});
  1. CSS overrides (for global styling of all items in a bar type):
/* In your module's CSS file */
.blacksmith-menubar-secondary[data-bar-type="cartographer"] .secondary-bar-item {
    background-color: rgba(100, 150, 200, 0.3);
    border-radius: 5px;
}

.blacksmith-menubar-secondary[data-bar-type="cartographer"] .secondary-bar-item.active {
    background-color: rgba(100, 150, 200, 0.6);
    box-shadow: 0 0 8px rgba(100, 150, 200, 0.4);
}

Note:

  • Per-item colors (via buttonColor and borderColor parameters) are applied as inline styles and take precedence over CSS rules
  • CSS hover/active states will still apply on top of the inline styles
  • Styles are defined in styles/menubar.css in the Blacksmith module. You can override them using more specific selectors in your module's CSS.

Creating a Custom Secondary Bar Template

For complex UIs (like the combat bar with portraits and health rings), you can provide a custom Handlebars template:

  1. Create your template file:
templates/partials/menubar-{your-type-id}.hbs
  1. Register the bar type with templatePath:
await blacksmith.registerSecondaryBarType('my-complex-bar', {
    height: 80,
    persistence: 'manual',
    templatePath: 'modules/my-module/templates/partials/menubar-my-complex-bar.hbs'
});
  1. Template Example:
{{!-- templates/partials/menubar-my-complex-bar.hbs --}}
<div class="my-complex-toolbar">
    <div class="toolbar-header">
        <h3>{{title}}</h3>
    </div>
    <div class="toolbar-content">
        {{#each items}}
        <div class="complex-item">
            {{name}}: {{value}}
        </div>
        {{/each}}
    </div>
</div>

Important:

  • Custom templates receive secondaryBar.data as their context
  • Templates are automatically registered as Handlebars partials: menubar-{typeId}
  • You do NOT need to modify Blacksmith's main template - it handles custom templates automatically

Complete Examples

Example 1: Simple Toolbar with Groups (Default Tool System)

Hooks.once('ready', async () => {
    const blacksmith = game.modules.get('coffee-pub-blacksmith')?.api;
    
    if (!blacksmith) {
        console.error('Blacksmith API not available');
        return;
    }
    
    // 1. Register the bar type with group configuration
    await blacksmith.registerSecondaryBarType('cartographer', {
        height: 60,
        persistence: 'manual',
        autoCloseDelay: 10000,
        groups: {
            'tools': {
                mode: 'default',      // Independent buttons
                order: 0
            },
            'line-size': {
                mode: 'switch',       // Only one active at a time
                order: 10
            },
            'colors': {
                mode: 'switch',       // Only one active at a time
                order: 20
            }
        }
    });
    
    // 2. Register tools in the 'tools' group (default mode - independent)
    blacksmith.registerSecondaryBarItem('cartographer', 'pencil-tool', {
        icon: 'fa-solid fa-pencil',
        label: 'Pencil',
        group: 'tools',
        toggleable: true,             // Can be toggled on/off
        order: 10,
        onClick: () => {
            console.log('Pencil tool toggled');
        }
    });
    
    blacksmith.registerSecondaryBarItem('cartographer', 'eraser-tool', {
        icon: 'fa-solid fa-eraser',
        label: 'Eraser',
        group: 'tools',
        toggleable: true,
        order: 20,
        onClick: () => {
            console.log('Eraser tool toggled');
        }
    });
    
    // 3. Register line size options (switch group - only one active)
    blacksmith.registerSecondaryBarItem('cartographer', 'small-line', {
        icon: 'fa-solid fa-minus',
        label: 'Small',
        group: 'line-size',
        order: 10,
        onClick: () => {
            console.log('Small line size selected');
        }
    });
    
    blacksmith.registerSecondaryBarItem('cartographer', 'medium-line', {
        icon: 'fa-solid fa-equals',
        label: 'Medium',
        group: 'line-size',
        order: 20,
        onClick: () => {
            console.log('Medium line size selected');
        }
    });
    
    blacksmith.registerSecondaryBarItem('cartographer', 'large-line', {
        icon: 'fa-solid fa-grip-lines',
        label: 'Large',
        group: 'line-size',
        order: 30,
        onClick: () => {
            console.log('Large line size selected');
        }
    });
    
    // 4. Register color options (switch group)
    blacksmith.registerSecondaryBarItem('cartographer', 'color-red', {
        icon: 'fa-solid fa-circle',
        label: 'Red',
        group: 'colors',
        iconColor: '#ff0000',              // Red icon color
        buttonColor: 'rgba(255, 0, 0, 0.5)',
        order: 10,
        onClick: () => {
            console.log('Red color selected');
        }
    });
    
    blacksmith.registerSecondaryBarItem('cartographer', 'color-blue', {
        icon: 'fa-solid fa-circle',
        label: 'Blue',
        group: 'colors',
        iconColor: '#0000ff',              // Blue icon color
        buttonColor: 'rgba(0, 0, 255, 0.5)',
        order: 20,
        onClick: () => {
            console.log('Blue color selected');
        }
    });
    
    // 5. Register a menubar tool to toggle the secondary bar
    blacksmith.registerMenubarTool('cartographer-toggle', {
        icon: 'fa-solid fa-map',
        name: 'cartographer-toggle',
        title: 'Toggle Cartographer Tools',
        zone: 'left',
        order: 20,
        moduleId: 'coffee-pub-cartographer',
        toggleable: true,  // Enable toggleable to show active state
        onClick: () => {
            blacksmith.toggleSecondaryBar('cartographer');
        }
    });
    
    // 6. Register the tool-to-bar mapping for automatic button state syncing
    blacksmith.registerSecondaryBarTool('cartographer', 'cartographer-toggle');
});

This creates a toolbar with:

  • Tools group (left): Pencil and Eraser (independent toggleable buttons)
  • Divider
  • Line Size group (middle): Small, Medium, Large (switch mode - only one active)
  • Divider
  • Colors group (right): Red, Blue (switch mode - only one active)

Example 2: Complex Toolbar (Custom Template)

Hooks.once('ready', async () => {
    const blacksmith = game.modules.get('coffee-pub-blacksmith')?.api;
    
    // Register with custom template
    await blacksmith.registerSecondaryBarType('my-complex-bar', {
        height: 80,
        persistence: 'manual',
        templatePath: 'modules/my-module/templates/partials/menubar-my-complex-bar.hbs'
    });
    
    // Open the bar with data for the template
    blacksmith.openSecondaryBar('my-complex-bar', {
        data: {
            title: 'Complex Toolbar',
            items: [
                { name: 'Item 1', value: 100 },
                { name: 'Item 2', value: 200 }
            ]
        }
    });
});

Example 3: Timing-Safe Registration (Items Before Bar Type)

// Items can be registered before the bar type - they'll be queued and applied later
Hooks.once('ready', async () => {
    const blacksmith = game.modules.get('coffee-pub-blacksmith')?.api;
    
    // Register items first (they'll be queued)
    blacksmith.registerSecondaryBarItem('cartographer', 'pencil-tool', {
        icon: 'fa-solid fa-pencil',
        label: 'Pencil',
        onClick: () => console.log('Pencil')
    });
    
    // Register bar type later - items will be automatically applied
    await blacksmith.registerSecondaryBarType('cartographer', {
        height: 60,
        persistence: 'manual'
    });
    // Items are now active!
});

Default Tool System vs. Custom Templates

Feature Default Tool System Custom Template
Recommended Use this by default ❌ Only when absolutely necessary
Complexity Simple toolbars with buttons Complex UIs (portraits, health rings, etc.)
Setup Register items, no template needed Create Handlebars template partial
Flexibility Standardized button layout Full control over HTML/CSS
Use Case Drawing tools, simple controls Combat bar, complex dashboards
Template Path Not needed (omit templatePath) Required (templatePath in config)
Example Cartographer drawing tools Combat tracker with portraits
Maintenance Low - API handles everything Higher - maintain template, ensure compatibility

Secondary Bar vs. Regular Tools

Feature Regular Tools Secondary Bar
Location In main menubar (left/middle/right zones) Below main menubar
Space Limited (icon + label) Full-width toolbar
Multiple Many tools can be visible Only one bar can be open
Use Case Quick actions, status indicators Complex toolbars, specialized interfaces
Template Not needed Optional (default tool system) or custom template

Best Practices

  1. Default to default tool system: Always use the default tool system unless you absolutely need a custom template
  2. Register early: Register your secondary bar type in a ready hook (async if using custom templates)
  3. Choose the right approach:
    • Default tool system: Use for all simple toolbars (drawing tools, filters, toggles, etc.)
    • Custom templates: Only use for complex UIs that cannot be achieved with buttons (portraits, health rings, complex nested layouts)
  4. Unique type IDs: Use descriptive, unique type IDs (e.g., 'cartographer', not 'tools')
  5. Timing-safe registration: Items can be registered before bar types - they'll be queued automatically
  6. Template organization: If using custom templates, keep them simple and focused
  7. Data structure: For custom templates, pass structured data for flexibility
  8. Cleanup: Consider closing your bar and unregistering items when your module is disabled

Troubleshooting

Bar doesn't open:

  • Verify the bar type is registered: Check console for registration success
  • Check if another bar is open (it should close automatically)
  • Verify the template partial exists and is named correctly

Template not rendering:

  • For custom templates: Ensure the template path is correct and the file exists
  • Verify templatePath is provided in registerSecondaryBarType config
  • For default tool system: Ensure items are registered with registerSecondaryBarItem
  • Check that the bar type is registered before opening

Bar closes unexpectedly:

  • Check the persistence setting (auto bars close after delay)
  • Verify no other code is calling closeSecondaryBar()

Version History

  • v13.0.0+: Enhanced Menubar Features

    • Main Toolbar Grouping System: Added tiered grouping system for organizing tools
      • Added group parameter to registerMenubarTool for organizing tools into groups
      • Added groupOrder parameter to control group positioning within zones
      • Groups are separated by visual dividers in the menubar
      • Blacksmith-defined groups take precedence over other modules
      • Organization hierarchy: Zone -> Group -> Module -> Order
      • Blacksmith groups: "combat" (order 1), "utility" (order 2), "party" (order 3), "general" (order 999)
      • Dynamic group creation: modules can create new groups automatically
    • Button Color Customization: Added button tinting support
      • Added buttonNormalTint parameter for custom normal button background color
      • Added buttonSelectedTint parameter for custom active/selected button background color
      • Both parameters accept any valid CSS color format (hex, rgba, named colors, etc.)
    • Enhanced Tool Properties:
      • title and tooltip can now be functions that return strings for dynamic content
      • visible can be a function for dynamic visibility
      • iconColor accepts any valid CSS color format
    • Main Toolbar Toggleable: Added toggleable and active parameters to registerMenubarTool
    • Added updateMenubarToolActive() method for programmatic state updates
    • Secondary Bar Groups: Added group system with two modes:
      • 'default' mode: Independent buttons (supports toggleable)
      • 'switch' mode: Radio-button behavior (only one active, one always active)
    • Added groups parameter to registerSecondaryBarType for group configuration
    • Added group parameter to registerSecondaryBarItem for organizing items
    • Added toggleable parameter to registerSecondaryBarItem for default-mode groups
    • Added updateSecondaryBarItemActive() method for programmatic state updates
    • Visual dividers between groups
    • Group ordering and sorting
    • Multiple modules can contribute groups to the same bar type
    • Hybrid Secondary Bar System:
      • Added default tool system (registerSecondaryBarItem, unregisterSecondaryBarItem, getSecondaryBarItems)
      • Added templatePath parameter to registerSecondaryBarType for custom templates
      • Timing-safe registration: items can be registered before bar types
      • Automatic template handling for both default and custom templates
      • Backward compatible with existing combat bar implementation
  • v12.1.8: Secondary Bar API

    • Added registerSecondaryBarType() method
    • Added openSecondaryBar(), closeSecondaryBar(), toggleSecondaryBar() methods
    • Added updateSecondaryBar() method for real-time updates
    • Tab-like behavior: only one secondary bar open at a time
    • Automatic switching between secondary bars
    • Persistence modes (manual/auto) with auto-close support
  • v12.1.7: Enhanced notification system

    • Added updateNotification() method for real-time notification updates
    • Added getNotificationIdsByModule() helper for module notification tracking
    • Improved timeout management to prevent memory leaks
    • Enhanced notification lifecycle management
    • Added comprehensive notification management examples
  • v12.1.6: Initial menubar API release

    • Basic tool registration/unregistration
    • Zone-based organization (left, middle, right)
    • Three-tier visibility system (GM, Leader, Player)
    • Dynamic visibility support
    • Tool ordering and organization
    • Basic notification system

Last Updated: Current session - Secondary Bar API with tab-like switching
Status: Production ready with comprehensive integration support
Next Milestone: Enhanced menubar features and ecosystem integration

⚠️ **GitHub.com Fallback** ⚠️