diff --git a/scss/components/shared/_form_group.scss b/scss/components/shared/_form_group.scss index 9f163ea9..2a3032b3 100644 --- a/scss/components/shared/_form_group.scss +++ b/scss/components/shared/_form_group.scss @@ -4,13 +4,12 @@ * SPDX-License-Identifier: MIT */ -.ds4-form-group, -.ds4-item-sheet .form-group { +.ds4-form-group { clear: both; display: flex; flex-direction: row; flex-wrap: wrap; - margin: 8px 0; + margin: 3px 0; align-items: center; &--start { @@ -25,78 +24,4 @@ flex: 2; line-height: var(--input-height); } - - // Add spacing between form groups - & + & { - margin-top: 12px; - } - - // Style for slim form groups (input + select combinations) - &.slim { - margin: 6px 0; - - .form-fields { - display: flex; - align-items: center; - gap: 4px; - - input { - flex: 1; - min-width: 60px; - } - - select { - flex: 0 0 auto; - min-width: 120px; - width: auto !important; - } - - label { - flex: 0 0 auto; - margin-right: 8px; - font-weight: bold; - } - } - } -} - -// Style standard Foundry form-fields containers -.ds4-item-sheet .form-fields { - display: flex; - align-items: center; - gap: 6px; - flex: 1; - - input, select, textarea { - background: rgba(255, 255, 255, 0.05); - border: 1px solid var(--color-border); - border-radius: 3px; - padding: 4px 8px; - color: var(--color-text-primary); - - &:focus { - border-color: var(--color-text-accent); - box-shadow: 0 0 3px var(--color-text-accent); - } - } - - select { - background: rgba(255, 255, 255, 0.05); - appearance: none; - background-image: url("data:image/svg+xml;charset=US-ASCII,"); - background-repeat: no-repeat; - background-position: right 8px center; - background-size: 8px; - padding-right: 24px; - width: auto !important; - } -} - -// Improve label styling -.ds4-item-sheet .form-group > label { - flex: 0 0 160px; - font-weight: bold; - color: var(--color-form-label); - line-height: var(--input-height); - margin-right: 12px; } diff --git a/scss/components/shared/_sheet_body.scss b/scss/components/shared/_sheet_body.scss index d0bcdd50..0718c6a6 100644 --- a/scss/components/shared/_sheet_body.scss +++ b/scss/components/shared/_sheet_body.scss @@ -4,14 +4,12 @@ * SPDX-License-Identifier: MIT */ -.ds4-sheet-body, -.sheet-body { +.ds4-sheet-body { height: 100%; overflow-y: auto; // Prevent double scrollbars on biography tab - .ds4-sheet-tab.tab.biography.active, - .tab[data-tab="biography"].active { + .ds4-sheet-tab.tab.biography.active { overflow: hidden; } } diff --git a/scss/components/shared/_sheet_tab.scss b/scss/components/shared/_sheet_tab.scss index 3074f121..ea1d13fc 100644 --- a/scss/components/shared/_sheet_tab.scss +++ b/scss/components/shared/_sheet_tab.scss @@ -4,8 +4,7 @@ * SPDX-License-Identifier: MIT */ -.ds4-sheet-tab, -.tab { +.ds4-sheet-tab { flex-direction: column; flex-wrap: nowrap; height: 100%; diff --git a/scss/components/shared/_sheet_tab_nav.scss b/scss/components/shared/_sheet_tab_nav.scss index 1a432568..676fea60 100644 --- a/scss/components/shared/_sheet_tab_nav.scss +++ b/scss/components/shared/_sheet_tab_nav.scss @@ -6,31 +6,20 @@ @use "../../utils/variables"; -.ds4-sheet-tab-nav, -nav.tabs { +.ds4-sheet-tab-nav { border-bottom: variables.$border-groove; border-top: variables.$border-groove; display: flex; flex-wrap: nowrap; - height: calc(2.5 * var(--line-height-16)); + height: calc(2 * var(--line-height-16)); justify-content: space-around; + line-height: calc(2 * var(--line-height-16)); margin: variables.$margin-sm 0; - .ds4-sheet-tab-nav__item, - .item { + &__item { flex: 0 1 auto !important; // necessary to override the styling from lang-de, see https://gitlab.com/henry4k/foundryvtt-lang-de/-/issues/9 font-weight: bold; white-space: nowrap; - display: flex; - align-items: center; - justify-content: center; - gap: 0.25rem; - padding: 0.5rem 0.75rem; - line-height: 1; - - i { - font-size: 0.875rem; - } &.active { text-shadow: 0 0 variables.$padding-md var(--color-shadow-primary); diff --git a/scss/global/_fonts.scss b/scss/global/_fonts.scss index 574bc4cb..d5e7adb4 100644 --- a/scss/global/_fonts.scss +++ b/scss/global/_fonts.scss @@ -59,11 +59,13 @@ --ds4-font-heading: "Wood Stamp", sans-serif; } -// Apply Wood Stamp font only to DS4 sheet-specific elements (excluding window titles) +// Apply Wood Stamp font only to DS4 sheet-specific elements +.ds4-actor-sheet h1, .ds4-actor-sheet h2, .ds4-actor-sheet h4, .ds4-actor-sheet h5, .ds4-actor-sheet h6, +.ds4-item-sheet h1, .ds4-item-sheet h2, .ds4-item-sheet h4, .ds4-item-sheet h5, @@ -74,11 +76,3 @@ text-transform: uppercase; font-weight: 100 !important; } - -// Keep window titles readable with standard font -.ds4-actor-sheet .window-title, -.ds4-item-sheet .window-title { - font-family: var(--font-sans) !important; - text-transform: none !important; - font-weight: normal !important; -} diff --git a/src/apps/actor/base-sheet.js b/src/apps/actor/base-sheet.js index 0d2193ca..285a023a 100644 --- a/src/apps/actor/base-sheet.js +++ b/src/apps/actor/base-sheet.js @@ -40,37 +40,16 @@ export class DS4ActorSheet extends foundry.applications.api.HandlebarsApplicatio deleteEffect: DS4ActorSheet.prototype._onDeleteEffect, changeEffect: DS4ActorSheet.prototype._onChangeEffect, sortItems: DS4ActorSheet.prototype._onSortItems, - + changeTab: DS4ActorSheet.prototype._onChangeTab, editImage: DS4ActorSheet.prototype._onEditImage, }, }; - static TABS = { - primary: { - initial: "values", - tabs: [ - { id: "values", label: "DS4.HeadingValues", icon: "fas fa-chart-bar" }, - { id: "inventory", label: "DS4.HeadingInventory", icon: "fas fa-backpack" }, - { id: "spells", label: "DS4.HeadingSpells", icon: "fas fa-magic" }, - { id: "abilities", label: "DS4.HeadingAbilities", icon: "fas fa-fist-raised" }, - { id: "effects", label: "DS4.HeadingEffects", icon: "fas fa-sparkles" }, - { id: "biography", label: "DS4.HeadingBiography", icon: "fas fa-book" } - ] - } - }; + static TABS = {}; constructor(options = {}) { super(options); - - // Initialize tabGroups with default values - if (!this.tabGroups) { - this.tabGroups = {}; - } - // Set default tab for primary group - if (!this.tabGroups.primary) { - this.tabGroups.primary = this.constructor.TABS.primary?.initial || "values"; - } - + this.activeTab = "values"; // Default active tab } get title() { @@ -146,9 +125,6 @@ export class DS4ActorSheet extends foundry.applications.api.HandlebarsApplicatio // Add tooltips to data this.addTooltipsToData(context); - // Add tabs configuration using ApplicationTab typedef - context.tabs = this._prepareTabs("primary"); - return context; } @@ -542,145 +518,86 @@ export class DS4ActorSheet extends foundry.applications.api.HandlebarsApplicatio * @param {Event} event - The triggering event * @param {HTMLElement} target - The target element */ + async _onChangeTab(event, target) { + event.preventDefault(); + const tab = target.dataset.tab; + if (!tab) return; - /** - * Prepare tabs for a given group using ApplicationTab typedef - * @param {string} group - The tab group identifier - * @returns {Record} Prepared tab data - */ - _prepareTabs(group) { + // Store the active tab + this.activeTab = tab; - const config = this.constructor.TABS[group]; - if (!config) return {}; + // Find tab navigation elements + const nav = target.closest(".ds4-sheet-tab-nav"); + const sheet = this.element.querySelector(".ds4-sheet-body"); - // Ensure tabGroups is initialized - if (!this.tabGroups[group]) { - this.tabGroups[group] = config.initial; + if (!nav || !sheet) return; + // Update navigation active state + nav.querySelectorAll(".ds4-sheet-tab-nav__item").forEach((item) => { + item.classList.remove("active"); + }); + target.classList.add("active"); + + // Update tab content visibility + sheet.querySelectorAll(".ds4-sheet-tab").forEach((tabContent) => { + tabContent.classList.remove("active"); + }); + + const activeTab = sheet.querySelector(`.ds4-sheet-tab[data-tab="${tab}"]`); + if (activeTab) { + activeTab.classList.add("active"); } - - const tabs = {}; - for (const tabConfig of config.tabs) { - const isActive = this.tabGroups[group] === tabConfig.id; - - tabs[tabConfig.id] = { - id: tabConfig.id, - group: group, - icon: tabConfig.icon, - label: game.i18n.localize(tabConfig.label), - active: isActive, - cssClass: isActive ? "active" : "" - }; - } - - - return tabs; } /** @override */ async _onRender(context, options) { - await super._onRender(context, options); - this._initializeTabHandlers(); + + // Initialize first tab as active + this._initializeTabs(); } /** - * Initialize tab event handlers manually since ApplicationV2 doesn't do this automatically + * Initialize tab state - show first tab, hide others */ - _initializeTabHandlers() { - const tabNavigation = this.element.querySelector('.tabs[data-group="primary"]'); - if (!tabNavigation) return; + _initializeTabs() { + const nav = this.element.querySelector(".ds4-sheet-tab-nav"); + const sheet = this.element.querySelector(".ds4-sheet-body"); - // Remove existing event listeners to prevent memory leaks - this._removeTabHandlers(); + if (!nav || !sheet) { + return; + } - // Store tab navigation reference for cleanup - this._tabNavigation = tabNavigation; + // Get all tab navigation items and tab content + const navItems = nav.querySelectorAll(".ds4-sheet-tab-nav__item"); + const tabContents = sheet.querySelectorAll(".ds4-sheet-tab"); - // Create bound handler function for later removal - this._tabClickHandler = (event) => { - event.preventDefault(); - const item = event.target.closest('.item[data-tab]'); - if (!item) return; + // Remove active class from all items first + navItems.forEach((item) => item.classList.remove("active")); + tabContents.forEach((content) => content.classList.remove("active")); - const tabId = item.dataset.tab; - const groupId = item.dataset.group; - if (tabId && groupId) { - this.changeTab(tabId, groupId); + // Find the currently active tab or default to first + let targetTab = this.activeTab; + let targetNavItem = nav.querySelector(`[data-tab="${targetTab}"]`); + + // If stored tab doesn't exist, fall back to first tab + if (!targetNavItem) { + targetNavItem = navItems[0]; + targetTab = targetNavItem?.dataset.tab; + } + + // Set target tab navigation as active + if (targetNavItem && targetTab) { + targetNavItem.classList.add("active"); + + // Set corresponding tab content as active + const activeTabContent = sheet.querySelector(`.ds4-sheet-tab[data-tab="${targetTab}"]`); + if (activeTabContent) { + activeTabContent.classList.add("active"); } - }; - - // Add single delegated event listener to the navigation container - tabNavigation.addEventListener('click', this._tabClickHandler); - } - - /** - * Remove tab event handlers to prevent memory leaks - */ - _removeTabHandlers() { - if (this._tabNavigation && this._tabClickHandler) { - this._tabNavigation.removeEventListener('click', this._tabClickHandler); } } - /** - * Override changeTab to ensure proper tab switching without forced re-render - */ - changeTab(tab, group, options = {}) { - // Call parent changeTab method - const result = super.changeTab(tab, group, options); - - // Update tab display without full re-render - this._updateTabDisplay(tab, group); - - return result; - } - - /** - * Update tab display without full re-render for better performance - */ - _updateTabDisplay(activeTab, group) { - const tabNavigation = this.element.querySelector(`.tabs[data-group="${group}"]`); - const tabContent = this.element.querySelector('.sheet-body'); - - if (!tabNavigation || !tabContent) return; - - // Update navigation active states - const navItems = tabNavigation.querySelectorAll('.item[data-tab]'); - navItems.forEach(item => { - if (item.dataset.tab === activeTab) { - item.classList.add('active'); - } else { - item.classList.remove('active'); - } - }); - - // Update content tab visibility - const contentTabs = tabContent.querySelectorAll('.tab[data-tab]'); - contentTabs.forEach(tab => { - if (tab.dataset.tab === activeTab) { - tab.classList.add('active'); - } else { - tab.classList.remove('active'); - } - }); - } - - /** - * Override _onClose to cleanup event listeners - */ - async _onClose(options) { - this._removeTabHandlers(); - return super._onClose(options); - } - - - - - - - - /** * Handle editing the actor's portrait image * @param {Event} event - The triggering event diff --git a/src/apps/actor/creature-sheet.js b/src/apps/actor/creature-sheet.js index 09fd917d..3c68de8e 100644 --- a/src/apps/actor/creature-sheet.js +++ b/src/apps/actor/creature-sheet.js @@ -14,20 +14,6 @@ export class DS4CreatureActorSheet extends DS4ActorSheet { classes: ["sheet", "ds4-actor-sheet", "ds4-creature-sheet", "themed"], }; - static TABS = { - primary: { - initial: "values", - tabs: [ - { id: "values", label: "DS4.HeadingValues", icon: "fas fa-chart-bar" }, - { id: "inventory", label: "DS4.HeadingInventory", icon: "fas fa-backpack" }, - { id: "spells", label: "DS4.HeadingSpells", icon: "fas fa-magic" }, - { id: "abilities", label: "DS4.HeadingAbilities", icon: "fas fa-fist-raised" }, - { id: "effects", label: "DS4.HeadingEffects", icon: "fas fa-sparkles" }, - { id: "description", label: "DS4.HeadingDescription", icon: "fas fa-file-text" } - ] - } - }; - /** @override */ async _prepareContext(options) { const context = await super._prepareContext(options); @@ -43,9 +29,6 @@ export class DS4CreatureActorSheet extends DS4ActorSheet { ); } - // Add tabs configuration using ApplicationTab typedef - context.tabs = this._prepareTabs("primary"); - return context; } } diff --git a/src/apps/item-sheet.js b/src/apps/item-sheet.js index 76efdabb..8ff85ee7 100644 --- a/src/apps/item-sheet.js +++ b/src/apps/item-sheet.js @@ -30,30 +30,16 @@ export class DS4ItemSheet extends foundry.applications.api.HandlebarsApplication createEffect: DS4ItemSheet.prototype._onCreateEffect, editEffect: DS4ItemSheet.prototype._onEditEffect, deleteEffect: DS4ItemSheet.prototype._onDeleteEffect, + changeTab: DS4ItemSheet.prototype._onChangeTab, editImage: DS4ItemSheet.prototype._onEditImage, }, }; - static TABS = { - primary: { - initial: "description", - tabs: [ - { id: "description", label: "DS4.HeadingDescription", icon: "fas fa-book" }, - { id: "effects", label: "DS4.HeadingEffects", icon: "fas fa-sparkles" } - ] - } - }; + static TABS = {}; constructor(options = {}) { super(options); - // Initialize tabGroups with default values - if (!this.tabGroups) { - this.tabGroups = {}; - } - // Set default tab for primary group - if (!this.tabGroups.primary) { - this.tabGroups.primary = this.constructor.TABS.primary?.initial || "description"; - } + this.activeTab = "description"; // Default active tab } get title() { @@ -98,9 +84,6 @@ export class DS4ItemSheet extends foundry.applications.api.HandlebarsApplication // Add configuration context.config = CONFIG.DS4; - // Add tabs configuration using ApplicationTab typedef - context.tabs = this._prepareTabs("primary"); - // Add enriched effects context.enrichedEffects = []; if (this.item.effects && this.item.effects.size > 0) { @@ -230,7 +213,41 @@ export class DS4ItemSheet extends foundry.applications.api.HandlebarsApplication } } + /** + * Handle tab changes manually for custom tab behavior + * @param {Event} event - The triggering event + * @param {HTMLElement} target - The target element + */ + async _onChangeTab(event, target) { + event.preventDefault(); + const tab = target.dataset.tab; + if (!tab) return; + // Store the active tab + this.activeTab = tab; + + // Find tab navigation elements + const nav = target.closest(".ds4-sheet-tab-nav"); + const sheet = this.element.querySelector(".ds4-sheet-body"); + + if (!nav || !sheet) return; + + // Update navigation active state + nav.querySelectorAll(".ds4-sheet-tab-nav__item").forEach((item) => { + item.classList.remove("active"); + }); + target.classList.add("active"); + + // Update tab content visibility + sheet.querySelectorAll(".ds4-sheet-tab").forEach((tabContent) => { + tabContent.classList.remove("active"); + }); + + const activeTab = sheet.querySelector(`.ds4-sheet-tab[data-tab="${tab}"]`); + if (activeTab) { + activeTab.classList.add("active"); + } + } /** * Handle editing the items's image @@ -252,173 +269,54 @@ export class DS4ItemSheet extends foundry.applications.api.HandlebarsApplication /** @override */ async _onRender(context, options) { await super._onRender(context, options); - this._initializeTabHandlers(); + + // Initialize first tab as active + this._initializeTabs(); + } + + /** @override */ + async _onClose(options) { + await super._onClose(options); } /** - * Initialize tab event handlers manually since ApplicationV2 doesn't do this automatically + * Initialize tab state - show first tab, hide others */ - _initializeTabHandlers() { - const tabNavigation = this.element.querySelector('.tabs[data-group="primary"]'); - if (!tabNavigation) { + _initializeTabs() { + const nav = this.element.querySelector(".ds4-sheet-tab-nav"); + const sheet = this.element.querySelector(".ds4-sheet-body"); + + if (!nav || !sheet) { return; } - // Remove existing event listeners to prevent memory leaks - this._removeTabHandlers(); + // Get all tab navigation items and tab content + const navItems = nav.querySelectorAll(".ds4-sheet-tab-nav__item"); + const tabContents = sheet.querySelectorAll(".ds4-sheet-tab"); - // Store tab navigation reference for cleanup - this._tabNavigation = tabNavigation; + // Remove active class from all items first + navItems.forEach((item) => item.classList.remove("active")); + tabContents.forEach((content) => content.classList.remove("active")); - // Create bound handler function for later removal - this._tabClickHandler = (event) => { - event.preventDefault(); - const item = event.target.closest('.item[data-tab]'); - if (!item) { - return; - } + // Find the currently active tab or default to first + let targetTab = this.activeTab; + let targetNavItem = nav.querySelector(`[data-tab="${targetTab}"]`); - const tabId = item.dataset.tab; - const groupId = item.dataset.group; - if (tabId && groupId) { - this.changeTab(tabId, groupId); - } - }; - - // Add single delegated event listener to the navigation container - tabNavigation.addEventListener('click', this._tabClickHandler); - } - - /** - * Override form submission to prevent re-rendering - */ - async _processSubmitData(event, form, submitData, options = {}) { - // Always prevent re-rendering on form updates - options.render = false; - return super._processSubmitData(event, form, submitData, options); - } - - /** - * Remove tab event handlers to prevent memory leaks - */ - _removeTabHandlers() { - if (this._tabNavigation && this._tabClickHandler) { - this._tabNavigation.removeEventListener('click', this._tabClickHandler); - } - } - - /** - * Override changeTab to ensure proper tab switching without forced re-render - */ - changeTab(tab, group, options = {}) { - // Call parent changeTab method - const result = super.changeTab(tab, group, options); - - // Update tab display without full re-render - this._updateTabDisplay(tab, group); - - return result; - } - - /** - * Update tab display without full re-render for better performance - */ - _updateTabDisplay(activeTab, group) { - const tabNavigation = this.element.querySelector(`.tabs[data-group="${group}"]`); - const tabContent = this.element.querySelector('.sheet-body'); - - if (!tabNavigation || !tabContent) return; - - // Update navigation active states - const navItems = tabNavigation.querySelectorAll('.item[data-tab]'); - navItems.forEach(item => { - if (item.dataset.tab === activeTab) { - item.classList.add('active'); - } else { - item.classList.remove('active'); - } - }); - - // Update content tab visibility - const contentTabs = tabContent.querySelectorAll('.tab[data-tab]'); - contentTabs.forEach(tab => { - if (tab.dataset.tab === activeTab) { - tab.classList.add('active'); - } else { - tab.classList.remove('active'); - } - }); - } - - /** - * Override _onClose to cleanup event listeners - */ - async _onClose(options) { - this._removeTabHandlers(); - return super._onClose(options); - } - - /** - * Prepare tabs for a given group using ApplicationTab typedef - * @param {string} group - The tab group identifier - * @returns {Record} Prepared tab data - */ - _prepareTabs(group) { - const config = this._getTabsConfigForItemType(); - if (!config) return {}; - - // Ensure tabGroups is initialized - if (!this.tabGroups[group]) { - this.tabGroups[group] = config.initial || "description"; + // If stored tab doesn't exist, fall back to first tab + if (!targetNavItem) { + targetNavItem = navItems[0]; + targetTab = targetNavItem?.dataset.tab; } - const tabs = {}; - for (const tabConfig of config.tabs) { - const isActive = this.tabGroups[group] === tabConfig.id; - tabs[tabConfig.id] = { - id: tabConfig.id, - group: group, - icon: tabConfig.icon, - label: game.i18n.localize(tabConfig.label), - active: isActive, - cssClass: isActive ? "active" : "" - }; + // Set target tab navigation as active + if (targetNavItem && targetTab) { + targetNavItem.classList.add("active"); + + // Set corresponding tab content as active + const activeTabContent = sheet.querySelector(`.ds4-sheet-tab[data-tab="${targetTab}"]`); + if (activeTabContent) { + activeTabContent.classList.add("active"); + } } - - return tabs; } - - /** - * Get tab configuration based on item type - * @returns {Object} Tab configuration for this item type - */ - _getTabsConfigForItemType() { - const tabs = [ - { id: "description", label: "DS4.HeadingDescription", icon: "fas fa-book" } - ]; - - // Item types that have properties tabs - const itemsWithProperties = [ - "weapon", "armor", "shield", "equipment", "loot", - "spell", "talent", "specialCreatureAbility" - ]; - - if (itemsWithProperties.includes(this.item.type)) { - tabs.push({ - id: "properties", - label: "DS4.HeadingProperties", - icon: "fas fa-cogs" - }); - } - - // All items can have effects - tabs.push({ - id: "effects", - label: "DS4.HeadingEffects", - icon: "fas fa-sparkles" - }); - - return { tabs, initial: "description" }; - } - } diff --git a/templates/sheets/actor/character-sheet.hbs b/templates/sheets/actor/character-sheet.hbs index d725c796..33322d79 100644 --- a/templates/sheets/actor/character-sheet.hbs +++ b/templates/sheets/actor/character-sheet.hbs @@ -13,47 +13,38 @@ SPDX-License-Identifier: MIT {{/systems/ds4/templates/sheets/actor/components/actor-header.hbs}} {{!-- Sheet Tab Navigation --}} -