[Feature] Add support for GM Notes (#2082)
Some checks failed
Project CI / build (24.x) (push) Has been cancelled

* Add support for GM Notes

* Localize GM Notes header label

* Fix active editor height and menu auto sizing

* Add tooltip to add gm note button
This commit is contained in:
Carlos Fernandez 2026-07-14 08:39:53 -04:00 committed by GitHub
parent 0c2d257871
commit 79d6522614
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 278 additions and 64 deletions

View file

@ -267,6 +267,10 @@ Hooks.on('i18nInit', () => {
}); });
Hooks.on('setup', () => { Hooks.on('setup', () => {
if (game.user.isGM) {
document.body.dataset.gm = true;
}
CONFIG.statusEffects = [ CONFIG.statusEffects = [
...CONFIG.statusEffects.filter(x => !['dead', 'unconscious'].includes(x.id)), ...CONFIG.statusEffects.filter(x => !['dead', 'unconscious'].includes(x.id)),
...Object.values(SYSTEM.GENERAL.conditions()).map(x => ({ ...Object.values(SYSTEM.GENERAL.conditions()).map(x => ({

View file

@ -2550,6 +2550,9 @@
}, },
"identifier": { "identifier": {
"label": "Identifier" "label": "Identifier"
},
"gmNotes": {
"label": "GM Notes"
} }
}, },
"Ancestry": { "Ancestry": {
@ -2565,6 +2568,9 @@
"severe": "Severe Threshold" "severe": "Severe Threshold"
} }
}, },
"Base": {
"addGMNote": "Add GM Note"
},
"Beastform": { "Beastform": {
"FIELDS": { "FIELDS": {
"beastformType": { "label": "Beastform Type" }, "beastformType": { "label": "Beastform Type" },

View file

@ -30,7 +30,8 @@ export default class DHBaseItemSheet extends DHApplicationMixin(ItemSheetV2) {
addFeature: DHBaseItemSheet.#addFeature, addFeature: DHBaseItemSheet.#addFeature,
deleteFeature: DHBaseItemSheet.#deleteFeature, deleteFeature: DHBaseItemSheet.#deleteFeature,
addResource: DHBaseItemSheet.#addResource, addResource: DHBaseItemSheet.#addResource,
removeResource: DHBaseItemSheet.#removeResource removeResource: DHBaseItemSheet.#removeResource,
editGMNote: DHBaseItemSheet.#onEditGMNote
}, },
dragDrop: [ dragDrop: [
{ dragSelector: null, dropSelector: '.drop-section' }, { dragSelector: null, dropSelector: '.drop-section' },
@ -76,10 +77,16 @@ export default class DHBaseItemSheet extends DHApplicationMixin(ItemSheetV2) {
/**@inheritdoc */ /**@inheritdoc */
async _preparePartContext(partId, context, options) { async _preparePartContext(partId, context, options) {
await super._preparePartContext(partId, context, options); await super._preparePartContext(partId, context, options);
const TextEditor = foundry.applications.ux.TextEditor.implementation;
switch (partId) { switch (partId) {
case 'description': case 'description':
context.enrichedDescription = await this.document.system.getEnrichedDescription(); context.enrichedDescription = await this.document.system.getEnrichedDescription({ gmNotes: false });
context.enrichedGMNotes = await TextEditor.implementation.enrichHTML(this.item.system.gmNotes, {
relativeTo: this.item,
rollData: this.item.getRollData(),
secrets: this.item.isOwner
})
break; break;
case 'effects': case 'effects':
await this._prepareEffectsContext(context, options); await this._prepareEffectsContext(context, options);
@ -331,4 +338,45 @@ export default class DHBaseItemSheet extends DHApplicationMixin(ItemSheetV2) {
} }
} }
} }
/**
* Handles the Add GM Note button being pressed. This is only used when an item has no GM notes.
* Later edits to a GM note instead go through the normal editor toggle workflow.
* @this DHBaseItemSheet
*/
static #onEditGMNote() {
// Open the editor, which might be hidden. We remove the css class to hide temporarily
// so that menu auto resizing functions properly.
const editor = this.element.querySelector('prose-mirror[name="system.gmNotes"]');
const wasHidden = editor.classList.contains('hide-if-inactive');
editor.classList.remove('hide-if-inactive');
editor.open = true;
window.setTimeout(() => {
if (wasHidden) editor.classList.add('hide-if-inactive');
}, 0);
}
/** @inheritdoc */
async _onRender(context, options) {
await super._onRender(context, options);
// Render an add gmnotes button if there are no set GM notes.
// We need to re-render on close since its possible to prosemirror to close *without* triggering a full re-render
if (game.user.isGM && !this.item.system.gmNotes) {
const description = this.element.querySelector('[name="system.description"]');
const addButton = () => {
if (description.querySelector('[data-action=editGMNote]')) return;
const button = document.createElement('button');
button.type = 'button';
button.classList.add('icon', 'toggle', 'fa-regular', 'fa-note-medical');
button.dataset.action = 'editGMNote';
button.dataset.tooltip = 'DAGGERHEART.ITEMS.Base.addGMNote';
description.appendChild(button);
}
addButton();
description.addEventListener('close', () => addButton());
}
}
} }

View file

@ -49,7 +49,10 @@ export default class BaseDataItem extends foundry.abstract.TypeDataModel {
}) })
}; };
if (this.metadata.hasDescription) schema.description = new fields.HTMLField({ required: true, nullable: true }); if (this.metadata.hasDescription) {
schema.description = new fields.HTMLField({ required: true, nullable: true });
schema.gmNotes = new fields.HTMLField({ required: true, nullable: true });
}
if (this.metadata.hasResource) { if (this.metadata.hasResource) {
schema.resource = new fields.SchemaField( schema.resource = new fields.SchemaField(
@ -134,7 +137,7 @@ export default class BaseDataItem extends foundry.abstract.TypeDataModel {
/** /**
* Augments the description for the item with type specific info to display. Implemented in applicable item subtypes. * Augments the description for the item with type specific info to display. Implemented in applicable item subtypes.
* @param {object} [options] - Options that modify the styling of the rendered template. { headerStyle: undefined|'none'|'large' } * @param {object} [options] - Options that modify the styling of the rendered template. { headerStyle: undefined|'none'|'large' }
* @returns {string} * @returns {Promise<{ prefix: string | null; value: string | null; suffix: string | null }>}
*/ */
async getDescriptionData(_options) { async getDescriptionData(_options) {
return { prefix: null, value: this.description, suffix: null }; return { prefix: null, value: this.description, suffix: null };
@ -145,14 +148,24 @@ export default class BaseDataItem extends foundry.abstract.TypeDataModel {
* @param {object} [options] - Options that modify the styling of the rendered template. { headerStyle: undefined|'none'|'large' } * @param {object} [options] - Options that modify the styling of the rendered template. { headerStyle: undefined|'none'|'large' }
* @returns {Promise<string>} * @returns {Promise<string>}
*/ */
async getEnrichedDescription() { async getEnrichedDescription({ gmNotes = true } = {}) {
if (!this.metadata.hasDescription) return ''; if (!this.metadata.hasDescription) return '';
const { prefix, value, suffix } = await this.getDescriptionData(); const { prefix, value, suffix } = await this.getDescriptionData();
const fullDescription = [prefix, value, suffix].filter(p => !!p).join('\n<hr>\n'); let fullDescription = [prefix, value, suffix].filter(p => !!p).join('\n<hr>\n');
if (this.gmNotes && gmNotes) {
const gmNotesElement = document.createElement('section');
gmNotesElement.classList.add('gm-notes-section');
gmNotesElement.dataset.visibility = 'gm';
const header = document.createElement('header');
header.classList.add('gm-notes');
header.textContent = _loc('DAGGERHEART.ITEMS.FIELDS.gmNotes.label');
gmNotesElement.innerHTML = header.outerHTML + this.gmNotes;
fullDescription += gmNotesElement.outerHTML;
}
return await foundry.applications.ux.TextEditor.implementation.enrichHTML(fullDescription, { return await foundry.applications.ux.TextEditor.implementation.enrichHTML(fullDescription, {
relativeTo: this, relativeTo: this.parent,
rollData: this.getRollData(), rollData: this.getRollData(),
secrets: this.parent.isOwner secrets: this.parent.isOwner
}); });

View file

@ -595,7 +595,37 @@
margin-top: 4px; margin-top: 4px;
color: light-dark(#14142599, #efe6d850); color: light-dark(#14142599, #efe6d850);
font-size: var(--font-size-12); font-size: var(--font-size-12);
padding-left: 3px; padding-left: 16px;
}
section.gm-notes-section {
padding-bottom: var(--spacer-4);
header.gm-notes + p {
margin-top: 0;
}
}
header.gm-notes {
position: relative;
display: flex;
gap: 6px;
align-items: center;
&::before,
&::after {
content: " ";
flex: 1;
border-bottom: 1px solid var(--color-dark-6);
}
&::before {
mask-image: linear-gradient(270deg, black 0%, black calc(100% - 10px), transparent 100%);
}
&::after {
mask-image: linear-gradient(270deg, transparent 0%, black 10px, black 100%);
}
margin-top: var(--spacer-8);
margin-bottom: var(--spacer-4);
font-size: var(--font-size-11);
text-transform: uppercase;
} }
secret-block { secret-block {
@ -866,4 +896,8 @@
right: 2px; right: 2px;
} }
} }
.gm-notes {
font-style: italic;
}
} }

View file

@ -3,7 +3,7 @@
.sheet.daggerheart.dh-style.item { .sheet.daggerheart.dh-style.item {
.tab.features { .tab.features {
padding: 0 10px; padding: 7px 10px;
overflow-y: auto; overflow-y: auto;
.feature-list { .feature-list {
display: flex; display: flex;

View file

@ -111,3 +111,7 @@ body.theme-light,
.themed.theme-light { .themed.theme-light {
color-scheme: light; color-scheme: light;
} }
body:not([data-gm=true]) [data-visibility="gm"] {
display: none;
}

View file

@ -12,12 +12,14 @@
}); });
.application.sheet.daggerheart.dh-style { .application.sheet.daggerheart.dh-style {
--portrait-size: 150px;
.item-sheet-header { .item-sheet-header {
display: flex; display: flex;
.profile { .profile {
height: 150px; height: var(--portrait-size);
width: 150px; width: var(--portrait-size);
object-fit: cover; object-fit: cover;
border-right: 1px solid light-dark(@dark-blue, @golden); border-right: 1px solid light-dark(@dark-blue, @golden);
border-bottom: 1px solid light-dark(@dark-blue, @golden); border-bottom: 1px solid light-dark(@dark-blue, @golden);
@ -34,19 +36,24 @@
text-align: center; text-align: center;
width: 80%; width: 80%;
.item-name input[type='text'] { .item-name {
font-size: var(--font-size-32); display: flex;
height: 42px; flex-direction: column;
text-align: center; margin: 10px 10px 0 10px;
width: 90%; input[type='text'] {
transition: all 0.3s ease; font-size: var(--font-size-30);
outline: 2px solid transparent; text-align: center;
border: 1px solid transparent; width: 100%;
transition: all 0.3s ease;
outline: 2px solid transparent;
border: 1px solid transparent;
text-overflow: ellipsis;
&:hover[type='text'], &:hover[type='text'],
&:focus[type='text'] { &:focus[type='text'] {
box-shadow: none; box-shadow: none;
outline: 2px solid light-dark(@dark-blue, @golden); outline: 2px solid light-dark(@dark-blue, @golden);
}
} }
} }

View file

@ -6,11 +6,80 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
flex: 1; flex: 1;
overflow-y: hidden !important; overflow: hidden;
padding-top: 10px; padding: 0;
margin: 0;
prose-mirror.active + .artist-attribution { .description-section {
display: none; flex: 1;
display: flex;
flex-direction: column;
overflow: auto;
padding: 12px 16px 4px 16px;
.with-scroll-shadows();
prose-mirror {
button.toggle {
top: 0px;
right: 0;
}
button[data-action=editGMNote] {
right: calc(var(--button-size) + 4px);
}
&.inactive {
height: unset!important;
overflow: unset;
.editor-content {
position: relative;
overflow: unset;
// Allows content links to peek out
margin-top: -4px;
padding: 4px 0 0 0;
}
}
&.active {
--min-height: 250px;
padding: 8px 0 0 16px;
button[data-action=editGMNote] {
display: none;
}
.editor-content {
padding-right: 16px;
padding-bottom: 4px;
}
}
}
/** Hide editors that are empty when inactive if we need them to be */
prose-mirror.inactive.hide-if-inactive {
display: none;
}
&:has(prose-mirror.active) {
padding: 0;
}
/** Description should fill available room (with overriden exceptions) */
prose-mirror[name="system.description"] {
flex: 1 0;
}
&:has(prose-mirror[name="system.gmNotes"]:not(.hide-if-inactive)) {
prose-mirror.inactive {
--min-height: 3rem;
&[name="system.description"] {
flex: 0 0;
}
&[name="system.gmNotes"] {
flex: 1 0;
}
}
}
}
/** Hide other elements if an editor is open */
&:has(prose-mirror.active) {
prose-mirror.inactive,
header.gm-notes,
.artist-attribution {
display: none;
}
} }
} }
} }

View file

@ -93,10 +93,6 @@
padding: 8px 0 0 16px; padding: 8px 0 0 16px;
} }
} }
.artist-attribution {
padding-left: 16px;
}
} }
.search-section { .search-section {

View file

@ -1,4 +1,6 @@
.application.sheet.daggerheart.dh-style.beastform { .application.sheet.daggerheart.dh-style.beastform {
--portrait-size: 130px;
.settings.tab { .settings.tab {
.advantage-on-section { .advantage-on-section {
display: flex; display: flex;
@ -9,4 +11,11 @@
font-style: italic; font-style: italic;
} }
} }
.tab.features.active {
display: flex;
flex-direction: column;
gap: 10px;
padding: 8px calc(12px - var(--scrollbar-width)) 4px 12px;
.stable-scroll-container();
}
} }

View file

@ -2,17 +2,9 @@
@import '../../utils/fonts.less'; @import '../../utils/fonts.less';
.application.sheet.daggerheart.dh-style.feature { .application.sheet.daggerheart.dh-style.feature {
.item-sheet-header { --portrait-size: 130px;
display: flex;
.profile {
height: 130px;
width: 130px;
}
}
section.tab { section.tab {
height: 400px;
overflow-y: auto; overflow-y: auto;
} }
} }

View file

@ -1,6 +1,6 @@
@import './item-sheet-shared.less';
@import './beastform.less'; @import './beastform.less';
@import './class.less'; @import './class.less';
@import './domain-card.less'; @import './domain-card.less';
@import './feature.less'; @import './feature.less';
@import './heritage.less'; @import './heritage.less';
@import './item-sheet-shared.less';

View file

@ -1,4 +1,4 @@
.application.sheet.daggerheart.dh-style.item { .item.daggerheart.dh-style:where(.application.sheet) {
&.minimized { &.minimized {
.attribution-header-label { .attribution-header-label {
display: none; display: none;
@ -14,4 +14,22 @@
button.plain.inline-control { button.plain.inline-control {
flex: 0 0 auto; flex: 0 0 auto;
} }
.tab-navigation {
margin-bottom: 0;
}
/** Default tab stylings */
.tab.active {
padding-top: 8px;
.with-scroll-shadows();
&.effects {
display: flex;
flex-direction: column;
gap: 10px;
padding: 8px calc(12px - var(--scrollbar-width)) 4px 12px;
.stable-scroll-container();
}
}
} }

View file

@ -226,7 +226,7 @@
ul, ul,
ol { ol {
margin: 1rem 0; margin: 0.5rem 0;
padding: 0 0 0 1.25rem; padding: 0 0 0 1.25rem;
li { li {

View file

@ -256,34 +256,34 @@
}, },
"Item": { "Item": {
"ancestry": { "ancestry": {
"htmlFields": ["description"] "htmlFields": ["description", "gmNotes"]
}, },
"community": { "community": {
"htmlFields": ["description"] "htmlFields": ["description", "gmNotes"]
}, },
"class": { "class": {
"htmlFields": ["description"] "htmlFields": ["description", "gmNotes"]
}, },
"subclass": { "subclass": {
"htmlFields": ["description"] "htmlFields": ["description", "gmNotes"]
}, },
"feature": { "feature": {
"htmlFields": ["description"] "htmlFields": ["description", "gmNotes"]
}, },
"domainCard": { "domainCard": {
"htmlFields": ["description"] "htmlFields": ["description", "gmNotes"]
}, },
"loot": { "loot": {
"htmlFields": ["description"] "htmlFields": ["description", "gmNotes"]
}, },
"consumable": { "consumable": {
"htmlFields": ["description"] "htmlFields": ["description", "gmNotes"]
}, },
"weapon": { "weapon": {
"htmlFields": ["description"] "htmlFields": ["description", "gmNotes"]
}, },
"armor": { "armor": {
"htmlFields": ["description"] "htmlFields": ["description", "gmNotes"]
}, },
"beastform": {} "beastform": {}
}, },

View file

@ -1,11 +1,25 @@
<section <section
class='tab {{tabs.description.cssClass}} {{tabs.description.id}}' class='tab {{tabs.description.cssClass}} {{tabs.description.id}}'
data-tab='{{tabs.description.id}}' data-tab='{{tabs.description.id}}'
data-group='{{tabs.description.group}}' data-group='{{tabs.description.group}}'
> >
{{formInput systemFields.description value=document.system.description enriched=enrichedDescription toggled=true}} <div class="description-section">
{{formInput systemFields.description value=document.system.description enriched=enrichedDescription toggled=true}}
{{#if (and showAttribution document.system.attribution.artist)}} {{#if (and systemFields.gmNotes @root.user.isGM)}}
<label class="artist-attribution">{{localize "DAGGERHEART.GENERAL.artistAttribution" artist=document.system.attribution.artist}}</label> <section class="gm-notes-section">
{{/if}} {{#if enrichedGMNotes}}
<header class="gm-notes">{{localize "DAGGERHEART.ITEMS.FIELDS.gmNotes.label"}}</header>
{{/if}}
<prose-mirror
name="system.gmNotes"
{{#unless enrichedGMNotes}}class="hide-if-inactive"{{/unless}}
toggled="true"
value="{{document.system.gmNotes}}"
>{{{enrichedGMNotes}}}</prose-mirror>
</section>
{{/if}}
</div>
{{#if (and showAttribution document.system.attribution.artist)}}
<label class="artist-attribution">{{localize "DAGGERHEART.GENERAL.artistAttribution" artist=document.system.attribution.artist}}</label>
{{/if}}
</section> </section>