Skip to content

api window

github-actions[bot] edited this page Oct 8, 2026 · 35 revisions

Blacksmith Window API Documentation

Audience: Developers integrating with Blacksmith and opening or registering Application V2–style windows.

This document describes the Window API: how to build standard editor windows or lightweight tool windows, register a window type with Blacksmith, and open it by id. It follows the same registration pattern as the Toolbar API: you register a window type (id + descriptor); Blacksmith routes "open this window" to your opener. You keep full control of content; Blacksmith provides the presentation contract and shared Application V2 behavior.

Status: The Window API is exposed on game.modules.get('coffee-pub-blacksmith').api. Use this document as the contract for integration.

Related docs:

  • documentation/architecture/architecture-window.md — Internal architecture (zone contract, registry, base class).
  • A working example window ships in the coffee-pub-prototype module, as loadable code rather than a copy-paste block.

Overview

The Window API allows external modules to:

  1. Register a window type with a unique id and a descriptor (how to open the window).
  2. Open that window by id via Blacksmith (openWindow(windowId, options)), so toolbars, macros, and other modules can open your window without importing your class.
  3. Unregister the window type when the module is disabled (cleanup).

You implement the window itself (Application V2 class, template, getData, actions) and decide which zones to use (option bar, header, body, action bar). Blacksmith does not inject content into your template; it only provides the zone contract and, when implemented, optional shared behavior (e.g. base class, scroll/delegation helpers).


Window registry vs public base class

These are two different supported surfaces on game.modules.get('coffee-pub-blacksmith').api:

Surface Purpose Minimises correctly
Registry (registerWindow, openWindow, unregisterWindow, …) Register an id and an opener so toolbars, macros, and other modules can open your window without importing your class. n/a — the registry does not own presentation
Standard base (BlacksmithWindowBaseV2, or getWindowBaseV2()) Subclass Blacksmith's full Application V2 base for editors, forms, and other windows that use the five-zone template. Yes — windowSizeConstraints are published as custom properties and zeroed while minimised
Tool base (BlacksmithToolWindowBaseV2, or getToolWindowBaseV2()) Subclass the compact Application V2 presentation for lightweight, persistent canvas tools and palettes. Yes — same mechanism, inherited from the standard base
Fullscreen base (BlacksmithFullscreenWindowBaseV2, or getFullscreenWindowBaseV2()) Subclass the viewport-covering, blocking presentation for handouts, cutscenes, and reveals. n/a — the window has no frame and cannot be minimised
  • Use the registry when something else (Blacksmith toolbar, another module, a macro) should call openWindow('your-id').
  • Use api.BlacksmithWindowBaseV2 for standard windows, api.BlacksmithToolWindowBaseV2 for compact tools, and api.BlacksmithFullscreenWindowBaseV2 for a surface that takes over the screen.

A class you extends comes in by import, not from module.api

extends is evaluated when your module script is evaluated, and module.api cannot be read then: game does not exist yet, so a top-level game.modules.get('coffee-pub-blacksmith') throws Cannot read properties of undefined (reading 'get'). ES modules cache a failed evaluation, so that throw disables your module for the rest of the session rather than being retried on the next access.

The dangerous version of this mistake is the defensive one, and it does not throw. A resolver written with optional chaining -- game?.modules?.get('coffee-pub-blacksmith')?.api?.BlacksmithWindowBaseV2 -- never errors at evaluation time. It simply yields undefined every time, so the fallback base is used on every render and the module.api branch never runs once. That is worse than the crash: a thrown error is found the day it ships, while a silent fallback looks correct and can run for months. Reported by the Regent session on 2026-09-09, where it had done exactly that.

Import the base class from the API bridge, which is a real ES module and therefore resolves at evaluation time:

import {
    BlacksmithWindowBaseV2,
    BlacksmithToolWindowBaseV2,
    BlacksmithFullscreenWindowBaseV2
} from '/modules/coffee-pub-blacksmith/api/blacksmith-api.js';

export class MyWindow extends BlacksmithWindowBaseV2 { }

BLACKSMITH_WINDOW_STYLES, BLACKSMITH_TOOL_TITLEBARS, BLACKSMITH_TOOL_THEMES, BLACKSMITH_FULLSCREEN_LAYOUTS, and BLACKSMITH_FULLSCREEN_FITS are exported from the same path, and are the same objects as api.windowStyles, api.toolTitlebars, api.toolThemes, api.fullscreenLayouts, and api.fullscreenFits.

The bridge is the supported path for this one purpose. Do not deep-link anything under scripts/ — those paths are not the stable contract.

Availability timing

  • All three base classes, their getters, and the style constants — BlacksmithWindowBaseV2, BlacksmithToolWindowBaseV2, BlacksmithFullscreenWindowBaseV2, getWindowBaseV2(), getToolWindowBaseV2(), getFullscreenWindowBaseV2(), windowStyles, toolTitlebars, toolThemes, fullscreenLayouts, and fullscreenFits are patched on module.api as soon as Blacksmith's module script has finished loading (before init / ready), as long as your module loads after coffee-pub-blacksmith in the manifest (or depends on it). This is for code that runs after game exists — a class body evaluated at module top level cannot use it; import the class instead, as above.
  • Window registry (registerWindow, openWindow, …) — Placeholders are cleared when the api-windows dynamic import completes during Blacksmith's init (after await addToolbarButton()). Prefer calling registerWindow / openWindow from ready or after await BlacksmithAPI.waitForReady() so the rest of the stack is consistent.
  • Most other module.api members — The public shell (registerModule, utils, HookManager, menubar bindings, etc.) is assigned synchronously at the start of Blacksmith's init (before any await there). Asset-backed fields (assetLookup, merged BLACKSMITH constants) finish during Blacksmith's ready; use BlacksmithAPI.waitForReady() if you need that data. See documentation/architecture/architecture-blacksmith.md §3.2–3.3.

Zone Contract (Summary)

Windows that follow the Blacksmith contract use up to six zones. Only Body is required; the rest are optional.

Zone Required? Description
Title bar Yes (Foundry) Foundry chrome; not in your template.
Option bar Optional Filters, toggles, global options (e.g. REFRESH CACHE, TOKENS/PORTRAITS).
Header Optional Icon, title, subtitle, header-right. Omit for minimal windows.
Tools Optional Bar below header: single content area for search, filters, progress bars, etc.
Body Yes Scrollable area; you inject your content here (forms, lists, grids, etc.).
Action bar Optional Bottom bar: secondary left, primary right.

The canonical markup for these zones is templates/window-template.hbs; each zone is named by a blacksmith-window-template-* class there.


