From 65b1d5520f7cae5b79668acfd79d39ff4665c058 Mon Sep 17 00:00:00 2001 From: Kim Mantas Date: Fri, 2 Aug 2024 19:01:58 +0100 Subject: [PATCH] Initial refactor for V2 sheet code and CSS sharing. (#3924) --- .editorconfig | 3 + dnd5e.mjs | 1 - less/dnd5e.less | 3 + less/elements.less | 3 +- less/v2/actors.less | 106 ------- less/v2/apps.less | 214 +++---------- less/v2/character.less | 13 +- less/v2/dark/actors.less | 2 - less/v2/dark/apps.less | 2 + less/v2/dark/compendium-browser.less | 1 - less/v2/dark/forms.less | 14 + less/v2/dark/npc.less | 2 + less/v2/forms.less | 293 ++++++++++++++++++ less/v2/inventory.less | 6 +- less/v2/journal.less | 260 +++++++++------- less/v2/npc.less | 5 +- less/v2/sheets.less | 115 +++++++ less/v2/tooltips.less | 10 - module/applications/_module.mjs | 1 + module/applications/actor/base-sheet.mjs | 7 +- module/applications/actor/npc-sheet-2.mjs | 28 +- module/applications/actor/sheet-v2-mixin.mjs | 162 +--------- .../components/adopted-stylesheet-mixin.mjs | 5 +- module/applications/components/checkbox.mjs | 13 +- module/applications/components/inventory.mjs | 4 +- .../components/item-list-controls.mjs | 4 +- module/applications/mixins/_module.mjs | 1 + module/applications/mixins/sheet-v2-mixin.mjs | 254 +++++++++++++++ module/data/abstract.mjs | 10 +- module/data/item/spell.mjs | 2 +- .../data/item/templates/activated-effect.mjs | 4 + module/documents/item.mjs | 4 +- module/utils.mjs | 3 +- templates/actors/character-sheet-2.hbs | 2 +- templates/actors/npc-sheet-2.hbs | 2 +- templates/actors/tabs/creature-features.hbs | 2 +- templates/actors/tabs/creature-spells.hbs | 2 +- .../compendium/browser-sidebar-filters.hbs | 6 +- templates/items/parts/item-tooltip.hbs | 43 +-- templates/items/parts/spell-block.hbs | 32 ++ templates/shared/active-effects2.hbs | 2 + templates/shared/inventory2.hbs | 88 +++--- ui/official/ampersand-gold.svg | 2 + 43 files changed, 1017 insertions(+), 719 deletions(-) create mode 100644 less/v2/dark/forms.less create mode 100644 less/v2/forms.less create mode 100644 less/v2/sheets.less create mode 100644 module/applications/mixins/_module.mjs create mode 100644 module/applications/mixins/sheet-v2-mixin.mjs create mode 100644 templates/items/parts/spell-block.hbs create mode 100644 ui/official/ampersand-gold.svg diff --git a/.editorconfig b/.editorconfig index 3e83457e53..ae897d8a8b 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,9 +1,12 @@ root = true [*] +charset = utf-8 +end_of_line = lf indent_style = space indent_size = 2 insert_final_newline = true +trim_trailing_whitespace = true [*.hbs] indent_size = 4 diff --git a/dnd5e.mjs b/dnd5e.mjs index 35f5d6b563..c4128cb215 100644 --- a/dnd5e.mjs +++ b/dnd5e.mjs @@ -122,7 +122,6 @@ Hooks.once("init", function() { }); Actors.registerSheet("dnd5e", applications.actor.ActorSheet5eNPC, { types: ["npc"], - makeDefault: true, label: "DND5E.SheetClassNPCLegacy" }); DocumentSheetConfig.registerSheet(Actor, "dnd5e", applications.actor.ActorSheet5eNPC2, { diff --git a/less/dnd5e.less b/less/dnd5e.less index 480d74ebcc..254a11779d 100644 --- a/less/dnd5e.less +++ b/less/dnd5e.less @@ -18,6 +18,8 @@ /* V2 Styles */ @import "v2/apps.less"; +@import "v2/forms.less"; +@import "v2/sheets.less"; @import "v2/activities.less"; @import "v2/actors.less"; @import "v2/character.less"; @@ -30,6 +32,7 @@ /* Dark Mode */ @import "v2/dark/apps.less"; +@import "v2/dark/forms.less"; @import "v2/dark/actors.less"; @import "v2/dark/character.less"; @import "v2/dark/npc.less"; diff --git a/less/elements.less b/less/elements.less index 794454ac04..2a5b5454f8 100644 --- a/less/elements.less +++ b/less/elements.less @@ -128,9 +128,10 @@ item-list-controls search, .dnd5e2 .filter-element search { height: unset; line-height: 1; font-family: var(--dnd5e-font-roboto); + outline: none; &:hover, &:focus-visible { - outline: 0; + outline: none; box-shadow: 0 0 12px var(--dnd5e-color-gold); } } diff --git a/less/v2/actors.less b/less/v2/actors.less index 8adfd83e9b..619db73df5 100644 --- a/less/v2/actors.less +++ b/less/v2/actors.less @@ -14,39 +14,6 @@ border-radius: 5px; } - > header { - background: transparent; - position: relative; - z-index: 1; - - .window-title { visibility: hidden; } - - /* Edit Mode Toggle */ - > slide-toggle { - --slide-toggle-track-color-checked: var(--dnd5e-color-gold); - --slide-toggle-track-color-unchecked: var(--dnd5e-color-light-gray); - flex: none; - - .slide-toggle-thumb { - line-height: 12px; - - &::before { - content: "\f0ad"; - font-family: var(--font-awesome); - color: var(--dnd5e-color-light-gray); - font-weight: bold; - font-size: var(--font-size-9); - } - } - - &[checked] .slide-toggle-thumb::before { - color: var(--dnd5e-color-gold); - } - } - } - - .window-content { margin-top: -30px; } - /* ---------------------------------- */ /* Navigation */ /* ---------------------------------- */ @@ -60,17 +27,6 @@ /* ---------------------------------- */ .sheet-header { - input, select { - background: rgb(0 0 0 / 45%); - border: 1px solid transparent; - transition: all 250ms ease; - - &:hover, &:focus { - border: 1px solid var(--dnd5e-color-gold); - box-shadow: 0 0 6px var(--dnd5e-color-gold); - } - } - /* Resting & Special Traits */ .sheet-header-buttons { display: flex; @@ -90,39 +46,6 @@ } } - /* ---------------------------------- */ - /* Badges */ - /* ---------------------------------- */ - - .badge { - background: transparent no-repeat center / contain; - font-family: var(--dnd5e-font-roboto); - font-weight: bold; - color: var(--color-text-light-0); - display: grid; - place-content: center; - font-size: var(--font-size-30); - - .config-button { - color: inherit; - font-size: inherit; - } - } - - /* AC */ - .ac-badge { - width: 68px; - height: 68px; - text-align: center; - line-height: 62px; - background-image: url("ui/ac-badge.webp"); - display: block; - place-content: unset; - font-size: var(--font-size-23); - margin-top: -21px; - filter: drop-shadow(0 0 6px var(--color-shadow-dark)); - } - /* ---------------------------------- */ /* Meters */ /* ---------------------------------- */ @@ -323,35 +246,6 @@ } } - .prosemirror menu { - .pm-dropdown { - background: none; - &:hover { background: var(--color-hover-bg); } - } - - button { - display: unset; - text-transform: unset; - box-shadow: none; - font-family: var(--font-primary); - font-size: var(--font-size-14); - font-weight: normal; - line-height: 28px; - } - } - - /* ---------------------------------- */ - /* Child Creation */ - /* ---------------------------------- */ - - .create-child { - display: none; - position: absolute; - inset: auto 16px 16px auto; - } - - form:is(.tab-inventory, .tab-features, .tab-spells, .tab-effects) .create-child { display: block; } - /* ---------------------------------- */ /* Warnings */ /* ---------------------------------- */ diff --git a/less/v2/apps.less b/less/v2/apps.less index 9167a23711..165d6b0cb2 100644 --- a/less/v2/apps.less +++ b/less/v2/apps.less @@ -442,6 +442,41 @@ font-size: var(--font-size-11); } + hr.ampersand { + position: relative; + height: 20px; + border: none; + background: url("ui/official/ampersand-gold.svg") no-repeat center / contain; + margin: 0; + + &::before, &::after { + content: ""; + position: absolute; + inset-block: 9.5px; + inset-inline: 0; + } + + &::before { + background: linear-gradient(to left, var(--dnd5e-color-gold), transparent); + inset-inline-end: calc(50% + 14px); + } + + &::after { + background: linear-gradient(to right, var(--dnd5e-color-gold), transparent); + inset-inline-start: calc(50% + 14px); + } + } + + .spell-block { + text-align: left; + font-size: var(--font-size-12); + + .materials, .condition { + font-size: var(--font-size-11); + color: var(--color-text-dark-5); + } + } + /* ---------------------------------- */ /* Meters */ /* ---------------------------------- */ @@ -546,185 +581,6 @@ } } - /* ---------------------------------- */ - /* Form Elements */ - /* ---------------------------------- */ - - /* Reset button & input styles */ - input.uninput, button.unbutton { - --border: none; - border: var(--border); - box-shadow: none; - - &:hover, &:focus { - box-shadow: none; - border: var(--border); - } - } - - input.uninput { - color: inherit; - font-weight: inherit; - height: unset; - } - - button.unbutton { - background: none; - margin: 0; - border-radius: 0; - line-height: normal; - - > i { margin: 0; } - } - - /* Inputs & Buttons */ - input, button { - transition: all 250ms ease; - } - - button { - background: var(--dnd5e-background-card); - font-size: var(--font-size-13); - text-transform: uppercase; - padding: 3px; - border: 1px solid var(--color-border-light-2); - box-shadow: 0 0 8px var(--dnd5e-shadow-15); - display: flex; - align-items: center; - justify-content: center; - gap: .25rem; - - &:not(.fas, .far, .fa-solid, .fa-regular, .fa-light, .fa-duotone, .fa-thin) { - font-family: var(--dnd5e-font-roboto); - font-weight: bold; - } - &:disabled { - cursor: default; - color: var(--color-text-dark-inactive); - } - &:hover:not(:disabled), &:focus { box-shadow: 0 0 5px var(--color-shadow-primary); } - } - - button.radio-button { - width: 13px; - height: 13px; - border-radius: 100%; - background: none; - box-shadow: inset 0 0 6px var(--dnd5e-shadow-45); - padding: 3px; - display: grid; - border: none; - justify-content: unset; - - &:hover { filter: drop-shadow(0 0 6px var(--dnd5e-color-gold)); } - - &[aria-pressed="true"]::before { - content: ""; - width: 100%; - height: 100%; - display: inline-block; - border-radius: 100%; - background: var(--color-border-dark-5); - } - } - - /* Reset select styles */ - select { - color: inherit; - - &.unselect { - border: none; - padding: 0; - background: transparent; - height: unset; - } - } - - /* Reset textarea styles */ - textarea.untext { - background: none; - border: none; - - &:focus { box-shadow: none; } - } - - .form-grid { - display: flex; - flex-wrap: wrap; - gap: .75rem; - justify-content: center; - - &.form-grid-20 > * { flex-basis: 20%; } - - &.form-grid-50 { - display: grid; - grid-template-columns: 1fr 1fr; - } - - input { - border: 2px solid var(--dnd5e-color-gold); - border-radius: 8px; - padding: .5rem; - background: var(--dnd5e-color-parchment); - font-size: var(--font-size-14); - height: unset; - width: 100%; - - &:focus { box-shadow: 0 0 6px var(--dnd5e-color-gold); } - &::placeholder { color: var(--color-text-light-6); } - } - - input[type="number"] { - font-weight: bold; - } - } - - label.input-stack { - display: flex; - flex-direction: column; - - span { - text-align: center; - font-size: var(--font-size-10); - color: var(--color-text-dark-5); - } - - input { text-align: center; } - } - - /* FA icon-based slide-toggle */ - label.slide-toggle { - display: block; - - > input { display: none; } - &:has(input:not([disabled])) { cursor: pointer; } - } - - /* Pips */ - .pips { - display: flex; - gap: 5px; - padding: 10px 12px; - - .pip { - --pip-size: 16px; - background: var(--dnd5e-color-light-gray); - border: 2px solid var(--dnd5e-color-gold); - border-radius: 100%; - width: var(--pip-size); - height: var(--pip-size); - display: grid; - place-content: center; - font-weight: bold; - font-size: var(--font-size-18); - box-shadow: 0 0 6px var(--dnd5e-shadow-45); - margin: 0; - - &.filled { background: var(--dnd5e-color-gold); } - &:not(:disabled):is(:hover, :focus) { box-shadow: 0 0 6px var(--dnd5e-color-gold); } - } - } - /* ---------------------------------- */ /* System Badge */ /* ---------------------------------- */ diff --git a/less/v2/character.less b/less/v2/character.less index c27cc384d7..8d86c93da9 100644 --- a/less/v2/character.less +++ b/less/v2/character.less @@ -89,7 +89,6 @@ .level-badge { width: var(--level-size); height: var(--level-size); - background-image: url("ui/level-badge.webp"); margin-left: auto; } @@ -830,6 +829,17 @@ .pill { padding: .25rem .5rem; } } + + /* Item Sections */ + .inventory-element { + gap: 16px; + .items-list { gap: 16px; } + } + + .effects-element { + gap: 16px; + .effects-list { gap: 16px; } + } } /* ---------------------------------- */ @@ -939,6 +949,7 @@ height: 23px; border-radius: 3px; border: 1px solid transparent; + text-align: center; &:hover, &:focus { border: 1px solid var(--dnd5e-color-gold); diff --git a/less/v2/dark/actors.less b/less/v2/dark/actors.less index af715d118f..5c2a548a9a 100644 --- a/less/v2/dark/actors.less +++ b/less/v2/dark/actors.less @@ -1,7 +1,5 @@ :is(.dnd5e-theme-dark, .theme-dark) .dnd5e2.sheet.actor, .dnd5e2.sheet.actor.dnd5e-theme-dark { - .window-content { background: var(--dnd5e-color-dark-gray) url("../../ui/denim075.png"); } - .ability-score { .label { font-weight: bold; } .mod { --dnd5e-color-black: var(--dnd5e-color-blue-white); } diff --git a/less/v2/dark/apps.less b/less/v2/dark/apps.less index 8d1fa5dfc8..15caf802c1 100644 --- a/less/v2/dark/apps.less +++ b/less/v2/dark/apps.less @@ -17,6 +17,8 @@ --filigree-border-color: var(--dnd5e-color-blue-gray-3); --dnd5e-background-5: var(--dnd5e-highlight-10); + .window-content { background: var(--dnd5e-color-dark-gray) url("../../ui/denim075.png"); } + select option { background: var(--dnd5e-color-blue-gray-2); } .window-resizable-handle { filter: invert(1); } diff --git a/less/v2/dark/compendium-browser.less b/less/v2/dark/compendium-browser.less index 3cc2d4041b..f6d6a566f6 100644 --- a/less/v2/dark/compendium-browser.less +++ b/less/v2/dark/compendium-browser.less @@ -2,7 +2,6 @@ --color-text-primary: var(--dnd5e-color-blue-white); .window-content { - background: var(--dnd5e-color-dark-gray) url("../../ui/denim075.png"); -webkit-backdrop-filter: none; backdrop-filter: none; diff --git a/less/v2/dark/forms.less b/less/v2/dark/forms.less new file mode 100644 index 0000000000..677e94df7f --- /dev/null +++ b/less/v2/dark/forms.less @@ -0,0 +1,14 @@ +.theme-dark .dnd5e2, +.dnd5e2.dnd5e-theme-dark { + fieldset { + border-color: var(--dnd5e-color-blue-gray-3); + > legend { color: var(--dnd5e-color-gold); } + } + + .form-group { + select, input { + border: var(--dnd5e-border-gold); + background: var(--dnd5e-background-parchment); + } + } +} diff --git a/less/v2/dark/npc.less b/less/v2/dark/npc.less index b1ed2cfbfa..b2829a5289 100644 --- a/less/v2/dark/npc.less +++ b/less/v2/dark/npc.less @@ -2,6 +2,8 @@ .dnd5e2.sheet.actor.npc.dnd5e-theme-dark { .window-content { + background: var(--dnd5e-color-dark-gray) url("../../ui/denim075.png"); + &::before { content: ""; position: absolute; diff --git a/less/v2/forms.less b/less/v2/forms.less new file mode 100644 index 0000000000..201a7b0ca2 --- /dev/null +++ b/less/v2/forms.less @@ -0,0 +1,293 @@ +.dnd5e2 { + /* Reset button & input styles */ + input.uninput, button.unbutton { + --border: none; + border: var(--border); + box-shadow: none; + outline: none; + + &:hover, &:focus { + box-shadow: none; + border: var(--border); + outline: none; + } + } + + input.uninput { + color: inherit; + font-weight: inherit; + height: unset; + } + + button.unbutton { + background: none; + margin: 0; + border-radius: 0; + line-height: normal; + + > i { margin: 0; } + } + + /* Inputs & Buttons */ + input, button { + transition: all 250ms ease; + } + + button { + background: var(--dnd5e-background-card); + font-size: var(--font-size-13); + text-transform: uppercase; + padding: 3px; + border: 1px solid var(--color-border-light-2); + box-shadow: 0 0 8px var(--dnd5e-shadow-15); + display: flex; + align-items: center; + justify-content: center; + gap: .25rem; + + &:not(.fas, .far, .fa-solid, .fa-regular, .fa-light, .fa-duotone, .fa-thin) { + font-family: var(--dnd5e-font-roboto); + font-weight: bold; + } + &:disabled { + cursor: default; + color: var(--color-text-dark-inactive); + } + &:hover:not(:disabled), &:focus { box-shadow: 0 0 5px var(--color-shadow-primary); } + } + + button.radio-button { + width: 13px; + height: 13px; + border-radius: 100%; + background: none; + box-shadow: inset 0 0 6px var(--dnd5e-shadow-45); + padding: 3px; + display: grid; + border: none; + justify-content: unset; + + &:hover { filter: drop-shadow(0 0 6px var(--dnd5e-color-gold)); } + + &[aria-pressed="true"]::before { + content: ""; + width: 100%; + height: 100%; + display: inline-block; + border-radius: 100%; + background: var(--color-border-dark-5); + } + } + + /* Reset select styles */ + select { + color: inherit; + + &.unselect { + border: none; + padding: 0; + background: transparent; + height: unset; + } + } + + /* Reset textarea styles */ + textarea.untext { + background: none; + border: none; + + &:focus { box-shadow: none; } + } + + .form-grid { + display: flex; + flex-wrap: wrap; + gap: .75rem; + justify-content: center; + + &.form-grid-20 > * { flex-basis: 20%; } + + &.form-grid-50 { + display: grid; + grid-template-columns: 1fr 1fr; + } + + input { + border: 2px solid var(--dnd5e-color-gold); + border-radius: 8px; + padding: .5rem; + background: var(--dnd5e-color-parchment); + font-size: var(--font-size-14); + height: unset; + width: 100%; + + &:focus { box-shadow: 0 0 6px var(--dnd5e-color-gold); } + &::placeholder { + --input-placeholder-color: var(--color-text-light-6); + opacity: unset; + color: var(--input-placeholder-color); + } + } + + input[type="number"] { + font-weight: bold; + } + } + + label.input-stack { + display: flex; + flex-direction: column; + + span { + text-align: center; + font-size: var(--font-size-10); + color: var(--color-text-dark-5); + } + + input { text-align: center; } + } + + label.checkbox { + display: flex; + align-items: center; + gap: 4px; + + > span { line-height: calc(var(--form-field-height) + 1px); } + } + + /* Fieldset */ + + fieldset { + display: flex; + flex-direction: column; + gap: 4px; + border-radius: 4px; + border: 1px solid var(--color-border-light-tertiary); + + > legend { + text-transform: uppercase; + font-weight: bold; + font-size: var(--font-size-11); + color: var(--color-text-dark-5); + padding: 0 6px; + } + } + + /* Form Groups */ + .form-group { + margin: 0; + + > label { + font-family: var(--dnd5e-font-roboto-slab); + font-weight: bold; + font-size: var(--font-size-12); + } + + &.label-top { + flex-direction: column; + align-items: start; + gap: 2px; + + > * { flex: unset; } + + > label { + font-family: var(--dnd5e-font-roboto); + text-transform: uppercase; + font-size: var(--font-size-10); + font-weight: bold; + line-height: 1; + } + + + .form-group { align-self: end; } + } + + /* Nested Groups */ + > .form-fields > .form-group { + flex-wrap: nowrap; + &:not(.label-top) { gap: 4px; } + > * { flex: unset; } + } + + select, input { + --input-border-color: transparent; + border: 1px solid var(--input-border-color); + font-size: var(--font-size-11); + transition: all 250ms ease; + line-height: normal; + padding: 6px; + background: var(--input-background-color); + + &:focus-visible, &:hover { + --input-border-color: var(--dnd5e-color-gold); + box-shadow: 0 0 6px var(--dnd5e-color-gold); + } + } + + input::placeholder { + --input-placeholder-color: var(--color-text-light-6); + opacity: unset; + color: var(--input-placeholder-color); + } + + option { background: var(--dnd5e-color-parchment); } + input[type=number] { text-align: center; } + select { cursor: pointer; } + } + + .form-fields > dnd5e-checkbox { flex: none; } + + .form-group.checkbox > label, label.checkbox { + font-weight: normal; + font-family: var(--dnd5e-font-roboto); + font-size: var(--font-size-11); + } + + .form-group.checkbox { justify-content: space-evenly; } + + /* Checkbox Grid */ + .checkbox-grid { + --num-cols: 4; + + &.checkbox-grid-3 { --num-cols: 3; } + + > .form-fields { + display: grid; + grid-template-columns: repeat(var(--num-cols), 1fr); + + > label { text-align: unset; } + } + } + + /* FA icon-based slide-toggle */ + label.slide-toggle { + display: block; + + > input { display: none; } + &:has(input:not([disabled])) { cursor: pointer; } + } + + /* Pips */ + .pips { + display: flex; + gap: 5px; + padding: 10px 12px; + + .pip { + --pip-size: 16px; + background: var(--dnd5e-color-light-gray); + border: 2px solid var(--dnd5e-color-gold); + border-radius: 100%; + width: var(--pip-size); + height: var(--pip-size); + display: grid; + place-content: center; + font-weight: bold; + font-size: var(--font-size-18); + box-shadow: 0 0 6px var(--dnd5e-shadow-45); + margin: 0; + + &.filled { background: var(--dnd5e-color-gold); } + &:not(:disabled):is(:hover, :focus) { box-shadow: 0 0 6px var(--dnd5e-color-gold); } + } + } +} diff --git a/less/v2/inventory.less b/less/v2/inventory.less index f7e929d364..99c73a995b 100644 --- a/less/v2/inventory.less +++ b/less/v2/inventory.less @@ -1,7 +1,7 @@ .dnd5e2 .inventory-element { display: flex; flex-direction: column; - gap: 1rem; + gap: 8px; /* ---------------------------------- */ /* Common Styles */ @@ -205,7 +205,7 @@ .items-list { display: flex; flex-direction: column; - gap: 1rem; + gap: 8px; container-type: inline-size; } @@ -565,7 +565,7 @@ .dnd5e2 section.currency, .dnd5e2 .currency-style { display: flex; - gap: 1rem; + gap: 8px; label { position: relative; diff --git a/less/v2/journal.less b/less/v2/journal.less index 8dc03f9684..c544b07ad9 100644 --- a/less/v2/journal.less +++ b/less/v2/journal.less @@ -73,126 +73,6 @@ > i::before { content: "\f52d"; } } - - /* Content */ - .journal-page-content { - font-family: var(--dnd5e-font-roboto-condensed); - - /* Content Links & Enrichers */ - .passive-check { - text-decoration-color: currentcolor; - text-decoration-style: dashed; - text-underline-offset: 2px; - } - - a:is(.content-link, .inline-roll), :is(.roll-link, .reference-link) a { - background: transparent; - border: none; - text-decoration: underline currentcolor; - text-underline-offset: 2px; - - > i { - color: var(--dnd5e-color-black); - opacity: .75; - transition: opacity 250ms ease; - } - - &.broken { - color: var(--dnd5e-color-maroon); - cursor: not-allowed; - > i { color: currentcolor; } - } - - &:hover { - text-shadow: none; - > i { opacity: 1; } - } - - &:focus-visible { text-shadow: 0 0 8px var(--color-shadow-primary); } - } - - a.enricher-action { - text-decoration: none; - - &::before { - content: "\f105"; - font-family: var(--font-awesome); - font-weight: bold; - margin: 0 4px 0 -6px; - font-size: var(--font-size-10); - color: var(--color-text-light-6); - } - } - - .roll-link a > i.fa-dice-d20, a.inline-roll > i.fa-dice-d20 { - display: inline-block; - width: 1em; - height: 1em; - background: url("../../icons/svg/d20-black.svg") no-repeat center / contain; - vertical-align: middle; - - &::before { content: unset; } - } - - /* Embed Captions */ - .content-embed figcaption cite { - display: block; - text-align: right; - &::before { content: "-"; } - } - - /* Embedded Roll Tables */ - figure.content-embed.caption-top { - display: flex; - flex-direction: column; - position: relative; - margin: 0 0 2rem 0; - - table, img { order: 2; } - - cite { - position: absolute; - inset: 100% 0 auto auto; - } - - .embed-caption { - font-weight: normal; - text-align: left; - font-style: italic; - } - } - - /* Tables */ - table { - --dnd5e-color-table-row-even: var(--dnd5e-color-parchment); - --dnd5e-color-table-row-odd: var(--dnd5e-color-card); - background: var(--dnd5e-color-card); - border-color: var(--dnd5e-color-gold); - - caption { - margin-block-end: 2px; - font-family: var(--dnd5e-font-roboto-slab); - font-variant: small-caps; - font-weight: bold; - } - - thead { - background: linear-gradient(to right, var(--dnd5e-color-hd-1), var(--dnd5e-color-hd-2)); - color: var(--color-text-light-0); - text-align: center; - font-family: var(--dnd5e-font-roboto-slab); - font-weight: bold; - text-shadow: none; - border-color: var(--dnd5e-color-gold); - - tr:nth-child(n) { background: unset; } - } - - tr:nth-child(even) { background-color: var(--dnd5e-color-table-row-even); } - tr:nth-child(odd) { background-color: var(--dnd5e-color-table-row-odd); } - td { padding: .375rem; } - } - } } :is(.dnd5e, .dnd5e2, .dnd5e2-journal) { @@ -293,3 +173,143 @@ p:last-of-type { margin-bottom: 0; } } } + +:is(.dnd5e2, .dnd5e2-journal) { + .journal-page-content, .editor-content { + font-family: var(--dnd5e-font-roboto-condensed); + + h3 { padding-bottom: 0; } + + /* Content Links & Enrichers */ + .passive-check { + text-decoration-color: currentcolor; + text-decoration-style: dashed; + text-underline-offset: 2px; + } + + /* Embed Captions */ + .content-embed figcaption cite { + display: block; + text-align: right; + &::before { content: "-"; } + } + + /* Embedded Roll Tables */ + figure.content-embed.caption-top { + display: flex; + flex-direction: column; + position: relative; + margin: 0 0 2rem 0; + + table, img { order: 2; } + + cite { + position: absolute; + inset: 100% 0 auto auto; + } + + .embed-caption { + font-weight: normal; + text-align: left; + font-style: italic; + } + } + + /* Tables */ + table { + --dnd5e-color-table-row-even: var(--dnd5e-color-parchment); + --dnd5e-color-table-row-odd: var(--dnd5e-color-card); + background: var(--dnd5e-color-card); + border-color: var(--dnd5e-color-gold); + + caption { + margin-block-end: 2px; + font-family: var(--dnd5e-font-roboto-slab); + font-variant: small-caps; + font-weight: bold; + } + + thead { + background: linear-gradient(to right, var(--dnd5e-color-hd-1), var(--dnd5e-color-hd-2)); + color: var(--color-text-light-0); + text-align: center; + font-family: var(--dnd5e-font-roboto-slab); + font-weight: bold; + text-shadow: none; + border-color: var(--dnd5e-color-gold); + + tr:nth-child(n) { background: unset; } + } + + tr:nth-child(even) { background-color: var(--dnd5e-color-table-row-even); } + tr:nth-child(odd) { background-color: var(--dnd5e-color-table-row-odd); } + td { padding: .375rem; } + } + } + + a:is(.content-link, .inline-roll), :is(.roll-link, .reference-link) a { + background: transparent; + border: none; + text-decoration: underline currentcolor; + text-underline-offset: 2px; + + > i { + color: var(--dnd5e-color-black); + opacity: .75; + transition: opacity 250ms ease; + } + + &.broken { + color: var(--dnd5e-color-maroon); + cursor: not-allowed; + > i { color: currentcolor; } + } + + &:hover { + text-shadow: none; + > i { opacity: 1; } + } + + &:focus-visible { text-shadow: 0 0 8px var(--color-shadow-primary); } + } + + a.enricher-action { + text-decoration: none; + + &::before { + content: "\f105"; + font-family: var(--font-awesome); + font-weight: bold; + margin: 0 4px 0 -6px; + font-size: var(--font-size-10); + color: var(--color-text-light-6); + } + } + + .roll-link a > i.fa-dice-d20, a.inline-roll > i.fa-dice-d20 { + display: inline-block; + width: 1em; + height: 1em; + background: url("../../icons/svg/d20-black.svg") no-repeat center / contain; + vertical-align: middle; + + &::before { content: unset; } + } + + .prosemirror menu { + .pm-dropdown { + background: none; + &:hover { background: var(--color-hover-bg); } + } + + button { + display: unset; + text-transform: unset; + box-shadow: none; + font-family: var(--font-primary); + font-size: var(--font-size-14); + font-weight: normal; + line-height: 28px; + } + } +} diff --git a/less/v2/npc.less b/less/v2/npc.less index a3534eb88b..f3c4511fba 100644 --- a/less/v2/npc.less +++ b/less/v2/npc.less @@ -27,7 +27,7 @@ display: flex; align-items: center; line-height: 1; - margin-top: 7px; + margin-top: 6px; gap: 4px; > div:has(> span:not(:empty)) + div::before { content: " • "; } @@ -263,6 +263,7 @@ .score { text-align: center; font-size: var(--font-size-13); + > input { text-align: center; } } .save-tab { @@ -526,10 +527,8 @@ /* Inventory */ .inventory-element { - gap: 8px; width: 100%; - .items-list { gap: 8px; } .spells-list { gap: 12px; } .bottom { diff --git a/less/v2/sheets.less b/less/v2/sheets.less new file mode 100644 index 0000000000..2e5c69a4dc --- /dev/null +++ b/less/v2/sheets.less @@ -0,0 +1,115 @@ +/* Styles common to Document sheets */ +.dnd5e2.sheet:is(.item, .actor) { + + /* ---------------------------------- */ + /* Outer Window */ + /* ---------------------------------- */ + + > header { + background: transparent; + position: relative; + z-index: 1; + + .window-title { visibility: hidden; } + + /* Edit Mode Toggle */ + > slide-toggle { + --slide-toggle-track-color-checked: var(--dnd5e-color-gold); + --slide-toggle-track-color-unchecked: var(--dnd5e-color-light-gray); + flex: none; + + .slide-toggle-thumb { + line-height: 12px; + + &::before { + content: "\f0ad"; + font-family: var(--font-awesome); + color: var(--dnd5e-color-light-gray); + font-weight: bold; + font-size: var(--font-size-9); + } + } + + &[checked] .slide-toggle-thumb::before { + color: var(--dnd5e-color-gold); + } + } + } + + .window-content { margin-top: -30px; } + + /* ---------------------------------- */ + /* Sheet Header */ + /* ---------------------------------- */ + + .sheet-header { + input, select { + --input-height: var(--form-field-height, 26px); + background: rgb(0 0 0 / 45%); + border: 1px solid transparent; + transition: all 250ms ease; + line-height: unset; + padding: 1px 4px; + border-radius: 3px; + + &:hover, &:focus { + border: 1px solid var(--dnd5e-color-gold); + box-shadow: 0 0 6px var(--dnd5e-color-gold); + } + } + } + + /* ---------------------------------- */ + /* Badges */ + /* ---------------------------------- */ + + .badge { + background: transparent no-repeat center / contain; + font-family: var(--dnd5e-font-roboto); + font-weight: bold; + color: var(--color-text-light-0); + display: grid; + place-content: center; + font-size: var(--font-size-30); + + .config-button { + color: inherit; + font-size: inherit; + } + } + + /* AC */ + .ac-badge { + width: 68px; + height: 68px; + text-align: center; + line-height: 62px; + background-image: url("ui/ac-badge.webp"); + display: block; + place-content: unset; + font-size: var(--font-size-23); + margin-top: -21px; + filter: drop-shadow(0 0 6px var(--color-shadow-dark)); + } + + /* Level */ + .level-badge { + width: 78px; + height: 78px; + background-image: url("ui/level-badge.webp"); + } + + /* ---------------------------------- */ + /* Child Creation */ + /* ---------------------------------- */ + + .create-child { + display: none; + position: absolute; + inset: auto 16px 16px auto; + } + + form:is(.tab-inventory, .tab-features, .tab-spells, .tab-effects, .tab-advancement) .create-child { + display: block; + } +} diff --git a/less/v2/tooltips.less b/less/v2/tooltips.less index 2fa6bbf9ce..a6d2fd90bc 100644 --- a/less/v2/tooltips.less +++ b/less/v2/tooltips.less @@ -137,16 +137,6 @@ } } - .info { - text-align: left; - font-size: var(--font-size-12); - - .materials, .condition { - font-size: var(--font-size-11); - color: var(--color-text-dark-5); - } - } - .description { text-align: left; font-size: var(--font-size-12); diff --git a/module/applications/_module.mjs b/module/applications/_module.mjs index 370aae7ecb..263191ed0b 100644 --- a/module/applications/_module.mjs +++ b/module/applications/_module.mjs @@ -7,6 +7,7 @@ export * as components from "./components/_module.mjs"; export * as dice from "./dice/_module.mjs"; export * as item from "./item/_module.mjs"; export * as journal from "./journal/_module.mjs"; +export * as mixins from "./mixins/_module.mjs"; export {default as Accordion} from "./accordion.mjs"; export {default as Award} from "./award.mjs"; diff --git a/module/applications/actor/base-sheet.mjs b/module/applications/actor/base-sheet.mjs index a8c9d48fe6..9f424723c0 100644 --- a/module/applications/actor/base-sheet.mjs +++ b/module/applications/actor/base-sheet.mjs @@ -531,7 +531,7 @@ export default class ActorSheet5e extends ActorSheetMixin(ActorSheet) { return items.filter(item => { // Subclass-specific logic. - const filtered = this._filterItem(item); + const filtered = this._filterItem(item, filters); if ( filtered !== undefined ) return filtered; // Action usage @@ -566,11 +566,12 @@ export default class ActorSheet5e extends ActorSheetMixin(ActorSheet) { /** * Determine whether an Item will be shown based on the current set of filters. - * @param {Item5e} item The item. + * @param {Item5e} item The item. + * @param {Set} filters Filters applied to the Item. * @returns {boolean|void} * @protected */ - _filterItem(item) {} + _filterItem(item, filters) {} /* -------------------------------------------- */ diff --git a/module/applications/actor/npc-sheet-2.mjs b/module/applications/actor/npc-sheet-2.mjs index 6e7a1dd53d..e536ce6116 100644 --- a/module/applications/actor/npc-sheet-2.mjs +++ b/module/applications/actor/npc-sheet-2.mjs @@ -35,6 +35,7 @@ export default class ActorSheet5eNPC2 extends ActorSheetV2Mixin(ActorSheet5eNPC) /* -------------------------------------------- */ + /** @override */ get template() { if ( !game.user.isGM && this.actor.limited ) return "systems/dnd5e/templates/actors/limited-sheet-2.hbs"; return "systems/dnd5e/templates/actors/npc-sheet-2.hbs"; @@ -47,20 +48,9 @@ export default class ActorSheet5eNPC2 extends ActorSheetV2Mixin(ActorSheet5eNPC) /** @inheritDoc */ async _renderOuter() { const html = await super._renderOuter(); - const elements = document.createElement("div"); - elements.classList.add("header-elements"); - elements.innerHTML = ` -
- - - - -
-
- `; - html[0].querySelector(".window-title")?.insertAdjacentElement("afterend", elements); - elements.querySelector(".config-button").addEventListener("click", this._onConfigMenu.bind(this)); + this._renderSourceOuter(html); + // XP value. + html[0].querySelector(".header-elements")?.insertAdjacentHTML("beforeend", '
'); return html; } @@ -69,17 +59,11 @@ export default class ActorSheet5eNPC2 extends ActorSheetV2Mixin(ActorSheet5eNPC) /** @inheritDoc */ async _render(force=false, options={}) { await super._render(force, options); + this._renderSource(); const [elements] = this.element.find(".header-elements"); if ( !elements ) return; - const { details } = this.actor.system; - const editable = this.isEditable && (this._mode === this.constructor.MODES.EDIT); - const sourceLabel = details.source.label; - elements.querySelector(".config-button")?.toggleAttribute("hidden", !editable); - elements.querySelector(".source-book > span").innerText = editable - ? (sourceLabel || game.i18n.localize("DND5E.Source")) - : sourceLabel; elements.querySelector(".cr-xp").innerText = game.i18n.format("DND5E.ExperiencePointsFormat", { - value: new Intl.NumberFormat(game.i18n.lang).format(details.xp.value) + value: new Intl.NumberFormat(game.i18n.lang).format(this.actor.system.details.xp.value) }); } diff --git a/module/applications/actor/sheet-v2-mixin.mjs b/module/applications/actor/sheet-v2-mixin.mjs index 450103a297..df791318c8 100644 --- a/module/applications/actor/sheet-v2-mixin.mjs +++ b/module/applications/actor/sheet-v2-mixin.mjs @@ -1,14 +1,15 @@ import * as Trait from "../../documents/actor/trait.mjs"; import { formatNumber, simplifyBonus, staticID } from "../../utils.mjs"; import Tabs5e from "../tabs.mjs"; +import DocumentSheetV2Mixin from "../mixins/sheet-v2-mixin.mjs"; /** - * Adds common V2 sheet functionality. + * Adds common V2 Actor sheet functionality. * @param {typeof ActorSheet5e} Base The base class being mixed. * @returns {typeof ActorSheetV2} */ export default function ActorSheetV2Mixin(Base) { - return class ActorSheetV2 extends Base { + return class ActorSheetV2 extends DocumentSheetV2Mixin(Base) { constructor(object, options={}) { const key = `${object.type}${object.limited ? ":limited" : ""}`; const { width, height } = game.user.getFlag("dnd5e", `sheetPrefs.${key}`) ?? {}; @@ -17,36 +18,6 @@ export default function ActorSheetV2Mixin(Base) { super(object, options); } - /** - * @typedef {object} SheetTabDescriptor5e - * @property {string} tab The tab key. - * @property {string} label The tab label's localization key. - * @property {string} [icon] A font-awesome icon. - * @property {string} [svg] An SVG icon. - */ - - /** - * Sheet tabs. - * @type {SheetTabDescriptor5e[]} - */ - static TABS = []; - - /** - * Available sheet modes. - * @enum {number} - */ - static MODES = { - PLAY: 1, - EDIT: 2 - }; - - /** - * The mode the sheet is currently in. - * @type {ActorSheetV2.MODES} - * @protected - */ - _mode = this.constructor.MODES.PLAY; - /** * The cached concentration information for the character. * @type {{items: Set, effects: Set}} @@ -61,41 +32,9 @@ export default function ActorSheetV2Mixin(Base) { /** @inheritDoc */ async _renderOuter() { const html = await super._renderOuter(); + if ( !game.user.isGM && this.actor.limited ) return html; const header = html[0].querySelector(".window-header"); - - // Adjust header buttons. - header.querySelectorAll(".header-button").forEach(btn => { - const label = btn.querySelector(":scope > i").nextSibling; - btn.dataset.tooltip = label.textContent; - btn.setAttribute("aria-label", label.textContent); - label.remove(); - }); - - if ( !game.user.isGM && this.actor.limited ) { - html[0].classList.add("limited"); - return html; - } - - // Add edit <-> play slide toggle. - if ( this.isEditable ) { - const toggle = document.createElement("slide-toggle"); - toggle.checked = this._mode === this.constructor.MODES.EDIT; - toggle.classList.add("mode-slider"); - toggle.dataset.tooltip = "DND5E.SheetModeEdit"; - toggle.setAttribute("aria-label", game.i18n.localize("DND5E.SheetModeEdit")); - toggle.addEventListener("change", this._onChangeSheetMode.bind(this)); - toggle.addEventListener("dblclick", event => event.stopPropagation()); - header.insertAdjacentElement("afterbegin", toggle); - } - - // Document UUID link. const firstButton = header.querySelector(".header-button"); - const idLink = header.querySelector(".document-id-link"); - if ( idLink ) { - firstButton?.insertAdjacentElement("beforebegin", idLink); - idLink.classList.add("header-button"); - idLink.dataset.tooltipDirection = "DOWN"; - } // Preparation warnings. const warnings = document.createElement("a"); @@ -146,8 +85,6 @@ export default function ActorSheetV2Mixin(Base) { async getData(options) { this._concentration = this.actor.concentration; // Cache concentration so it's not called for every item. const context = await super.getData(options); - context.editable = this.isEditable && (this._mode === this.constructor.MODES.EDIT); - context.cssClass = context.editable ? "editable" : this.isEditable ? "interactable" : "locked"; const activeTab = (game.user.isGM || !this.actor.limited) ? this._tabs?.[0]?.active ?? this.options.tabs[0].initial : "biography"; @@ -248,6 +185,7 @@ export default function ActorSheetV2Mixin(Base) { } context.effects.suppressed.info = context.effects.suppressed.info[0]; + context.hasConditions = true; return context; } @@ -448,9 +386,7 @@ export default function ActorSheetV2Mixin(Base) { html.find(".rollable:is(.saving-throw, .ability-check)").on("click", this._onRollAbility.bind(this)); 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)); // Prevent default middle-click scrolling when locking a tooltip. @@ -463,7 +399,6 @@ export default function ActorSheetV2Mixin(Base) { if ( this.isEditable ) { html.find(".meter > .hit-points").on("click", event => this._toggleEditHP(event, true)); html.find(".meter > .hit-points > input").on("blur", event => this._toggleEditHP(event, false)); - html.find(".create-child").on("click", this._onCreateChild.bind(this)); } // Play mode only. @@ -474,29 +409,9 @@ export default function ActorSheetV2Mixin(Base) { /* -------------------------------------------- */ - /** - * Handle the user toggling the sheet mode. - * @param {Event} event The triggering event. - * @protected - */ - async _onChangeSheetMode(event) { - const { MODES } = this.constructor; - const toggle = event.currentTarget; - const label = game.i18n.localize(`DND5E.SheetMode${toggle.checked ? "Play" : "Edit"}`); - toggle.dataset.tooltip = label; - toggle.setAttribute("aria-label", label); - this._mode = toggle.checked ? MODES.EDIT : MODES.PLAY; - await this.submit(); - this.render(); - } - - /* -------------------------------------------- */ - /** @inheritDoc */ _onChangeTab(event, tabs, active) { super._onChangeTab(event, tabs, active); - this.form.className = this.form.className.replace(/tab-\w+/g, ""); - this.form.classList.add(`tab-${active}`); const sheetPrefs = `sheetPrefs.${this.actor.type}.tabs.${active}`; const sidebarCollapsed = game.user.getFlag("dnd5e", `${sheetPrefs}.collapseSidebar`); if ( sidebarCollapsed !== undefined ) this._toggleSidebar(sidebarCollapsed); @@ -520,11 +435,7 @@ export default function ActorSheetV2Mixin(Base) { /* -------------------------------------------- */ - /** - * Handle creating a new embedded child. - * @returns {ActiveEffect5e|Item5e|void} - * @protected - */ + /** @override */ _onCreateChild() { const activeTab = this._tabs?.[0]?.active ?? this.options.tabs[0].initial; @@ -636,40 +547,6 @@ export default function ActorSheetV2Mixin(Base) { /* -------------------------------------------- */ - /** - * Handle toggling an Item's description. - * @param {MouseEvent} event The triggering event. - * @protected - */ - async _onToggleDescription(event) { - const target = event.currentTarget; - const icon = target.querySelector(":scope > i"); - const row = target.closest("[data-item-id]"); - const summary = row.querySelector(":scope > .item-description > .wrapper"); - const { itemId } = row.dataset; - const expanded = this._expanded.has(itemId); - const item = this.actor.items.get(itemId); - if ( !item ) return; - - if ( expanded ) { - summary.addEventListener("transitionend", () => { - if ( row.classList.contains("collapsed") ) summary.replaceChildren(); - }, { once: true }); - this._expanded.delete(itemId); - } else { - const context = await item.getChatData({ secrets: item.isOwner }); - summary.innerHTML = await renderTemplate("systems/dnd5e/templates/items/parts/item-summary.hbs", context); - await new Promise(resolve => requestAnimationFrame(resolve)); - this._expanded.add(itemId); - } - - row.classList.toggle("collapsed", expanded); - icon.classList.toggle("fa-compress", !expanded); - icon.classList.toggle("fa-expand", expanded); - } - - /* -------------------------------------------- */ - /** * Handle toggling a pip on the character sheet. * @param {PointerEvent} event The triggering event. @@ -735,33 +612,6 @@ export default function ActorSheetV2Mixin(Base) { /* -------------------------------------------- */ - /** - * Initialize item tooltips on an element. - * @param {HTMLElement} element The tooltipped element. - * @protected - */ - _applyItemTooltips(element) { - if ( "tooltip" in element.dataset ) return; - const target = element.closest("[data-item-id], [data-effect-id], [data-uuid]"); - let uuid = target.dataset.uuid; - if ( !uuid && target.dataset.itemId ) { - const item = this.actor.items.get(target.dataset.itemId); - uuid = item?.uuid; - } else if ( !uuid && target.dataset.effectId ) { - const { effectId, parentId } = target.dataset; - const collection = parentId ? this.actor.items.get(parentId).effects : this.actor.effects; - uuid = collection.get(effectId)?.uuid; - } - if ( !uuid ) return; - element.dataset.tooltip = ` -
- `; - element.dataset.tooltipClass = "dnd5e2 dnd5e-tooltip item-tooltip"; - element.dataset.tooltipDirection ??= "LEFT"; - } - - /* -------------------------------------------- */ - /** * Initialize a rule tooltip on an element. * @param {HTMLElement} element The tooltipped element. diff --git a/module/applications/components/adopted-stylesheet-mixin.mjs b/module/applications/components/adopted-stylesheet-mixin.mjs index 07f67434ca..1c39055f12 100644 --- a/module/applications/components/adopted-stylesheet-mixin.mjs +++ b/module/applications/components/adopted-stylesheet-mixin.mjs @@ -23,7 +23,8 @@ export default function AdoptedStyleSheetMixin(Base) { /** @inheritDoc */ adoptedCallback() { - this._adoptStyleSheet(this._getStyleSheet()); + const sheet = this._getStyleSheet(); + if ( sheet ) this._adoptStyleSheet(this._getStyleSheet()); } /* -------------------------------------------- */ @@ -35,7 +36,7 @@ export default function AdoptedStyleSheetMixin(Base) { */ _getStyleSheet() { let sheet = this.constructor._stylesheets.get(this.ownerDocument); - if ( !sheet ) { + if ( !sheet && this.ownerDocument.defaultView ) { sheet = new this.ownerDocument.defaultView.CSSStyleSheet(); sheet.replaceSync(this.constructor.CSS); this.constructor._stylesheets.set(this.ownerDocument, sheet); diff --git a/module/applications/components/checkbox.mjs b/module/applications/components/checkbox.mjs index 7920ad3859..b0c68b8319 100644 --- a/module/applications/components/checkbox.mjs +++ b/module/applications/components/checkbox.mjs @@ -32,7 +32,7 @@ export default class CheckboxElement extends AdoptedStyleSheetMixin( height: var(--checkbox-size, 18px); aspect-ratio: 1; } - + :host > div { width: 100%; height: 100%; @@ -41,7 +41,7 @@ export default class CheckboxElement extends AdoptedStyleSheetMixin( background: var(--checkbox-empty-color, transparent); box-sizing: border-box; } - + :host :is(.checked, .disabled, .indeterminate) { display: none; height: 100%; @@ -50,7 +50,7 @@ export default class CheckboxElement extends AdoptedStyleSheetMixin( align-items: center; justify-content: center; } - + :host([checked]) .checked { display: flex; } :host([indeterminate]) .indeterminate { display: flex; } :host([indeterminate]) .checked { display: none; } @@ -136,6 +136,13 @@ export default class CheckboxElement extends AdoptedStyleSheetMixin( this._setValue(value); } + /** @override */ + _getValue() { + // Workaround for FormElementExtended only checking the value property and not the checked property. + if ( typeof this._value === "string" ) return this._value; + return this.checked; + } + /* -------------------------------------------- */ /* Element Lifecycle */ /* -------------------------------------------- */ diff --git a/module/applications/components/inventory.mjs b/module/applications/components/inventory.mjs index b26245330c..d6b03146a2 100644 --- a/module/applications/components/inventory.mjs +++ b/module/applications/components/inventory.mjs @@ -217,8 +217,8 @@ export default class InventoryElement extends HTMLElement { { name: "DND5E.ConcentrationBreak", icon: '', - condition: () => this.actor.concentration?.items.has(item), - callback: () => this.actor.endConcentration(item), + condition: () => this.actor?.concentration?.items.has(item), + callback: () => this.actor?.endConcentration(item), group: "state" } ]; diff --git a/module/applications/components/item-list-controls.mjs b/module/applications/components/item-list-controls.mjs index 36a1f7e1aa..9f8ab9f8fc 100644 --- a/module/applications/components/item-list-controls.mjs +++ b/module/applications/components/item-list-controls.mjs @@ -181,7 +181,7 @@ export default class ItemListControlsElement extends HTMLElement {
  • @@ -302,7 +302,7 @@ export default class ItemListControlsElement extends HTMLElement { el.hidden = true; }); for ( const entry of entries ) { - const el = elementMap[`${entry.parent.id}.${entry.id}`] ?? elementMap[entry.id]; + const el = elementMap[`${entry.parent?.id}.${entry.id}`] ?? elementMap[entry.id]; if ( el ) el.hidden = false; } this.list.querySelectorAll(".items-section:has(.item-list .item:not([hidden]))").forEach(el => el.hidden = false); diff --git a/module/applications/mixins/_module.mjs b/module/applications/mixins/_module.mjs new file mode 100644 index 0000000000..a0946fe7cf --- /dev/null +++ b/module/applications/mixins/_module.mjs @@ -0,0 +1 @@ +export {default as DocumentSheetV2Mixin} from "./sheet-v2-mixin.mjs"; diff --git a/module/applications/mixins/sheet-v2-mixin.mjs b/module/applications/mixins/sheet-v2-mixin.mjs new file mode 100644 index 0000000000..46d7334cf1 --- /dev/null +++ b/module/applications/mixins/sheet-v2-mixin.mjs @@ -0,0 +1,254 @@ +/** + * Adds common V2 sheet functionality. + * @param {typeof DocumentSheet} Base The base class being mixed. + * @returns {typeof DocumentSheetV2} + */ +export default function DocumentSheetV2Mixin(Base) { + return class DocumentSheetV2 extends Base { + /** + * @typedef {object} SheetTabDescriptor5e + * @property {string} tab The tab key. + * @property {string} label The tab label's localization key. + * @property {string} [icon] A font-awesome icon. + * @property {string} [svg] An SVG icon. + * @property {SheetTabCondition5e} [condition] A predicate to check before rendering the tab. + */ + + /** + * @callback SheetTabCondition5e + * @param {Document} doc The Document instance. + * @returns {boolean} Whether to render the tab. + */ + + /** + * Sheet tabs. + * @type {SheetTabDescriptor5e[]} + */ + static TABS = []; + + /** + * Available sheet modes. + * @enum {number} + */ + static MODES = { + PLAY: 1, + EDIT: 2 + }; + + /** + * The mode the sheet is currently in. + * @type {ActorSheetV2.MODES} + * @protected + */ + _mode = this.constructor.MODES.PLAY; + + /* -------------------------------------------- */ + /* Rendering */ + /* -------------------------------------------- */ + + /** @inheritDoc */ + async _renderOuter() { + const html = await super._renderOuter(); + const header = html[0].querySelector(".window-header"); + + // Adjust header buttons. + header.querySelectorAll(".header-button").forEach(btn => { + const label = btn.querySelector(":scope > i").nextSibling; + btn.dataset.tooltip = label.textContent; + btn.setAttribute("aria-label", label.textContent); + label.remove(); + }); + + if ( !game.user.isGM && this.document.limited ) { + html[0].classList.add("limited"); + return html; + } + + // Add edit <-> play slide toggle. + if ( this.isEditable ) { + const toggle = document.createElement("slide-toggle"); + toggle.checked = this._mode === this.constructor.MODES.EDIT; + toggle.classList.add("mode-slider"); + toggle.dataset.tooltip = "DND5E.SheetModeEdit"; + toggle.setAttribute("aria-label", game.i18n.localize("DND5E.SheetModeEdit")); + toggle.addEventListener("change", this._onChangeSheetMode.bind(this)); + toggle.addEventListener("dblclick", event => event.stopPropagation()); + header.insertAdjacentElement("afterbegin", toggle); + } + + // Document UUID link. + const firstButton = header.querySelector(".header-button"); + const idLink = header.querySelector(".document-id-link"); + if ( idLink ) { + firstButton?.insertAdjacentElement("beforebegin", idLink); + idLink.classList.add("header-button"); + idLink.dataset.tooltipDirection = "DOWN"; + } + + return html; + } + + /* -------------------------------------------- */ + + /** + * Render source information in the Document's title bar. + * @param {jQuery} html The outer frame HTML. + * @protected + */ + _renderSourceOuter([html]) { + const elements = document.createElement("div"); + elements.classList.add("header-elements"); + elements.innerHTML = ` +
    + + + + +
    + `; + html.querySelector(".window-title")?.insertAdjacentElement("afterend", elements); + elements.querySelector(".config-button").addEventListener("click", this._onConfigMenu.bind(this)); + } + + /* -------------------------------------------- */ + + /** + * Update the source information when re-rendering the sheet. + * @protected + */ + _renderSource() { + const [elements] = this.element.find(".header-elements"); + const source = this.actor?.system.details?.source ?? this.item?.system.source; + if ( !elements || !source ) return; + const editable = this.isEditable && (this._mode === this.constructor.MODES.EDIT); + elements.querySelector(".config-button")?.toggleAttribute("hidden", !editable); + elements.querySelector(".source-book > span").innerText = editable + ? (source.label || game.i18n.localize("DND5E.Source")) + : source.label; + } + + /* -------------------------------------------- */ + + /** @inheritDoc */ + async getData(options) { + const context = await super.getData(options); + context.editable = this.isEditable && (this._mode === this.constructor.MODES.EDIT); + context.cssClass = context.editable ? "editable" : this.isEditable ? "interactable" : "locked"; + return context; + } + + /* -------------------------------------------- */ + /* Event Listeners & Handlers */ + /* -------------------------------------------- */ + + /** @inheritDoc */ + activateListeners(html) { + super.activateListeners(html); + html.find("[data-toggle-description]").on("click", this._onToggleDescription.bind(this)); + this.form.querySelectorAll(".item-tooltip").forEach(this._applyItemTooltips.bind(this)); + + if ( this.isEditable ) { + html.find(".create-child").on("click", this._onCreateChild.bind(this)); + } + } + + /* -------------------------------------------- */ + + /** + * Initialize item tooltips on an element. + * @param {HTMLElement} element The tooltipped element. + * @protected + */ + _applyItemTooltips(element) { + if ( "tooltip" in element.dataset ) return; + const target = element.closest("[data-item-id], [data-effect-id], [data-uuid]"); + let uuid = target.dataset.uuid; + if ( !uuid && target.dataset.itemId ) { + const item = this.actor?.items.get(target.dataset.itemId); + uuid = item?.uuid; + } else if ( !uuid && target.dataset.effectId ) { + const { effectId, parentId } = target.dataset; + const collection = parentId ? this.actor?.items.get(parentId).effects : this.actor?.effects; + uuid = collection?.get(effectId)?.uuid; + } + if ( !uuid ) return; + element.dataset.tooltip = ` +
    + `; + element.dataset.tooltipClass = "dnd5e2 dnd5e-tooltip item-tooltip"; + element.dataset.tooltipDirection ??= "LEFT"; + } + + /* -------------------------------------------- */ + + /** + * Handle the user toggling the sheet mode. + * @param {Event} event The triggering event. + * @protected + */ + async _onChangeSheetMode(event) { + const { MODES } = this.constructor; + const toggle = event.currentTarget; + const label = game.i18n.localize(`DND5E.SheetMode${toggle.checked ? "Play" : "Edit"}`); + toggle.dataset.tooltip = label; + toggle.setAttribute("aria-label", label); + this._mode = toggle.checked ? MODES.EDIT : MODES.PLAY; + await this.submit(); + this.render(); + } + + /* -------------------------------------------- */ + + /** @inheritDoc */ + _onChangeTab(event, tabs, active) { + super._onChangeTab(event, tabs, active); + this.form.className = this.form.className.replace(/tab-\w+/g, ""); + this.form.classList.add(`tab-${active}`); + } + + /* -------------------------------------------- */ + + /** + * Handle creating a new embedded child. + * @returns {any} + * @protected + * @abstract + */ + _onCreateChild() {} + + /* -------------------------------------------- */ + + /** + * Handle toggling an Item's description. + * @param {PointerEvent} event The triggering event. + * @protected + */ + async _onToggleDescription(event) { + const target = event.currentTarget; + const icon = target.querySelector(":scope > i"); + const row = target.closest("[data-uuid]"); + const summary = row.querySelector(":scope > .item-description > .wrapper"); + const { uuid } = row.dataset; + const item = await fromUuid(uuid); + if ( !item ) return; + + const expanded = this._expanded.has(item.id); + if ( expanded ) { + summary.addEventListener("transitionend", () => { + if ( row.classList.contains("collapsed") ) summary.replaceChildren(); + }, { once: true }); + this._expanded.delete(item.id); + } else { + const context = await item.getChatData({ secrets: item.isOwner }); + summary.innerHTML = await renderTemplate("systems/dnd5e/templates/items/parts/item-summary.hbs", context); + await new Promise(resolve => requestAnimationFrame(resolve)); + this._expanded.add(item.id); + } + + row.classList.toggle("collapsed", expanded); + icon.classList.toggle("fa-compress", !expanded); + icon.classList.toggle("fa-expand", expanded); + } + }; +} diff --git a/module/data/abstract.mjs b/module/data/abstract.mjs index 9494fa6d3d..ae291e5837 100644 --- a/module/data/abstract.mjs +++ b/module/data/abstract.mjs @@ -471,7 +471,7 @@ export class ItemDataModel extends SystemDataModel { async getCardData(enrichmentOptions={}) { const { name, type, img } = this.parent; let { - price, weight, uses, identified, unidentified, description, school, materials, activation, properties + price, weight, uses, identified, unidentified, description, school, materials, activation } = this; const rollData = this.parent.getRollData(); const isIdentified = identified !== false; @@ -508,14 +508,6 @@ export class ItemDataModel extends SystemDataModel { ); } - if ( context.labels.duration ) { - context.labels.concentrationDuration = properties?.has("concentration") - ? game.i18n.format("DND5E.ConcentrationDuration", { - duration: context.labels.duration.toLocaleLowerCase(game.i18n.lang) - }) - : context.labels.duration; - } - context.properties = context.properties.filter(_ => _); context.hasProperties = context.tags?.length || context.properties.length; return context; diff --git a/module/data/item/spell.mjs b/module/data/item/spell.mjs index 32002be577..80e65e9827 100644 --- a/module/data/item/spell.mjs +++ b/module/data/item/spell.mjs @@ -151,7 +151,7 @@ export default class SpellData extends ItemDataModel.mixin( const context = await super.getCardData(enrichmentOptions); context.isSpell = true; context.subtitle = [this.parent.labels.level, CONFIG.DND5E.spellSchools[this.school]?.label].filterJoin(" • "); - if ( this.parent.labels.components.vsm ) context.tags = [this.parent.labels.components.vsm, ...context.tags]; + context.properties = context.tags; return context; } diff --git a/module/data/item/templates/activated-effect.mjs b/module/data/item/templates/activated-effect.mjs index bd0aee7a35..f0c169e943 100644 --- a/module/data/item/templates/activated-effect.mjs +++ b/module/data/item/templates/activated-effect.mjs @@ -126,6 +126,10 @@ export default class ActivatedEffectTemplate extends SystemDataModel { // Prepare labels this.parent.labels ??= {}; this.parent.labels.duration = [this.duration.value, CONFIG.DND5E.timePeriods[this.duration.units]].filterJoin(" "); + this.parent.labels.concentrationDuration = this.properties?.has("concentration") + ? game.i18n.format("DND5E.ConcentrationDuration", { + duration: this.parent.labels.duration.toLocaleLowerCase(game.i18n.lang) + }) : this.parent.labels.duration; this.parent.labels.activation = this.activation.type ? [ (this.activation.type in CONFIG.DND5E.staticAbilityActivationTypes) ? null : this.activation.cost, CONFIG.DND5E.abilityActivationTypes[this.activation.type] diff --git a/module/documents/item.mjs b/module/documents/item.mjs index d631ee699f..372cfcfda5 100644 --- a/module/documents/item.mjs +++ b/module/documents/item.mjs @@ -613,7 +613,7 @@ export default class Item5e extends SystemDocumentMixin(Item) { else obj.vsm.push(abbr); return obj; }, {all: [], vsm: [], tags: []}); - this.labels.components.vsm = new Intl.ListFormat(game.i18n.lang, { style: "narrow", type: "conjunction" }) + this.labels.components.vsm = game.i18n.getListFormatter({ style: "narrow", type: "conjunction" }) .format(this.labels.components.vsm); this.labels.materials = this.system?.materials?.value ?? null; } @@ -745,7 +745,7 @@ export default class Item5e extends SystemDocumentMixin(Item) { * @returns {{damageType: string, formula: string, label: string}[]} */ getDerivedDamageLabel() { - if ( !this.hasDamage || !this.isOwned ) return []; + if ( !this.hasDamage ) return []; const rollData = this.getRollData(); const damageLabels = { ...CONFIG.DND5E.damageTypes, ...CONFIG.DND5E.healingTypes }; const derivedDamage = this.system.damage?.parts?.map((damagePart, index) => { diff --git a/module/utils.mjs b/module/utils.mjs index cd0b7f4920..c3c3463c7d 100644 --- a/module/utils.mjs +++ b/module/utils.mjs @@ -18,7 +18,7 @@ export function formatCR(value) { * @param {number} mod The modifier. * @returns {Handlebars.SafeString} */ -function formatModifier(mod) { +export function formatModifier(mod) { if ( !Number.isFinite(mod) ) return new Handlebars.SafeString(""); return new Handlebars.SafeString(`${mod < 0 ? "-" : "+"}${Math.abs(mod)}`); } @@ -362,6 +362,7 @@ export async function preloadHandlebarsTemplates() { "systems/dnd5e/templates/items/parts/item-source.hbs", "systems/dnd5e/templates/items/parts/item-summary.hbs", "systems/dnd5e/templates/items/parts/item-tooltip.hbs", + "systems/dnd5e/templates/items/parts/spell-block.hbs", // Journal Partials "systems/dnd5e/templates/journal/parts/journal-table.hbs", diff --git a/templates/actors/character-sheet-2.hbs b/templates/actors/character-sheet-2.hbs index 0b7f007229..b7239ef997 100644 --- a/templates/actors/character-sheet-2.hbs +++ b/templates/actors/character-sheet-2.hbs @@ -515,7 +515,7 @@ {{!-- Child Creation --}} diff --git a/templates/actors/npc-sheet-2.hbs b/templates/actors/npc-sheet-2.hbs index 0feb71e050..dff5be68ce 100644 --- a/templates/actors/npc-sheet-2.hbs +++ b/templates/actors/npc-sheet-2.hbs @@ -523,7 +523,7 @@ {{!-- Child Creation --}} diff --git a/templates/actors/tabs/creature-features.hbs b/templates/actors/tabs/creature-features.hbs index f554f098d5..239f482018 100644 --- a/templates/actors/tabs/creature-features.hbs +++ b/templates/actors/tabs/creature-features.hbs @@ -39,7 +39,7 @@ {{!-- Items --}}
  • + data-grouped="{{ ctx.group }}" data-ungrouped="{{ ctx.ungroup }}" data-uuid="{{ item.uuid }}">
    diff --git a/templates/actors/tabs/creature-spells.hbs b/templates/actors/tabs/creature-spells.hbs index cd825e0d9a..ab2a6a8d1a 100644 --- a/templates/actors/tabs/creature-spells.hbs +++ b/templates/actors/tabs/creature-spells.hbs @@ -122,7 +122,7 @@ {{!-- Spells --}}
  • diff --git a/templates/compendium/browser-sidebar-filters.hbs b/templates/compendium/browser-sidebar-filters.hbs index ef8d9526bd..e87aa1e8d1 100644 --- a/templates/compendium/browser-sidebar-filters.hbs +++ b/templates/compendium/browser-sidebar-filters.hbs @@ -1,15 +1,15 @@
    {{#each additional}} {{#if (eq type "boolean")}} -
    +
    {{else if (eq type "range")}} -
    - +
    +
    - {{#if (eq type "spell")}} -
      -
    • - {{ localize "DND5E.SpellCastTime" }}: - - {{ labels.activation }} - {{#if activation.condition}} - ({{ activation.condition }}) - {{/if}} - -
    • -
    • - {{ localize "DND5E.Range" }}: - {{ labels.range }} -
    • -
    • - {{ localize "DND5E.Target" }}: - {{ labels.target }} -
    • -
    • - {{ localize "DND5E.Components" }}: - - {{ labels.components.vsm }} - {{#if materials.value}} - ({{ materials.value }}) - {{/if}} - -
    • -
    • - {{ localize "DND5E.Duration" }}: - {{ labels.concentrationDuration }} -
    • -
    + {{#if isSpell}} + {{> "dnd5e.spell-block" }} {{/if}}
    {{{ description.value }}}
      - {{#if isSpell}} - {{#each tags}} -
    • - {{ this }} -
    • - {{/each}} - {{else}} {{#each properties}}
    • {{ this }}
    • {{/each}} - {{/if}}
    {{#if controlHints}}
    diff --git a/templates/items/parts/spell-block.hbs b/templates/items/parts/spell-block.hbs new file mode 100644 index 0000000000..1814f9a7c3 --- /dev/null +++ b/templates/items/parts/spell-block.hbs @@ -0,0 +1,32 @@ +
      +
    • + {{ localize "DND5E.SpellCastTime" }}: + + {{ labels.activation }} + {{#if activation.condition}} + ({{ activation.condition }}) + {{/if}} + +
    • +
    • + {{ localize "DND5E.Range" }}: + {{ labels.range }} +
    • +
    • + {{ localize "DND5E.Target" }}: + {{ labels.target }} +
    • +
    • + {{ localize "DND5E.Components" }}: + + {{ labels.components.vsm }} + {{#if labels.materials}} + ({{ labels.materials }}) + {{/if}} + +
    • +
    • + {{ localize "DND5E.Duration" }}: + {{ labels.concentrationDuration }} +
    • +
    diff --git a/templates/shared/active-effects2.hbs b/templates/shared/active-effects2.hbs index feb5f4b2b0..c4bd636701 100644 --- a/templates/shared/active-effects2.hbs +++ b/templates/shared/active-effects2.hbs @@ -109,6 +109,7 @@ + {{#if hasConditions}}
    @@ -140,5 +141,6 @@
    + {{/if}} diff --git a/templates/shared/inventory2.hbs b/templates/shared/inventory2.hbs index c18106ed1d..1f3486f21b 100644 --- a/templates/shared/inventory2.hbs +++ b/templates/shared/inventory2.hbs @@ -5,6 +5,7 @@
    {{!-- Encumbrance --}} + {{#if isCharacter}}
    {{#with encumbrance}}
    + {{/if}} {{!-- Containers --}}
      @@ -138,7 +140,7 @@ {{!-- Items --}}
    • + data-ungrouped="all" data-grouped="{{ item.type }}" data-uuid="{{ item.uuid }}">
      @@ -188,46 +190,46 @@ {{else}} {{!-- Item Price --}} -
      - {{#if item.system.price.value}} - {{ifThen ctx.concealDetails "—" (dnd5e-numberFormat item.system.price.value)}} - - {{/if}} -
      +
      + {{#if item.system.price.value}} + {{ifThen ctx.concealDetails "—" (dnd5e-numberFormat item.system.price.value)}} + + {{/if}} +
      - {{!-- Item Weight --}} -
      - {{#if ctx.totalWeight}} - {{ ctx.totalWeight }} - {{/if}} -
      + {{!-- Item Weight --}} +
      + {{#if ctx.totalWeight}} + {{ ctx.totalWeight }} + {{/if}} +
      - {{!-- Item Quantity --}} -
      - {{#if @root.owner}} - - - - {{/if}} - - {{#if @root.owner}} - - - - {{/if}} -
      + {{!-- Item Quantity --}} +
      + {{#if @root.owner}} + + + + {{/if}} + + {{#if @root.owner}} + + + + {{/if}} +
      - {{!-- Item Uses --}} -
      - {{#if ctx.hasUses}} - - / - {{ item.system.uses.max }} - {{/if}} -
      + {{!-- Item Uses --}} +
      + {{#if ctx.hasUses}} + + / + {{ item.system.uses.max }} + {{/if}} +
      {{/if}} @@ -306,7 +308,7 @@ - {{#if isNPC}} + {{#if (or isNPC isContainer)}}
      {{!-- Encumbrance --}} @@ -315,13 +317,17 @@
      + {{#if (eq units "Items")}} + + {{else}} + {{/if}} {{ value }} / - {{ max }} + {{#if maxLabel}}{{{ maxLabel }}}{{else}}{{ max }}{{/if}}
      - +
      diff --git a/ui/official/ampersand-gold.svg b/ui/official/ampersand-gold.svg new file mode 100644 index 0000000000..5c40251eed --- /dev/null +++ b/ui/official/ampersand-gold.svg @@ -0,0 +1,2 @@ + +