API: Menubar - Drowbe/coffee-pub-blacksmith GitHub Wiki
Audience: Developers integrating with Blacksmith and leveraging the exposed API.
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.
// 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
});
}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.
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]
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"
);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
});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);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`);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`);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`);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`);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 tonameif omitted. Can be an empty string or null for icon-only buttons. -
tooltip(string|Function, optional): Alternative tooltip text. If provided, overridestitlefor 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) => arrayfor 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>').submenuis 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).
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);Removes a tool from the Blacksmith menubar system.
Parameters:
-
toolId(string): Unique identifier for the tool
Returns: boolean - Success status
Gets all registered tools.
Returns: Map - Map of all registered tools (toolId -> toolData)
Gets all tools registered by a specific module.
Parameters:
-
moduleId(string): Module identifier
Returns: Array - Array of tools registered by the module
Checks if a tool is registered.
Parameters:
-
toolId(string): Unique identifier for the tool
Returns: boolean - Whether the tool is registered
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 bygroupOrder -
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
ordervalue - Modules within a group are sorted with Blacksmith first, then by minimum tool
ordervalue
Note: Only visible tools are included in the returned structure. Tools filtered by gmOnly, leaderOnly, or visible are excluded.
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): Iftrue, re-render immediately. Iffalse(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
}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 omithide) 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 };
});Unregister a visibility override (e.g. on module unload).
Parameters:
-
moduleId(string): The same module ID used when registering
Example:
blacksmith?.unregisterMenubarVisibilityOverride('my-module');The menubar system organizes tools into three predefined zones:
-
left- Action tools (movement, interface, voting, skill checks) -
middle- General tools and utilities (supports grouping with visual dividers) -
right- Informational tools (leader display, timer)
-
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
- Tools within each zone can be organized into groups using the
groupparameter - Groups are separated by visual dividers in the menubar
- The grouping system is most commonly used in the
middlezone, but works in all zones - Groups are sorted by
groupOrder, with lower numbers appearing first
The menubar uses a three-tier visibility system:
- 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
-
gmOnly: true: Only visible to Game Masters -
leaderOnly: true: Visible to party leaders and GMs - Default: Visible to all users
// 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
}
});The menubar uses a tiered grouping system to organize tools logically:
Tools are organized in the following hierarchy:
-
Zone (
left,middle,right) -
Group (e.g.,
"combat","utility","party","general") - Module (tools from the same module are clustered together)
-
Order (within the module, by
ordervalue)
- 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
ordervalue of their tools
-
Within a module, tools are sorted by
ordervalue
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
- Blacksmith groups take precedence: If Blacksmith defines a group order, it overrides any other module's definition
- "general" group is always last: The default group always has order 999 and appears after all other groups
-
Dynamic groups: Modules can create new groups by specifying a
groupname. If the group doesn't exist, it's created automatically -
Auto-assignment: If a module specifies a
groupOrder>= 999, it's automatically assigned to the first available slot below 999
-
gmOnlyandleaderOnlyaffect visibility only - tools are still added to their specified group - Hidden tools don't affect group ordering or module clustering
-
Lower
groupOrdervalues appear first within each zone -
Recommended
groupOrderranges:-
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)
-
-
Lower
ordervalues appear first within each module - Blacksmith tools take precedence over other modules within the same group
-
Recommended
orderranges:-
1-10: Core/primary tools -
11-50: Secondary tools -
51-100: Utility tools -
101+: Optional/advanced tools
-
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();
}
});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);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!");
}
});// 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");
}
});// 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!");
}
});// 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");
}
});// 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);// 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!");
}
});// 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!");
}
});// 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);
});
}
});The API includes robust error handling:
-
Invalid tool data: Returns
falseand logs error -
Duplicate tool IDs: Returns
false(tools must be unique) -
Missing required properties: Returns
falseand logs error - API not available: Check for API availability before use
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.
// 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
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
});// Bind the function to maintain its original context
blacksmith.registerMenubarTool('my-tool', {
onClick: myFunction.bind(this) // Maintains original context
});// Access your module's API instead of direct imports
const myFunction = () => {
const myAPI = game.modules.get('my-module')?.api;
myAPI.MyManager?.doSomething();
};// 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()
});Use self-contained functions (Solution 1) because they:
- Are explicit about dependencies
- Work regardless of execution context
- Are easier to debug
- Are more reusable
- 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
-
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: () => {} });
-
Self-Contained Functions: Make onClick functions completely self-contained with all dependencies imported
-
Unique Tool IDs: Use descriptive, unique tool identifiers
-
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
-
-
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
groupOrderto control group positioning
- Use existing Blacksmith groups (
-
Consistent Ordering: Use consistent
ordervalues within your module:- Lower numbers appear first
- Reserve ranges for different tool types
-
Color Format Flexibility: When using
iconColor,buttonNormalTint, orbuttonSelectedTint, 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%)'
- Hex:
-
Module Cleanup: Unregister tools when your module is disabled
-
Error Handling: Always check return values and handle errors gracefully
-
API Availability: Check if the API is available before using it
-
Scope Awareness: Understand that onClick functions execute in Blacksmith's context, not your module's context
- 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
- Use correct API path:
blacksmith.registerMenubarTool()notblacksmith.api.registerMenubarTool() - Wait for
readyhook, notblacksmithUpdatedhook - Check if module is active:
game.modules.get('coffee-pub-blacksmith')?.active
- Verify
zoneproperty is set correctly - Check zone spelling (must match "left", "middle", or "right" exactly)
- Default zone is "left" if not specified
-
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
- Verify
onClickfunction is provided and valid - Check for JavaScript errors in onClick function
- Ensure tool is not disabled by visibility logic
For issues or questions about the Blacksmith Menubar API:
- Check this documentation first
- Review console logs for error messages
- Test with a simple tool registration
- Contact the Blacksmith development team
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
- 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
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
iconorimageandonClick. Same as before. -
Info: Display-only item with
labeland/orvalue. NoonClick. UseupdateSecondaryBarItemInfo(barTypeId, itemId, { value, label })to update the displayed text so the bar can show dynamic content (e.g. Party CR, Difficulty) without re-registering.
-
Button (default): Clickable item with
Before you can open a secondary bar, you must register its type. The system supports two approaches:
- Default Tool System (recommended - use this unless custom template is absolutely necessary) - Register individual tools/items
- 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
groupBannerEnabledistrue, 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
// 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.
// Close the currently open secondary bar
const success = blacksmith.closeSecondaryBar();
if (success) {
console.log("Secondary bar closed!");
}Returns: boolean - Success status
// 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 asopenSecondaryBar)
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
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 bars support groups - logical collections of buttons that are visually separated by dividers. Groups support two behavior modes:
'default' mode (default):
- Buttons are independent
- Each button can be toggleable (use
toggleable: truein 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
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)
});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
// ...
});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 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
}
});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 withupdateSecondaryBarItemInfo. -
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 withoutvalue. -
value(string, optional): For info items only. Display value (e.g. "2", "Medium"). Can be updated later withupdateSecondaryBarItemInfo. -
tooltip(string, optional): Tooltip text on hover. If omitted, useslabelas 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. AddsactiveCSS class whentrue. 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 withoutorderappear after items withorder, sorted alphabetically byitemId. -
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 changesProgressbar 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
groupparameter - Groups are separated by visual dividers
- Group behavior is controlled by the group's
mode(set inregisterSecondaryBarType):-
'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.
// 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 groupUse 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
-
Info items:
Returns: boolean - Success status
If the bar is currently open, it re-renders so the new value/label is visible immediately.
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
sceneto use the current canvas scene. Value is read fromgame.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>—trueif set,falseif not GM or no scene.
getReputationScaleEntry(value)
- Returns the scale entry from
reputation.jsonfor 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);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: truefor 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.
// 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.
// Get all registered items for a bar type
const items = blacksmith.getSecondaryBarItems('cartographer');
// Returns: Array of item data objectsParameters:
-
barTypeId(string, required): The bar type ID
Returns: Array<Object> - Array of registered item data objects
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 whenbuttonColornot specified) -
--blacksmith-menubar-secondary-bordercolor- Default border color (used whenborderColornot specified)
Custom Styling Example:
You can customize styling in two ways:
- 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: () => {}
});- 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
buttonColorandborderColorparameters) 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.cssin the Blacksmith module. You can override them using more specific selectors in your module's CSS.
For complex UIs (like the combat bar with portraits and health rings), you can provide a custom Handlebars template:
- Create your template file:
templates/partials/menubar-{your-type-id}.hbs
- 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'
});- Template Example:
Important:
- Custom templates receive
secondaryBar.dataas 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
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)
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 }
]
}
});
});// 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!
});| 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 |
| 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 |
- Default to default tool system: Always use the default tool system unless you absolutely need a custom template
-
Register early: Register your secondary bar type in a
readyhook (async if using custom templates) -
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)
-
Unique type IDs: Use descriptive, unique type IDs (e.g.,
'cartographer', not'tools') - Timing-safe registration: Items can be registered before bar types - they'll be queued automatically
- Template organization: If using custom templates, keep them simple and focused
- Data structure: For custom templates, pass structured data for flexibility
- Cleanup: Consider closing your bar and unregistering items when your module is disabled
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
templatePathis provided inregisterSecondaryBarTypeconfig - For default tool system: Ensure items are registered with
registerSecondaryBarItem - Check that the bar type is registered before opening
Bar closes unexpectedly:
- Check the
persistencesetting (auto bars close after delay) - Verify no other code is calling
closeSecondaryBar()
-
v13.0.0+: Enhanced Menubar Features
-
Main Toolbar Grouping System: Added tiered grouping system for organizing tools
- Added
groupparameter toregisterMenubarToolfor organizing tools into groups - Added
groupOrderparameter 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
- Added
-
Button Color Customization: Added button tinting support
- Added
buttonNormalTintparameter for custom normal button background color - Added
buttonSelectedTintparameter for custom active/selected button background color - Both parameters accept any valid CSS color format (hex, rgba, named colors, etc.)
- Added
-
Enhanced Tool Properties:
-
titleandtooltipcan now be functions that return strings for dynamic content -
visiblecan be a function for dynamic visibility -
iconColoraccepts any valid CSS color format
-
-
Main Toolbar Toggleable: Added
toggleableandactiveparameters toregisterMenubarTool - 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
groupsparameter toregisterSecondaryBarTypefor group configuration - Added
groupparameter toregisterSecondaryBarItemfor organizing items - Added
toggleableparameter toregisterSecondaryBarItemfor 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
templatePathparameter toregisterSecondaryBarTypefor 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
- Added default tool system (
-
Main Toolbar Grouping System: Added tiered grouping system for organizing tools
-
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
- Added
-
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
- Added
-
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