Lightweight tool/palette style

Use BlacksmithToolWindowBaseV2 for small utilities that should remain open over the canvas: dice trays, health controls, macro palettes, trackers, and similar tools. It uses Foundry's native Application V2 frame, so dragging, focus/z-order, minimizing, closing, and window lifecycle remain standard. It deliberately omits the full editor header and five-zone layout.

The base supplies the complete shared visual shell even when the consumer returns an empty body: parchment surface, gold border, matching controls, and compact window shadow. No additional parchment class is required. Consumers own only their body content and may override these public custom properties when a deliberate tool-specific variation is required:

Property Purpose
--blacksmith-tool-background Shared frame, title-bar, and empty-body surface. May be a color or gradient.
--blacksmith-tool-border Outer frame border.
--blacksmith-tool-divider Title/toolbar/footer dividers.
--blacksmith-tool-text Default body and control text.
--blacksmith-tool-accent Title text and primary frame accent.
--blacksmith-tool-field-background Surface of inputs, selects, and textareas.
--blacksmith-tool-field-border Field border.
--blacksmith-tool-field-text Field text.
--blacksmith-tool-field-placeholder Placeholder text.
--blacksmith-tool-field-focus-border / --blacksmith-tool-field-focus-ring Focus outline and glow.
--blacksmith-tool-field-option-background / --blacksmith-tool-field-option-text <option> rows in an open dropdown.
--blacksmith-tool-surface-raised Decorative raised area — group headings, banded rows. May be translucent.
--blacksmith-tool-surface-sunken Recessed area — list wells, inset panels.
--blacksmith-tool-surface-hover Row and item hover.
--blacksmith-tool-surface-selected Selected row or item.
--blacksmith-tool-text-muted Secondary text — captions, badges, status lines.
--blacksmith-tool-scrim Backing that must stay legible over arbitrary content. Use for sticky elements.

Content surfaces: use these instead of picking a colour

The frame variables describe the shell. These describe what you put inside it. Reach for them for rows, headings, hover, and selection rather than choosing a colour: any literal you pick is correct in one theme and wrong in the other two, which is how a Tool window ends up looking right in Light and broken in Glass.

Two distinctions worth getting right:

  • surface-raised vs scrim. raised is decorative and may be translucent. scrim guarantees legibility over whatever is behind it. The two are near-identical under Light and Dark and differ sharply under Glass, which is the case they exist for. For a sticky element, reach for raised plus backdrop-filter: blur(6px) rather than scrim. Both mask the content scrolling underneath, but scrim under Glass is nearly opaque and reads as a hole punched through the frost — heavier than the window's own title bar. A blur smears what passes beneath instead of hiding it, which is all a heading needs, and under Light and Dark raised is already opaque so the blur costs nothing. Save scrim for something that must stay readable over genuinely arbitrary content, like a full-width overlay.
  • text-muted rather than opacity. Dimming with opacity fades a whole element including its background and borders, and compounds when nested. text-muted is tuned per theme and touches only the colour.

Blacksmith's own styles/window-compendium-search.css is the reference: it contains no colour literals at all — every surface, tone, and field comes from this family, and the window follows Light, Dark, and Glass with no theme-specific rules of its own.

These are component properties of the Tool shell, not design tokens. Global tokens live in styles/vars.css, are documented in designsystem/design-tokens.md, and are enforced by tools/check-design-tokens.mjs; they carry one fixed value each and so cannot express something that changes per theme. Do not add theme-varying values to vars.css.

Form fields follow the theme automatically

A Tool window repaints input, select, and textarea inside it from the --blacksmith-tool-field-* family, covering both bare elements and the shared blacksmith-input / blacksmith-select / blacksmith-textarea classes. A consumer writes an ordinary field and it follows the user's Light, Dark, or Glass choice with no theme-aware code.

This exists because the shared form-control classes in styles/window-form-controls.css are built for Blacksmith's dark standard windows and hard-code a #222 surface, which renders as a black box on a parchment or frosted shell. Do not work around that by hard-coding a field color in your own stylesheet — override the variables above, scoped to your application class, if you need a deliberate variation.

Two things a consumer still owns:

  • An open <select> dropdown is an OS popup, not part of the page. It inherits nothing from the window, which is why it has its own option pair. Those must stay opaque; a translucent value renders as the browser default.
  • A sticky element inside your body needs its own opaque surface. Under Glass, --blacksmith-tool-background is deliberately near-transparent, so a sticky header painted with it lets scrolled content read straight through. Give it a solid background under .blacksmith-window-tool-theme-glass.

Scope any override to the consumer's own application class. Do not copy Blacksmith's combatant-card CSS:

.application.blacksmith-window-tool.my-module-tool-window {
    --blacksmith-tool-background: #111;
    --blacksmith-tool-text: #eee;
}

Ephemeral tools: no registration required

Nothing in either base assumes a Tool window is registered or persistent. There is no menubar coupling, no singleton enforcement, no static instance tracking, and no fixed id in DEFAULT_OPTIONS. Constructing and rendering directly is the supported path, and it is how Blacksmith opens its own Send Toast window (api-menubar.js):

await new MyTransferTool({ id: `my-transfer-${foundry.utils.randomID()}` }).render({ force: true });

Register a window only when something else — a toolbar tool, a macro, another module — needs to open it by id. A tool that only ever opens from an in-flow action gains nothing from registration.

Four things matter for a tool that is ephemeral or can have several instances open at once:

  • Give each instance a distinct id. Two Application V2 instances sharing an id collide in the DOM.
  • Set rememberPosition: false. windowPositionKey defaults to the class name, so sibling instances share one key and overwrite each other's saved position — the second opens on top of the first. This does not cost theme persistence: the theme and title-bar keys are gated by their own rememberToolTheme / rememberTitlebarMode flags, so a user's Glass choice still persists. Set it also if you persist bounds yourself, for a different reason: the base restores position in _onFirstRender, which runs AFTER the subclass constructor. A subclass that restores its own bounds writes first and the base writes second, so the base silently wins and the subclass's restore appears to do nothing. Regent persists to a world setting rather than localStorage and hit exactly this.
  • Options are frozen. Use setToolTheme() / setToolTitlebarMode(); never assign this.options.toolTheme.
  • Watch the growth axis. height: 'auto' with resizable: false is the Tool default; the body scrolls and the base clamps to maxHeight: calc(100vh - 16px). If your content is a list that can get long, check it at realistic sizes.

ACTION_HANDLERS is safe for multi-instance tools as long as handlers read the instance from their third argument — see "ACTION_HANDLERS and the instance argument" below.

