Add support for GM Notes

This commit is contained in:
Carlos Fernandez 2026-07-12 20:49:17 -04:00
parent 3faf588e6c
commit 9d277ea8ea
16 changed files with 256 additions and 64 deletions

View file

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

View file

@ -30,7 +30,8 @@ export default class DHBaseItemSheet extends DHApplicationMixin(ItemSheetV2) {
addFeature: DHBaseItemSheet.#addFeature,
deleteFeature: DHBaseItemSheet.#deleteFeature,
addResource: DHBaseItemSheet.#addResource,
removeResource: DHBaseItemSheet.#removeResource
removeResource: DHBaseItemSheet.#removeResource,
editGMNote: DHBaseItemSheet.#onEditGMNote
},
dragDrop: [
{ dragSelector: null, dropSelector: '.drop-section' },
@ -76,10 +77,16 @@ export default class DHBaseItemSheet extends DHApplicationMixin(ItemSheetV2) {
/**@inheritdoc */
async _preparePartContext(partId, context, options) {
await super._preparePartContext(partId, context, options);
const TextEditor = foundry.applications.ux.TextEditor.implementation;
switch (partId) {
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;
case 'effects':
await this._prepareEffectsContext(context, options);
@ -331,4 +338,32 @@ export default class DHBaseItemSheet extends DHApplicationMixin(ItemSheetV2) {
}
}
}
static #onEditGMNote() {
const editor = this.element.querySelector('prose-mirror[name="system.gmNotes"]');
editor.open = true;
}
/** @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';
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) {
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.
* @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) {
return { prefix: null, value: this.description, suffix: null };
@ -145,14 +148,21 @@ export default class BaseDataItem extends foundry.abstract.TypeDataModel {
* @param {object} [options] - Options that modify the styling of the rendered template. { headerStyle: undefined|'none'|'large' }
* @returns {Promise<string>}
*/
async getEnrichedDescription() {
async getEnrichedDescription({ gmNotes = true } = {}) {
if (!this.metadata.hasDescription) return '';
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';
gmNotesElement.innerHTML = '<header class="gm-notes">GM Notes</header>' + this.gmNotes;
fullDescription += gmNotesElement.outerHTML;
}
return await foundry.applications.ux.TextEditor.implementation.enrichHTML(fullDescription, {
relativeTo: this,
relativeTo: this.parent,
rollData: this.getRollData(),
secrets: this.parent.isOwner
});

View file

@ -595,7 +595,37 @@
margin-top: 4px;
color: light-dark(#14142599, #efe6d850);
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;
}
}
@ -817,4 +847,8 @@
right: 2px;
}
}
.gm-notes {
font-style: italic;
}
}

View file

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

View file

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

View file

@ -6,11 +6,80 @@
display: flex;
flex-direction: column;
flex: 1;
overflow-y: hidden !important;
padding-top: 10px;
overflow: hidden;
padding: 0;
margin: 0;
prose-mirror.active + .artist-attribution {
display: none;
.description-section {
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 {
--min-height: 3rem;
&.inactive[name="system.description"] {
flex: 0 0;
}
&.inactive[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;
}
}
.artist-attribution {
padding-left: 16px;
}
}
.search-section {

View file

@ -1,4 +1,6 @@
.application.sheet.daggerheart.dh-style.beastform {
--portrait-size: 130px;
.settings.tab {
.advantage-on-section {
display: flex;
@ -9,4 +11,11 @@
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';
.application.sheet.daggerheart.dh-style.feature {
.item-sheet-header {
display: flex;
.profile {
height: 130px;
width: 130px;
}
}
--portrait-size: 130px;
section.tab {
height: 400px;
overflow-y: auto;
}
}

View file

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

View file

@ -1,4 +1,4 @@
.application.sheet.daggerheart.dh-style.item {
.item.daggerheart.dh-style:where(.application.sheet) {
&.minimized {
.attribution-header-label {
display: none;
@ -14,4 +14,22 @@
button.plain.inline-control {
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,
ol {
margin: 1rem 0;
margin: 0.5rem 0;
padding: 0 0 0 1.25rem;
li {

View file

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

View file

@ -1,11 +1,25 @@
<section
class='tab {{tabs.description.cssClass}} {{tabs.description.id}}'
data-tab='{{tabs.description.id}}'
data-group='{{tabs.description.group}}'
>
{{formInput systemFields.description value=document.system.description enriched=enrichedDescription toggled=true}}
{{#if (and showAttribution document.system.attribution.artist)}}
<label class="artist-attribution">{{localize "DAGGERHEART.GENERAL.artistAttribution" artist=document.system.attribution.artist}}</label>
{{/if}}
<section
class='tab {{tabs.description.cssClass}} {{tabs.description.id}}'
data-tab='{{tabs.description.id}}'
data-group='{{tabs.description.group}}'
>
<div class="description-section">
{{formInput systemFields.description value=document.system.description enriched=enrichedDescription toggled=true}}
{{#if (and systemFields.gmNotes @root.user.isGM)}}
<section class="gm-notes-section">
{{#if enrichedGMNotes}}
<header class="gm-notes">GM Notes</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>