diff --git a/less/v2/actors.less b/less/v2/actors.less
index ee9c7ef7f1..8adfd83e9b 100644
--- a/less/v2/actors.less
+++ b/less/v2/actors.less
@@ -352,6 +352,42 @@
form:is(.tab-inventory, .tab-features, .tab-spells, .tab-effects) .create-child { display: block; }
+ /* ---------------------------------- */
+ /* Warnings */
+ /* ---------------------------------- */
+
+ dialog.warnings:is(#tooltip, .locked-tooltip) { /* :is used here to override specificity of base tooltip styles */
+ position: fixed;
+ width: 300px;
+ max-width: unset;
+ max-height: unset;
+ margin: 0;
+ outline: none;
+ padding: 4px 8px;
+ font-family: var(--dnd5e-font-roboto-condensed);
+
+ li {
+ padding: 6px 8px;
+ border-bottom: 1px dotted var(--dnd5e-color-gold);
+ &:last-child { border: none; }
+
+ a:hover {
+ text-shadow: none;
+ text-decoration: underline dotted;
+ }
+
+ &.warning::before, &.error::before {
+ font-family: var(--font-awesome);
+ font-weight: bold;
+ color: var(--color-text-dark-5);
+ margin-right: 2px;
+ }
+
+ &.warning::before { content: "\f071"; }
+ &.error::before { content: "\f06a"; }
+ }
+ }
+
/* ---------------------------------- */
/* Minimized */
/* ---------------------------------- */
diff --git a/less/v2/apps.less b/less/v2/apps.less
index 7dc39de7fd..9167a23711 100644
--- a/less/v2/apps.less
+++ b/less/v2/apps.less
@@ -19,9 +19,9 @@
height: 18px;
aspect-ratio: 1;
line-height: unset;
- display: grid;
place-content: center;
+ &:not([hidden]) { display: grid; }
> i { margin: 0 }
}
diff --git a/module/applications/actor/sheet-v2-mixin.mjs b/module/applications/actor/sheet-v2-mixin.mjs
index 8e24f109ab..450103a297 100644
--- a/module/applications/actor/sheet-v2-mixin.mjs
+++ b/module/applications/actor/sheet-v2-mixin.mjs
@@ -88,14 +88,24 @@ export default function ActorSheetV2Mixin(Base) {
header.insertAdjacentElement("afterbegin", toggle);
}
+ // Document UUID link.
+ const firstButton = header.querySelector(".header-button");
const idLink = header.querySelector(".document-id-link");
if ( idLink ) {
- const firstButton = header.querySelector(".header-button");
firstButton?.insertAdjacentElement("beforebegin", idLink);
idLink.classList.add("header-button");
idLink.dataset.tooltipDirection = "DOWN";
}
+ // Preparation warnings.
+ const warnings = document.createElement("a");
+ warnings.classList.add("header-button", "preparation-warnings");
+ warnings.dataset.tooltip = "Warnings";
+ warnings.setAttribute("aria-label", game.i18n.localize("Warnings"));
+ warnings.innerHTML = '';
+ warnings.addEventListener("click", this._onOpenWarnings.bind(this));
+ firstButton?.insertAdjacentElement("beforebegin", warnings);
+
// Render tabs.
const nav = document.createElement("nav");
nav.classList.add("tabs", "tabs-right");
@@ -123,6 +133,15 @@ export default function ActorSheetV2Mixin(Base) {
/* -------------------------------------------- */
+ /** @inheritDoc */
+ async _render(force=false, options={}) {
+ await super._render(force, options);
+ const [warnings] = this.element.find(".header-button.preparation-warnings");
+ warnings?.toggleAttribute("hidden", !this.actor._preparationWarnings?.length);
+ }
+
+ /* -------------------------------------------- */
+
/** @inheritDoc */
async getData(options) {
this._concentration = this.actor.concentration; // Cache concentration so it's not called for every item.
@@ -430,6 +449,7 @@ export default function ActorSheetV2Mixin(Base) {
html.find(".sidebar-collapser").on("click", this._onToggleSidebar.bind(this));
html.find("[data-item-id][data-action]").on("click", this._onItemAction.bind(this));
html.find("[data-toggle-description]").on("click", this._onToggleDescription.bind(this));
+ html.find("dialog.warnings").on("click", this._onCloseWarnings.bind(this));
this.form.querySelectorAll(".item-tooltip").forEach(this._applyItemTooltips.bind(this));
this.form.querySelectorAll("[data-reference-tooltip]").forEach(this._applyReferenceTooltips.bind(this));
@@ -488,6 +508,18 @@ export default function ActorSheetV2Mixin(Base) {
/* -------------------------------------------- */
+ /**
+ * Handle closing the warnings dialog.
+ * @param {PointerEvent} event The triggering event.
+ * @protected
+ */
+ _onCloseWarnings(event) {
+ if ( event.target instanceof HTMLDialogElement ) event.target.close();
+ if ( event.target instanceof HTMLAnchorElement ) event.target.closest("dialog")?.close();
+ }
+
+ /* -------------------------------------------- */
+
/**
* Handle creating a new embedded child.
* @returns {ActiveEffect5e|Item5e|void}
@@ -548,6 +580,22 @@ export default function ActorSheetV2Mixin(Base) {
/* -------------------------------------------- */
+ /**
+ * Handle opening the warnings dialog.
+ * @param {PointerEvent} event The triggering event.
+ * @protected
+ */
+ _onOpenWarnings(event) {
+ event.stopImmediatePropagation();
+ const { top, left, height } = event.target.getBoundingClientRect();
+ const { clientWidth } = document.documentElement;
+ const dialog = this.form.querySelector("dialog.warnings");
+ Object.assign(dialog.style, { top: `${top + height}px`, left: `${Math.min(left - 16, clientWidth - 300)}px` });
+ dialog.showModal();
+ }
+
+ /* -------------------------------------------- */
+
/**
* Toggle editing hit points.
* @param {PointerEvent} event The triggering event.
diff --git a/module/utils.mjs b/module/utils.mjs
index 40788e594b..9111c7613b 100644
--- a/module/utils.mjs
+++ b/module/utils.mjs
@@ -327,6 +327,7 @@ export async function preloadHandlebarsTemplates() {
"systems/dnd5e/templates/actors/parts/actor-inventory.hbs",
"systems/dnd5e/templates/actors/parts/actor-spellbook.hbs",
"systems/dnd5e/templates/actors/parts/actor-warnings.hbs",
+ "systems/dnd5e/templates/actors/parts/actor-warnings-dialog.hbs",
"systems/dnd5e/templates/actors/parts/biography-textbox.hbs",
"systems/dnd5e/templates/actors/tabs/character-biography.hbs",
"systems/dnd5e/templates/actors/tabs/character-details.hbs",
diff --git a/templates/actors/character-sheet-2.hbs b/templates/actors/character-sheet-2.hbs
index c2bec0fc6c..0b7f007229 100644
--- a/templates/actors/character-sheet-2.hbs
+++ b/templates/actors/character-sheet-2.hbs
@@ -518,4 +518,8 @@
aria-label="{{ "SIDEBAR.Create" type=(localize "DOCUMENT.Item") }}">
+
+ {{!-- Warnings --}}
+ {{> "dnd5e.actor-warnings-dialog" }}
+
diff --git a/templates/actors/npc-sheet-2.hbs b/templates/actors/npc-sheet-2.hbs
index be83a18307..0feb71e050 100644
--- a/templates/actors/npc-sheet-2.hbs
+++ b/templates/actors/npc-sheet-2.hbs
@@ -527,4 +527,7 @@
+ {{!-- Warnings --}}
+ {{> "dnd5e.actor-warnings-dialog" }}
+
diff --git a/templates/actors/parts/actor-warnings-dialog.hbs b/templates/actors/parts/actor-warnings-dialog.hbs
new file mode 100644
index 0000000000..63e89e3293
--- /dev/null
+++ b/templates/actors/parts/actor-warnings-dialog.hbs
@@ -0,0 +1,11 @@
+