Title-bar modes

Tool windows support three chrome modes through the top-level toolTitlebar option:

Value Constant Behavior
"full" api.toolTitlebars.FULL Default. The full parchment title bar shows the title, direct tool actions, a Blacksmith context-menu launcher, and Close. Existing consumers remain in this mode.
"micro" api.toolTitlebars.MICRO A 14px parchment drag rail, always visible. The title and the consumer's own header actions are hidden; the menu dot and Close remain. Both sit muted until you hover the rail, which brings them to full strength, and hovering one takes that glyph alone to the interactive colour — no background plate at that size. Right-clicking anywhere on the rail opens the same context menu.
"auto" api.toolTitlebars.AUTO The bar is fully hidden at rest — collapsed to a near-invisible sensor strip along the top edge — and animates open to the same chrome as Full (title, tool actions, menu, Close) on hover or keyboard focus. The expanded bar is an overlay: it is taken out of layout flow, so the body never shifts to make room for it. It closes again when the pointer leaves.

The menu is rendered at the document level by Blacksmith's shared UIContextMenu, not inside the Application frame, so it remains usable even when the tool window is very small. It contains the consumer's getToolHeaderActions(), inherited Application V2 header controls, Minimize/Restore (when enabled), Reset Position, and Close. Active actions receive a checkmark in their menu label.

All three modes also include a Title Bar submenu offering Full / Micro / Auto-Hide, with a checkmark against the active mode. The user's selection is remembered per tool using the same stable identity as position persistence, so reopening that tool restores the chosen mode.

Tool themes

Tool windows support "light" (the default parchment presentation), "dark" (the established Blacksmith dark-window family), and "glass" (a translucent, lightly frosted floating-tool shell). The shared context menu exposes all three under Theme and remembers the user's selection independently for each tool. Glass makes the shared shell translucent while leaving consumer-owned content free to remain opaque for readability. Consumer content should inherit the exposed Tool variables rather than hard-coding a conflicting surface.

Consumers may set the initial theme with toolTheme, read app.toolTheme, or change it at runtime with:

await app.setToolTheme(api.toolThemes.GLASS);

As with title-bar mode, finalized Application V2 options are immutable; do not assign to app.options.toolTheme.

Every render also receives toolTheme ("light", "dark", or "glass"), toolThemeIsDark, and toolThemeIsGlass in its Handlebars context. For JavaScript-driven presentation, a consumer may override the post-change lifecycle callback:

async onToolThemeChanged(theme, previousTheme) {
    await super.onToolThemeChanged(theme, previousTheme);
    // Update canvas, chart, or third-party UI that cannot inherit CSS variables.
}

Blacksmith then broadcasts:

Hooks.on('blacksmith.toolWindowThemeChanged', (app, theme, previousTheme) => {
    // Observe theme changes across Tool Window consumers.
});

The callback and hook fire after the shared frame state and requested rerender have completed. They fire only for an actual runtime change, not when the saved initial theme is restored during construction. CSS-driven content normally needs neither: it should inherit --blacksmith-tool-*, or select the frame's data-tool-theme / .blacksmith-window-tool-theme-light|dark|glass state.

static DEFAULT_OPTIONS = foundry.utils.mergeObject(
    foundry.utils.mergeObject({}, super.DEFAULT_OPTIONS ?? {}),
    {
        id: 'my-module-micro-tool',
        toolTitlebar: 'micro',
        position: { width: 340, height: 'auto' },
        window: { title: 'My Tool', resizable: false, minimizable: true }
    }
);

"full" is the fallback for an omitted or unknown value. Foundry freezes finalized Application V2 options, so consumers must use setToolTitlebarMode() for runtime changes rather than assigning this.options.toolTitlebar.

The tool template accepts:

Key Type Description
appId string Application instance id.
bodyContent string (HTML) Main tool content.
toolBarLeft / toolBarRight string (HTML) Optional compact toolbar content.
showToolBar boolean Override automatic toolbar visibility.
toolFooterLeft / toolFooterRight string (HTML) Optional compact footer content.
showToolFooter boolean Override automatic footer visibility.

Subclass getToolHeaderActions() to add direct icon controls beside Foundry's native title controls. Each entry accepts { id, icon, label, active, disabled, onClick }. label may be a localization key. Blacksmith renders it as data-tooltip and an accessible label.

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

class MyCanvasTool extends ToolBase {
    static DEFAULT_OPTIONS = foundry.utils.mergeObject(
        foundry.utils.mergeObject({}, super.DEFAULT_OPTIONS ?? {}),
        {
            id: 'my-module-canvas-tool',
            position: { width: 340, height: 'auto' },
            window: { title: 'My Tool', resizable: false }
        }
    );

    getToolHeaderActions() {
        return [{
            id: 'follow',
            icon: 'fa-solid fa-crosshairs',
            label: 'Follow selection',
            active: this.followSelection,
            onClick: () => {
                this.followSelection = !this.followSelection;
                return this.render(false);
            }
        }];
    }

    async getData() {
        return {
            appId: this.id,
            toolBarLeft: '<strong>TOOLS</strong>',
            bodyContent: '<div>Persistent canvas utility content</div>'
        };
    }
}

Tool windows remember their last position per user by default. Set rememberPosition: false for transient instances, or set windowPositionKey when several instances should share one saved position. The same options also work on the standard base.

api.windowStyles exposes the stable identifiers STANDARD ("standard"), TOOL ("tool"), and FULLSCREEN ("fullscreen") for consumers that store or exchange a style choice. api.toolTitlebars exposes FULL ("full"), MICRO ("micro"), and AUTO ("auto"), while api.toolThemes exposes LIGHT ("light"), DARK ("dark"), and GLASS ("glass"). api.fullscreenLayouts exposes CENTERED, BAR, SPLIT, and FULL, and api.fullscreenFits exposes COVER, CONTAIN, and TILE. The registry remains presentation-agnostic: any style, Tool title-bar mode, Tool theme, and fullscreen layout can be registered and opened through registerWindow / openWindow.

Consumers may control mode switching and persistence with:

Option Default Purpose
allowTitlebarModeToggle true Include the Title Bar (Full/Micro/Auto-Hide) submenu in the controls menu. Set false to lock the configured mode and ignore saved user choices.
rememberTitlebarMode true Persist the user's selected mode in local storage.
toolTitlebarPreferenceKey derived Optional stable storage key. By default it is derived from windowPositionKey or the window class.
toolTheme "light" Initial shared Tool-shell theme: "light", "dark", or "glass".
allowToolThemeToggle true Include the Light/Dark/Glass chooser in the shared context menu. Set false to lock the configured theme and ignore saved user choices.
rememberToolTheme true Persist the user's selected theme in local storage.
toolThemePreferenceKey derived Optional stable theme-storage key. By default it is derived from windowPositionKey or the window class.

