Repository navigation
api window
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-prototypemodule, as loadable code rather than a copy-paste block.
The Window API allows external modules to:
- Register a window type with a unique id and a descriptor (how to open the window).
-
Open that window by id via Blacksmith (
openWindow(windowId, options)), so toolbars, macros, and other modules can open your window without importing your class. - 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).
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.BlacksmithWindowBaseV2for standard windows,api.BlacksmithToolWindowBaseV2for compact tools, andapi.BlacksmithFullscreenWindowBaseV2for a surface that takes over the screen.
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, andfullscreenFitsare patched onmodule.apias soon as Blacksmith's module script has finished loading (beforeinit/ready), as long as your module loads aftercoffee-pub-blacksmithin the manifest (or depends on it). This is for code that runs aftergameexists — 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'sinit(afterawait addToolbarButton()). Prefer callingregisterWindow/openWindowfromreadyor afterawait BlacksmithAPI.waitForReady()so the rest of the stack is consistent. -
Most other
module.apimembers — The public shell (registerModule,utils,HookManager, menubar bindings, etc.) is assigned synchronously at the start of Blacksmith'sinit(before anyawaitthere). Asset-backed fields (assetLookup, mergedBLACKSMITHconstants) finish during Blacksmith'sready; useBlacksmithAPI.waitForReady()if you need that data. See documentation/architecture/architecture-blacksmith.md §3.2–3.3.
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.
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. |
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-raisedvsscrim.raisedis decorative and may be translucent.scrimguarantees 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 forraisedplusbackdrop-filter: blur(6px)rather thanscrim. 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 Darkraisedis already opaque so the blur costs nothing. Savescrimfor something that must stay readable over genuinely arbitrary content, like a full-width overlay. -
text-mutedrather thanopacity. Dimming withopacityfades a whole element including its background and borders, and compounds when nested.text-mutedis 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.
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 ownoptionpair. 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-backgroundis 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;
}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.windowPositionKeydefaults 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 ownrememberToolTheme/rememberTitlebarModeflags, 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 thanlocalStorageand hit exactly this. -
Options are frozen. Use
setToolTheme()/setToolTitlebarMode(); never assignthis.options.toolTheme. -
Watch the growth axis.
height: 'auto'withresizable: falseis the Tool default; the body scrolls and the base clamps tomaxHeight: 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.
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 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 }.
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.
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);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. |
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.
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.
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.
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-fromgroup, 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. |
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.
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: trueorshowActionBar: true. ReturnshowHeader: falsefor a surface that carries its own titling, as the Cinematic band does.
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.
| 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.
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();
}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.
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 addminWidth/maxWidth/minHeight/maxHeighttoposition— 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-widthand siblings) on the application element, which also receives ablacksmith-windowclass — not as inlinemin-width/min-height. This is what lets minimising work: Foundry collapses a window with an inlinemax-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.minimizedand.blacksmith-window.minimizingzero the minima with ordinary specificity. - Consequently you do not need
!importantoverrides in a consumer stylesheet to make a constrained window minimise, and you should not add theblacksmith-windowclass throughDEFAULT_OPTIONS.classes—mergeObjectoverwrites arrays rather than concatenating, so classes declared on a base are dropped by any subclass that declares its own.
- Applied as CSS custom properties (
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 }
}
);
}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).
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.// 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
});From a toolbar tool, macro, or another module:
blacksmith.openWindow('my-module-window', { /* optional options */ });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.
The following methods are the planned surface. Signatures and behavior are the contract for implementation.
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 callsrender(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'
});Removes a window type from the registry.
Parameters:
-
windowId(string): The id passed toregisterWindow.
Returns: boolean — Success status (e.g. true if a registration was removed).
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'sopenfunction. 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')
});The following may be exposed for debugging and cleanup:
-
getRegisteredWindows()— Returns aMapof registered window ids to descriptors. -
isWindowRegistered(windowId)— Returnsboolean.
Both are exposed on module.api.
-
Toolbar: Register a tool with
onClick: () => blacksmith.openWindow('your-window-id'). Your module registers both the toolbar tool and the window type (e.g. inready). -
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.
-
Unique window ids — Use a prefix (e.g. module id) to avoid collisions:
'my-module-settings','my-module-query'. -
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. -
Choose the right contract — Use the standard five-zone base for forms/editors and
BlacksmithToolWindowBaseV2for persistent canvas utilities. Tool consumers choose Full or Micro chrome without rebuilding the frame. -
Own your content — Your template/
getDataowns the standard window's content; Tool consumers providebodyContentand optional toolbar/footer content while Blacksmith owns the shared frame. -
Application V2 only — Build your window with
HandlebarsApplicationMixin(ApplicationV2)and the patterns in the guidance doc (delegation, scroll save/restore, unique instance id).
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.
-
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 inlineonclick="someFunction()"unless that function is already defined onwindowby a module script that runs at load (e.g. a separate.jsfile in your module'sesmodulesthat assignswindow.someFunction = ...). Preferdata-actionwithACTION_HANDLERSso handlers are attached in JS and work regardless of when the body is injected. -
Body controls (buttons, drop zones) — Use
ACTION_HANDLERS, or bind onthis.elementin_onRender. Do not defineactivateListeners(html): Application V2 never calls it, so anything wired there silently never runs.this.elementis 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
onclickhandlers (e.g. a complex worksheet), you can either: (1) Migrate todata-actionand 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 towindow, 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.
-
registerWindow/openWindowundefined — Window API not loaded yet. Wait forreadyand checkgame.modules.get('coffee-pub-blacksmith')?.api?.registerWindow. -
Window doesn't open — Ensure the window type is registered before calling
openWindow. Check thatdescriptor.openreturns 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 ofDEFAULT_OPTIONS. -
Buttons or controls in the body do nothing — Application V2 may not run
<script>inside injected body HTML, and Application V2 never callsactivateListeners(html). Usedata-actionwithACTION_HANDLERS(see "Application V2: Body injection and scripts" under Best Practices) or bind onthis.elementin_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_HANDLERSand the instance argument". -
Cannot assign to read only property 'toolTitlebar'— Foundry freezes finalized Application V2 options. Do not mutatethis.options.toolTitlebar; callawait app.setToolTitlebarMode('full' | 'micro' | 'auto').
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.
// 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.
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.
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.
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.
-
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, andtoolThemesare exposed onmodule.apiso 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 oldwindow-base-v2.jsre-export shim has been removed; usemodule.api, not a file path.
For internal architecture and implementation details, see architecture-window.
- Ai assistance
- Applicationv2 traps
- CSS traps
- dnd5e conditions
- Documentation standard
- Token actor identity
- Campaign
- Canvas
- Chatcards
- Compendiums
- Contextmenu
- Core
- Create journal entry
- Dialog
- Active Effects
- Entity list
- Equiplocations
- Geography
- GM request
- Gmnotes
- Health
- Hookmanager
- Importer
- Inventory
- Menubar
- Notes
- Party
- Pins
- Quantity split
- Requestroll
- Rolls
- Scene config
- Sockets
- Stats
- Tags
- Toast
- Tokens
- Toolbar
- Window
- Worldclock