The public setToolTitlebarMode(mode, options?) method switches modes programmatically. It accepts { persist = true, render = true }.


One window per target (tool base)

A tool that opens against something - a token, an actor, a scene - usually wants exactly one window per target, and a repeated click to bring the existing one forward rather than build a second. BlacksmithToolWindowBaseV2 owns that registry, so a subclass does not carry it.

class LootWindow extends BlacksmithToolWindowBaseV2 {
    constructor(tokenDocument, options = {}) { ... }
}

await LootWindow.openFor(tokenDocument);        // opens, or focuses the one already open
LootWindow.isOpenFor(tokenDocument);
LootWindow.openWindowFor(tokenDocument);        // the instance, or null
LootWindow.openWindows();                       // every one this subclass has open
await LootWindow.closeFor(tokenDocument);

openFor(target, options) constructs your subclass with (target, options). Windows deregister themselves on close, so reopening builds a fresh one.

The entry is written before the render is awaited, so a second call arriving while the first is still rendering finds it instead of building a duplicate. If that render throws, the entry is removed and the error propagates -- a window that failed to open is not left registered, which would otherwise make every later openFor on that target focus an instance that never opened.

Targets are keyed by uuid by default, via a static keyFor(target) that also accepts a plain string. Override it to key on something else; returning null means "not addressable", and openFor declines rather than registering under a key nothing can look up again.

Registries are per subclass. A Loot window and a Shop window opened against the same token do not evict each other. this.registryKey reports the key an instance is registered under, or null for one built directly with new.

Fullscreen surface

Use BlacksmithFullscreenWindowBaseV2 for anything that should take over the screen: a handout, a cutscene, a reveal, a scoreboard at the end of a fight. It covers the entire viewport -- canvas, sidebar, hotbar, and any open window -- and it blocks: nothing underneath receives a pointer event while it is open. Escape closes it, and so does the close control in the upper right.

It is an Application V2 like the other two, so the registry, ACTION_HANDLERS, and scroll restoration all behave the same. What differs is that it renders frameless and unpositioned: Foundry writes no title bar, no inline geometry, and no inline z-index, and the stylesheet owns the layout. Consequently resizable, minimizable, position, and windowSizeConstraints have no effect here, and position is never persisted.

import {
    BlacksmithFullscreenWindowBaseV2,
    BLACKSMITH_FULLSCREEN_LAYOUTS
} from '/modules/coffee-pub-blacksmith/api/blacksmith-api.js';

class HandoutWindow extends BlacksmithFullscreenWindowBaseV2 {
    static DEFAULT_OPTIONS = foundry.utils.mergeObject(
        foundry.utils.mergeObject({}, super.DEFAULT_OPTIONS ?? {}),
        {
            id: 'my-module-handout',
            fullscreenLayout: BLACKSMITH_FULLSCREEN_LAYOUTS.CENTERED,
            fullscreenBackdrop: {
                image: 'modules/my-module/images/parchment.webp',
                color: 'rgba(0, 0, 0, 0.8)',
                opacity: 0.35,
                blur: 8,
                fit: 'cover'
            }
        }
    );

    async getData() {
        return {
            appId: this.id,
            headerIcon: 'fa-solid fa-scroll',
            windowTitle: 'The Letter',
            subtitle: 'Found in the reliquary',
            bodyContent: '<p>...</p>'
        };
    }
}

await new HandoutWindow().render(true);

One at a time

The registry of open fullscreen windows is shared across every subclass, unlike the tool base's per-subclass one. Rendering a fullscreen window closes whichever one was already open. Two viewport- covering surfaces stacked is not a layout: the second hides the first completely and nothing can reach it again.

Member Purpose
BlacksmithFullscreenWindowBaseV2.current The fullscreen window open on this client, or null.
BlacksmithFullscreenWindowBaseV2.closeCurrent() Close it, whatever it is.

Layouts

Set fullscreenLayout in DEFAULT_OPTIONS or at construction. The value reaches the template as data-layout on the root and is implemented entirely in CSS. An unknown value falls back to centered.

Value Constant Shape
"centered" api.fullscreenLayouts.CENTERED Default. A width-capped panel centred in the viewport, with a background, border, and shadow; the body scrolls.
"bar" api.fullscreenLayouts.BAR A full-width horizontal band, vertically centred, with no panel chrome and no clipping. Request a Roll's Cinematic mode uses this.
"split" api.fullscreenLayouts.SPLIT A panel whose body is two equal columns -- its two child elements. Collapses to one column below 900px.
"full" api.fullscreenLayouts.FULL Edge to edge, no panel chrome. You own the whole surface.

Four, and closed. Anything richer belongs inside your own bodyContent; Blacksmith is not the suite's layout engine.

Backdrop

fullscreenBackdrop describes what is behind your content. Every key is optional.

Key Type Effect
image string Background image, rendered on its own layer behind the panel. Give it an ordinary Foundry path (modules/my-module/images/thing.webp) -- the base makes it root-absolute and applies the server's route prefix. An absolute path, a data: URI, and a full URL are all passed through untouched.
color string A CSS colour washed over the whole surface, behind the image. Defaults to a dark scrim.
opacity number 0 to 1, applied to the image layer only, so an image can sit at partial strength over the colour. Content is never dimmed by it.
blur number or string Blur applied to everything showing through from behind the surface -- the canvas and windows underneath. A number is pixels.
saturate number or string Saturation applied alongside blur on that same pass. A number is a percentage. A heavy blur averages colour towards grey, so a translucent backdrop without this reads as fog; 115 is what the Tool shell's glass theme uses and is a sensible starting point.
imageBlur number or string Blur applied to the image layer itself, which turns a detailed image into texture that content stays readable over. Not the same as blur, and they compose.
fit string api.fullscreenFits.COVER (default), CONTAIN, or TILE.
position string Any background-position value.

These resolve to custom properties on the window element, so a consumer stylesheet can override any one of them with ordinary specificity.

Staged entrance and exit

A surface arrives in four stages -- the surface, then the panel it carries, then the content on the panel, then the items, staggered -- and leaves in the reverse order. Pick a preset with fullscreenAnimation (api.fullscreenAnimations):

Preset Arrival
fade A clean crossfade. The default, and the floor.
slide Items travel in from their own side, overshoot, rebound, and pop into place.
drop The same arrival, from above.
bars Letterbox bars drive in, then the content between them.
slam Items hurled in from behind the viewer, spinning, landing hard, with a kick on the stage.
random One of the above, drawn once when the window opens.

There is no none. A preset that animates nothing is a preset that does nothing; set the durations to zero if you want stillness.

random resolves to a concrete preset before anything renders and is remembered for the life of the window, so data-fs-animation always names a real preset and the exit matches the entrance that played.

The base owns the first two stages, because they are its own elements. For the other two you mark your own DOM and write no animation code:

<h2 data-fs-stage="content">Gilded Flagon</h2>
<div class="shop-filters" data-fs-stage="content">...</div>

<div class="shop-item" data-fs-stage="items">...</div>
<div class="shop-item" data-fs-stage="items">...</div>

Mark nothing and the surface and panel still animate, which is a reasonable floor.

A staged element must rest in its final, visible state. Do not give it opacity: 0 or an offset transform expecting the stage to reveal it. The stage holds the end state through animation-fill-mode, and three ordinary things drop that fill: the entrance finishing, prefers-reduced-motion, and a preset with a zero duration. Any of them leaves the element at its resting state, so a hidden resting state is content that flashes and disappears. The entrance borrows an element's appearance and hands it back.

Anything that must keep running -- an ambient loop, a pulse -- belongs on an inner element or a pseudo-element, never on the staged element itself. The base sets animation: none on every [data-fs-stage] once the entrance completes, and a stage already owns that element's transform; two animations on one property do not compose, the later simply wins.

Entrances play once. data-fs-entered is set when the sequence completes, so a re-render -- a filter change, an appended row -- does not fly the whole surface in again.

Direction: data-fs-from

slide and drop need to know which edge an item travels in from. Put data-fs-from on the item or on any ancestor, and a whole group inherits it (api.fullscreenFrom: left, right, top, bottom). Unmarked items arrive from the left and above.

<div class="challengers" data-fs-from="left">  ...items... </div>
<div class="defenders"   data-fs-from="right"> ...items... </div>

It does two jobs, and the second is not obvious:

  • Which way an item travels. One keyframe set serves every edge by multiplying the distance by a sign, so directions cannot drift apart the way four near-identical keyframe sets would.
  • How items are numbered. Items are staggered within their data-fs-from group, not across the whole surface -- so two opposing groups arrive in step rather than one after the other. Within a group they are filled away from the edge they enter through: a group entering from the left fills its rightmost slot first, so each arrival stops short of the ones already placed instead of flying over them.

The base assigns two custom properties to every staged item; never set them yourself:

Property Purpose
--fs-index Position within its data-fs-from group, for the stagger.
--fs-random A stable 0-1 seed, assigned once, for ambient effects that should not march in step. Vary a duration as well as a delay with it, or items keep a fixed relative order forever.

Retiming

Only durations are authored; every delay is a calc() of the stage before it, for both chains. Retiming one stage cascades instead of forcing you to re-sum everything downstream by hand.

Property Purpose
--fs-stage-surface-duration / -panel- / -content- / -item-duration Entrance durations.
--fs-stage-item-stagger Gap between consecutive items.
--fs-exit-item-duration / -content- / -panel- / -surface-duration, --fs-exit-item-stagger The same for the exit.
--fs-stage-*-delay, --fs-exit-*-delay Derived. Read them to stay in time with a stage; do not author them.
--fs-stage-total, --fs-exit-total Derived, and read back by the base to know how long to wait.

The two totals are registered with @property { syntax: "<time>" } in styles/window-fullscreen.css, and that registration is load-bearing. An unregistered custom property has no computed value, so getPropertyValue returns the specified token stream -- the literal text calc(calc(120ms + 300ms) ...) -- and every parse of it is NaN. Registration is what makes the browser resolve the chain before the base reads it. If you add a total, register it.

Timings are judged rather than asserted, so the harness carries a tuner: Fullscreen Window -> Open the tuner gives sliders for every dial and a Copy CSS button.

Zones

The zone names match the standard window contract -- headerIcon, windowTitle, subtitle, headerRight, toolsContent, bodyContent, actionBarLeft, actionBarRight -- so what you know from one carries to the other. Two differences:

  • There is no option bar. A filter strip has no meaning over a cutscene.
  • The defaults differ. The header is on; tools and the action bar are off until you return showTools: true or showActionBar: true. Return showHeader: false for a surface that carries its own titling, as the Cinematic band does.

Appearance

Everything the shell draws is a CSS custom property, following the Tool shell's precedent. Scope any override to your own application class, and reach for these rather than picking a colour:

Property Purpose
--blacksmith-fullscreen-z-index Stacking. Defaults above everything the viewer could otherwise see or reach through a blocking surface -- windows, the sidebar, Blacksmith's menubar, toasts, and pin previews -- and below Blacksmith's global context menu, which the surface may open for itself. Foundry's notifications sit behind it; blocking wins.
--blacksmith-fullscreen-backdrop-image-filter The image layer's filter, which imageBlur sets. Take it directly for a tint, a desaturation, or anything else filter does.
--blacksmith-fullscreen-backdrop-filter The whole surface backdrop-filter, composed from blur and saturate. Replace it outright for a brightness lift, a hue shift, or anything else.
--blacksmith-fullscreen-panel-background / -border / -radius / -shadow / -padding The panel, in the layouts that draw one.
--blacksmith-fullscreen-max-width / -max-height Panel size caps.
--blacksmith-fullscreen-gap Spacing between zones.
--blacksmith-fullscreen-text / -text-muted / -accent / -divider Content tones.
--blacksmith-fullscreen-close-size / -color / -color-hover / -background The close control.
--blacksmith-fullscreen-transition Fade duration.

These are component properties of the fullscreen shell, not design tokens. Global tokens live in styles/vars.css and carry one fixed value each; these describe one component and belong with it.

An id selector outranks the shell's class selectors, so a rule written against your window's id silently takes the shell over -- including the parts that make it cover and block. Style your content, not the cover.

Options

Option Default Purpose
fullscreenLayout "centered" The layout, from the table above.
fullscreenBackdrop {} The backdrop, from the table above.
showCloseButton true Render the close control in the upper right.
dismissOnEscape true Escape closes the surface.
dismissOnBackdrop false A click landing on the backdrop rather than the content closes the surface.
fullscreenTransitionMs 220 Fade duration in milliseconds. 0 disables the fade.

The three dismissal options above only decide which routes exist. What a dismissal does is onDismiss.

Escape is handled by the base and swallowed, rather than left to Foundry's dismiss chain. A frameless application never becomes ui.activeWindow, so that chain would not reach this window at all -- it would dismiss whatever is behind the surface, which the viewer cannot see.

onDismiss(reason) -- the viewer asked, as opposed to anything else

All three dismissal routes call onDismiss(reason), where reason is "escape", "close-button", or "backdrop". The default implementation closes the window; override it to confirm first, save, broadcast, or decline.

It is deliberately not close(). Every other route -- a timer, a socket, your own code -- reaches close() too, and the things a window wants to do on the way out are usually specific to the viewer having asked. Request a Roll's cinematic is the case in point: a GM dismissing it ends the scene for the whole table, while the same window closing because the rolls finished must not broadcast, since every client reaches that point on its own and would send the same message.

async onDismiss(reason) {
    if (this.hasUnsavedWork && !(await this.confirmDiscard())) return;  // decline
    await this.close();
}

Template data contract (core template)

When you use Blacksmith's core template (templates/window-template.hbs) and extend BlacksmithWindowBaseV2, your getData() return value can include the following. All are optional unless noted. HTML slots are rendered as HTML (use triple-brace in Handlebars if you author your own template).

Key Type Description
appId string Required. Application instance id (e.g. this.id). Used as the root element id.
showOptionBar boolean Show the option bar. Default true if omitted.
showHeader boolean Show the header. Default true if omitted.
showTools boolean Show the tools bar (below header). Default true if omitted.
showActionBar boolean Show the action bar. Default true if omitted.
optionBarLeft string (HTML) Option bar left zone (filters, toggles).
optionBarRight string (HTML) Option bar right zone.
headerIcon string Font Awesome class for the header icon (e.g. 'fa-solid fa-hammer'). If omitted, default hammer icon is used.
windowTitle string Main title in the header.
subtitle string Subtitle line below the title.
headerRight string (HTML) Header right zone (buttons, dropdowns, labels).
toolsContent string (HTML) Tools bar content (search, filters, progress bars, etc.). Single area; module controls layout.
bodyContent string (HTML) Main scrollable body content.
actionBarLeft string (HTML) Action bar left (secondary buttons, status text). Use class blacksmith-window-btn-secondary and data-action="name" for buttons that trigger ACTION_HANDLERS.
actionBarRight string (HTML) Action bar right (primary buttons). Use blacksmith-window-btn-primary for primary style. For destructive actions use blacksmith-window-btn-critical.

The base class sets showOptionBar, showHeader, showTools, and showActionBar to true when not provided, so all zones are visible by default. Return showOptionBar: false (or showHeader / showTools / showActionBar) to hide a zone.


Window options (resizable, min/max size)

When extending BlacksmithWindowBaseV2, set DEFAULT_OPTIONS (or pass options at construction) so the window frame behaves as needed:

  • window.resizable (boolean) — Whether the window can be resized by the user. Default is up to the module.
  • window.minimizable (boolean) — Whether the window can be minimized.
  • position.width / position.height — Initial size (numbers or "auto" per Foundry). Do not add minWidth/maxWidth/minHeight/maxHeight to position — Foundry's position object is not extensible.
  • windowSizeConstraints (object, optional) — Min/max size applied by the base class to the window element after render: { minWidth, minHeight, maxWidth, maxHeight } (numbers in pixels). Omit to leave unconstrained.
    • Applied as CSS custom properties (--blacksmith-window-min-width and siblings) on the application element, which also receives a blacksmith-window class — not as inline min-width / min-height. This is what lets minimising work: Foundry collapses a window with an inline max-height, and CSS resolves min-height over max-height, so an inline minimum would pin the frame open and render a title bar above an empty rectangle. In the cascade, .blacksmith-window.minimized and .blacksmith-window.minimizing zero the minima with ordinary specificity.
    • Consequently you do not need !important overrides in a consumer stylesheet to make a constrained window minimise, and you should not add the blacksmith-window class through DEFAULT_OPTIONS.classes — mergeObject overwrites arrays rather than concatenating, so classes declared on a base are dropped by any subclass that declares its own.

Example (in your window class):

static get DEFAULT_OPTIONS() {
  return foundry.utils.mergeObject(
    foundry.utils.mergeObject({}, super.DEFAULT_OPTIONS ?? {}),
    {
      position: { width: 800, height: 600 },
      window: { title: 'My Window', resizable: true, minimizable: true },
      windowSizeConstraints: { minWidth: 500, maxWidth: 1400 }
    }
  );
}

Getting Started

1. Build Your Window (Application V2)

Build an Application V2 window with HandlebarsApplicationMixin(ApplicationV2), PARTS, getData, document-level delegation, and scroll save/restore. The coffee-pub-prototype module carries a working example window to start from; include only the zones you need (option bar, header, tools, body, action bar).

2. Access the API

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

if (blacksmith?.registerWindow) {
    // Window registry is available
} else {
    Hooks.once('ready', () => {
        // Registry attaches during Blacksmith ready
    });
}
// A base class you `extends` is imported from api/blacksmith-api.js, not read from here -- see above.

3. Register Your Window

// When your module is ready (e.g. in ready hook)
blacksmith.registerWindow('my-module-window', {
    open: async (options = {}) => {          // `async` — the body awaits a dynamic import
        const { MyModuleWindow } = await import('/modules/my-module/scripts/my-window.js');
        const win = new MyModuleWindow(options);
        return win.render(true);
    },
    title: 'My Window',   // optional: default window title
    moduleId: 'my-module' // optional: for debugging
});

4. Open Your Window

From a toolbar tool, macro, or another module:

blacksmith.openWindow('my-module-window', { /* optional options */ });

5. Cleanup

There is no per-module teardown step. Foundry has no module-unload event — unloadModule is never fired by Foundry or Blacksmith — and disabling a module reloads the world, which tears everything down anyway. Your registered window types simply go away with the reload. Call unregisterWindow(id) only if you need to remove a window type at runtime. See api-hookmanager.md for the full explanation of the missing unload event.


API Reference

The following methods are the planned surface. Signatures and behavior are the contract for implementation.

Window Registration

registerWindow(windowId, descriptor)

Registers a window type with Blacksmith. Only one window per windowId; re-registering overwrites.

Parameters:

  • windowId (string): Unique identifier for the window type (e.g. 'my-module-window').
  • descriptor (Object): Descriptor object.

Returns: boolean — Success status.

Descriptor properties:

  • open (Function, required): (options?: Object) => Promise<Application | void> | Application | void. Called when the window should be opened. Typically instantiates your Application V2 class and calls render(true). May be async.
  • title (string, optional): Default window title (e.g. for Foundry's title bar).
  • moduleId (string, optional): Module id that owns this window (for debugging and cleanup).

Example:

blacksmith.registerWindow('my-module-query', {
    open: async (options) => {
        const { MyQueryWindow } = await import('/modules/my-module-id/scripts/window-query.js');
        const w = new MyQueryWindow(options);
        return w.render(true);
    },
    title: 'My Query Window',
    moduleId: 'my-module-id'
});

unregisterWindow(windowId)

Removes a window type from the registry.

Parameters:

  • windowId (string): The id passed to registerWindow.

Returns: boolean — Success status (e.g. true if a registration was removed).


Opening Windows

openWindow(windowId, options?)

Opens the window registered under windowId. The registered open function is called with options.

Parameters:

  • windowId (string): Id of a registered window type.
  • options (Object, optional): Passed through to the descriptor's open function. Use for window-specific options (e.g. initial data, size).

Returns: Promise<Application | void> | Application | void — Whatever the registered open returns (typically the rendered Application).

Example:

// From a toolbar tool
blacksmith.registerToolbarTool('my-module-query', {
    icon: 'fa-solid fa-crystal-ball',
    title: 'My Query Window',
    onClick: () => blacksmith.openWindow('my-module-query')
});

Querying (Optional)

The following may be exposed for debugging and cleanup:

  • getRegisteredWindows() — Returns a Map of registered window ids to descriptors.
  • isWindowRegistered(windowId) — Returns boolean.

Both are exposed on module.api.


Integration with Toolbar and Menubar

  • Toolbar: Register a tool with onClick: () => blacksmith.openWindow('your-window-id'). Your module registers both the toolbar tool and the window type (e.g. in ready).
  • Menubar: Same pattern: a menubar action can call openWindow('your-window-id').
  • Macros: A macro can call game.modules.get('coffee-pub-blacksmith').api.openWindow('your-window-id') so users can open your window by id without scripting your class.

Best Practices

  1. Unique window ids — Use a prefix (e.g. module id) to avoid collisions: 'my-module-settings', 'my-module-query'.
  2. No manual teardown needed — Foundry has no module-unload event, and disabling a module reloads the world; your registered window types are cleared automatically. Use unregisterWindow(id) only to remove a window type at runtime.
  3. Choose the right contract — Use the standard five-zone base for forms/editors and BlacksmithToolWindowBaseV2 for persistent canvas utilities. Tool consumers choose Full or Micro chrome without rebuilding the frame.
  4. Own your content — Your template/getData owns the standard window's content; Tool consumers provide bodyContent and optional toolbar/footer content while Blacksmith owns the shared frame.
  5. Application V2 only — Build your window with HandlebarsApplicationMixin(ApplicationV2) and the patterns in the guidance doc (delegation, scroll save/restore, unique instance id).

ACTION_HANDLERS and the instance argument

Set static ACTION_HANDLERS = { actionName: handler } on your window class and mark up controls with data-action="actionName". The base binds one click listener per instance on the window frame and invokes:

static ACTION_HANDLERS = {
    save: (event, target, win) => win.save(),
    // `this` is the instance too, so a regular function works as well:
    cancel(event, target) { return this.close(); }
};

Always use the instance argument (or this). Do not resolve the instance from a shared reference — a class static, a module-level let, or MyWindow._ref. Those are wrong whenever two instances of the class are open at once: they point at whichever rendered last, so a click in one window acts on the other.

MyWindow._ref still exists and still tracks the most recently rendered instance, but only so unmigrated consumers keep working. It is deprecated; migrate to the third argument.

Application V2: Body injection and scripts

  • Scripts in body/partials do not run. When Application V2 injects the body part (e.g. from Handlebars), it does not execute <script> tags inside the injected HTML. Any logic you put in a <script> block in a partial will never run. Do not rely on inline onclick="someFunction()" unless that function is already defined on window by a module script that runs at load (e.g. a separate .js file in your module's esmodules that assigns window.someFunction = ...). Prefer data-action with ACTION_HANDLERS so handlers are attached in JS and work regardless of when the body is injected.
  • Body controls (buttons, drop zones) — Use ACTION_HANDLERS, or bind on this.element in _onRender. Do not define activateListeners(html): Application V2 never calls it, so anything wired there silently never runs. this.element is the frame, created before parts render and retained across part re-renders, so a listener there catches late-injected content — which is why per-instance binding is sufficient and document-level delegation is not needed.
  • Legacy inline onclick — If you have many existing inline onclick handlers (e.g. a complex worksheet), you can either: (1) Migrate to data-action and document-level delegation (recommended long term), or (2) Keep inline onclick by moving the handler implementations into a module script that runs at load and assigns them to window, so the same attribute strings resolve when the body is injected. With option 2, a module script registers the globals at load and each global delegates to the app instance via a ref.

Troubleshooting

  • registerWindow / openWindow undefined — Window API not loaded yet. Wait for ready and check game.modules.get('coffee-pub-blacksmith')?.api?.registerWindow.
  • Window doesn't open — Ensure the window type is registered before calling openWindow. Check that descriptor.open returns or resolves to the Application instance if you need a reference.
  • Layout or behavior issues — Check document-level delegation, scroll save/restore, _getRoot(), and a safe merge of DEFAULT_OPTIONS.
  • Buttons or controls in the body do nothing — Application V2 may not run <script> inside injected body HTML, and Application V2 never calls activateListeners(html). Use data-action with ACTION_HANDLERS (see "Application V2: Body injection and scripts" under Best Practices) or bind on this.element in _onRender.
  • A control acts on the wrong window — two instances of the class are open and the handler is resolving the instance from a shared reference instead of its third argument. See "ACTION_HANDLERS and the instance argument".
  • Cannot assign to read only property 'toolTitlebar' — Foundry freezes finalized Application V2 options. Do not mutate this.options.toolTitlebar; call await app.setToolTitlebarMode('full' | 'micro' | 'auto').

Windows Blacksmith registers

These ship with Blacksmith and are openable by id from any module or macro.

Window Id Options
Dice Tray blacksmith-dice-tray none
Macros blacksmith-macros none
Health blacksmith-health { tokens } -- see below
Party Statistics blacksmith-stats-party none
Player Statistics blacksmith-stats-player { actorId } -- required
XP Distribution blacksmith-xp none
Status Effects blacksmith-status-effects { actor | actorUuid, descriptionEffectId, descriptionStatusId } -- see below
Compendium Search blacksmith-compendium-search { type, subtype, query } -- see below
Send Toast blacksmith-toast-send none
Start a Vote blacksmith-vote none
Pin Layers blacksmith-pin-layers none
World Calendar blacksmith-calendar none
Notes blacksmith-notes none
Note editor blacksmith-note-editor { note } -- a note page or its uuid; omitted opens a new note
Rest blacksmith-rest-window none

Each of these is a single-instance window: opening one that is already open raises it rather than creating a second. What a second open does beyond raising differs by window, and the difference is deliberate:

  • Party Statistics re-renders, because it is a read-only view and stale numbers would be misleading.
  • Player Statistics re-targets when given a different actorId, since opening it for another actor means "show me that one instead".
  • XP Distribution raises and nothing more. It is a working surface -- the GM toggles players and adversaries in and out of an award -- so recomputing would silently discard their setup.
  • Health re-targets when given tokens.

blacksmith-stats-player returns undefined if called without an actorId, rather than opening an empty frame. It also raises a toast, because the caller is another module's code and a console line alone would leave whoever clicked with nothing.

blacksmith-status-effects and its options

// An actor, by document or by uuid. One of the two is required.
blacksmith.openWindow('blacksmith-status-effects', { actor });
blacksmith.openWindow('blacksmith-status-effects', { actorUuid });

// Open with a particular row's description already showing.
blacksmith.openWindow('blacksmith-status-effects', { actorUuid, descriptionEffectId });
blacksmith.openWindow('blacksmith-status-effects', { actorUuid, descriptionStatusId });
Option Meaning
actor The Actor to show. Wins over actorUuid when both are given. Optional -- falls back to the selected token, then to game.user.character.
actorUuid Resolved to an Actor when actor is absent.
descriptionEffectId Id of an ActiveEffect already on the actor. Opens with its description shown.
descriptionStatusId Id of a configured status, which may not be applied to the actor yet. Opens with that condition's rules text shown.

The two description options are not interchangeable, and both are part of the contract. They are listed here rather than left to be inferred from call sites because a consumer reading only existing callers would find descriptionEffectId and never learn the other exists -- which is exactly what happened while this window was being handed over.

No actor is required. With none resolvable the window opens on an empty state -- the full conditions list, greyed out -- and fills in as soon as a token is selected. It follows the canvas selection thereafter, the same way blacksmith-health does, so a caller that means "whatever is selected" should pass nothing.

Reopening never creates a second window: it retargets the existing one at the new actor and description. Changing actor clears the selected description, since both description ids name something belonging to the actor being replaced.

Some windows are deliberately not in this list. Request a Roll has blacksmith.requestRoll(options), GM Notes has blacksmith.gmNotes, and JSON import has registerJsonImportKind -- all of them take enough configuration that a registry id would be the worse door. Pin configuration, bulk pin tags, and the combatant card are contextual: they exist against a specific pin or combatant and have no meaning opened cold. The CSS editor is a settings surface and stays internal.

blacksmith-compendium-search and its options

All three are optional. With none, the palette opens in whatever state the user last left it, which is what an "open the search" button should do.

// Just open it.
blacksmith.openWindow('blacksmith-compendium-search');

// Open it already looking for something specific.
blacksmith.openWindow('blacksmith-compendium-search', { type: 'spell' });
blacksmith.openWindow('blacksmith-compendium-search', { type: 'item', subtype: 'weapon' });
blacksmith.openWindow('blacksmith-compendium-search', { type: 'item', query: 'longsword' });
Option Meaning
type Any accepted type token ('item', 'Spell', 'monster', ...), normalized like every other compendium call. '__all__' selects All types. Ignored if the type has no configured source.
subtype A document subtype such as 'weapon'. Must belong to type, and is validated against it -- so pass type too. '' clears it.
query Prefills the search box. Runs as soon as the window renders, subject to the same 3-character minimum a typed query has.

A seed is one open, not a new default. It does not write the user's saved palette preferences; whatever they do next is theirs and saves as normal.

An already-open palette is re-seeded, not merely raised. A caller asking for spells means it, and raising a window still showing the last thing the user typed answers a different question. A call carrying no seed -- or one whose values are already set -- raises without re-rendering, so it cannot discard a search somebody is part-way through typing.

Invalid values are dropped, not guessed at. An unmapped type, or a subtype that does not belong to the type, is ignored with a debug line. A control holding a value its own option list does not contain renders blank, and the user then cannot tell what is being searched.

The window is deliberately not GM-only -- a player equipping their own character is the main case -- but whether players may drag results out onto a sheet is governed by the world setting Players Can Add From Compendium Search. A caller does not need to check it; the window enforces it either way.

blacksmith-health and the tokens option

Left alone, the Health window follows the canvas selection. A caller that wants the current selection shown should therefore just select the tokens and let the window follow -- no call is needed.

tokens is for the other case: showing a set without changing what the GM has selected.

// One token.
blacksmith.openWindow('blacksmith-health', { tokens: [token] });

// Several.
blacksmith.openWindow('blacksmith-health', { tokens: partyTokens });

It is always an array of Token placeables, never an Actor and never a bare token. Entries whose token has no readable hit points are dropped rather than shown as an empty row.

The call behaves the same whether the window is open or closed: closed, it opens showing those tokens; open, it raises and retargets. It returns the window.

The set is not sticky. The window keeps following selection, so the next time the user changes what is selected on the canvas, the passed set is replaced. The meaning is "show this now, then resume following me" -- correct for a click on a health bar, wrong if a caller expects a lock.

Passing { tokens } through invokeMenubarTool or invokeIntent does not work -- the Health tool's onClick ignores its context argument. Use openWindow.

Window ids another module may provide

Blacksmith looks for one window it does not register itself:

Id Effect when registered
blacksmith-status-effects The Health window shows a conditions button on each row, opening this window with { actor, actorUuid }.

The id names a capability rather than a module, the same way menubar intents do. When nobody registers it the button does not render, which is the correct behaviour for an optional integration.

Version History

  • Implemented — Window registry (api-windows.js), standard template/base (window-template.hbs, window-base.js), and Tool template/base (window-tool-template.hbs, window-tool-base.js).
  • Public API — Both base classes/getters, windowStyles, toolTitlebars, and toolThemes are exposed on module.api so consumers never import Blacksmith implementation scripts directly.
  • Tool chrome — Full and Micro title bars, native menu actions, mode switching, per-tool preference persistence, position reset, and frozen-options-safe runtime state were live-verified on the combatant card on 2026-07-28.
  • Canonical file — scripts/window-base.js. The old window-base-v2.js re-export shim has been removed; use module.api, not a file path.

For internal architecture and implementation details, see architecture-window.

Clone this wiki locally