implement custom hotbar card view for Daggerheart items with dynamic tooltips
This commit is contained in:
commit
373eb12a6d
5 changed files with 359 additions and 0 deletions
24
README.md
Normal file
24
README.md
Normal file
|
|
@ -0,0 +1,24 @@
|
||||||
|
# Daggerheart Hotbar Card View
|
||||||
|
|
||||||
|
A Foundry VTT module for the Daggerheart system that enhances the hotbar experience. This module replaces the standard, simple hover-tooltips on the hotbar icons with a full, visually appealing playing card view of the item, making it much easier to read abilities, spells, and items at a glance.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Fully stylized playing card view for hotbar items.
|
||||||
|
- Dynamic color support: seamlessly integrates with both light and dark modes within the Foundry VTT UI.
|
||||||
|
- Properly displays the item's level, cost, domains, and full descriptions without clipping.
|
||||||
|
|
||||||
|
## Installation
|
||||||
|
|
||||||
|
You can install this module by pasting the following link into the **Manifest URL** field in the Foundry VTT Module installation menu:
|
||||||
|
|
||||||
|
```text
|
||||||
|
https://git.geeks.gay/cosmo/dh-hotbar-cardview/raw/branch/main/module.json
|
||||||
|
```
|
||||||
|
|
||||||
|
Alternatively, you can download the [zip archive directly](https://git.geeks.gay/cosmo/dh-hotbar-cardview/releases/latest/download/dh-hotbar-cardview.zip).
|
||||||
|
|
||||||
|
## Compatibility
|
||||||
|
|
||||||
|
- **Foundry VTT**: V14+
|
||||||
|
- **System**: Daggerheart (v2.1.0+)
|
||||||
38
module.json
Normal file
38
module.json
Normal file
|
|
@ -0,0 +1,38 @@
|
||||||
|
{
|
||||||
|
"id": "dh-hotbar-cardview",
|
||||||
|
"title": "Daggerheart Hotbar Card View",
|
||||||
|
"description": "Replaces the hover-tooltips on the hotbar icons with a full card view of the item for Daggerheart.",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"compatibility": {
|
||||||
|
"minimum": "14",
|
||||||
|
"verified": "14"
|
||||||
|
},
|
||||||
|
"authors": [
|
||||||
|
{
|
||||||
|
"name": "Cosmo",
|
||||||
|
"email": "cptncosmo@gmail.com",
|
||||||
|
"url": "https://git.geeks.gay/cosmo/",
|
||||||
|
"discord": "@cptn_cosmo"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"relationships": {
|
||||||
|
"systems": [
|
||||||
|
{
|
||||||
|
"id": "daggerheart",
|
||||||
|
"type": "system",
|
||||||
|
"compatibility": {
|
||||||
|
"minimum": "2.1.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"esmodules": [
|
||||||
|
"scripts/main.js"
|
||||||
|
],
|
||||||
|
"styles": [
|
||||||
|
"styles/cardview.css"
|
||||||
|
],
|
||||||
|
"url": "https://git.geeks.gay/cosmo/dh-hotbar-cardview",
|
||||||
|
"manifest": "https://git.geeks.gay/cosmo/dh-hotbar-cardview/raw/branch/main/module.json",
|
||||||
|
"download": "https://git.geeks.gay/cosmo/dh-hotbar-cardview/releases/latest/download/dh-hotbar-cardview.zip"
|
||||||
|
}
|
||||||
84
scripts/main.js
Normal file
84
scripts/main.js
Normal file
|
|
@ -0,0 +1,84 @@
|
||||||
|
Hooks.once('init', () => {
|
||||||
|
const originalActivate = CONFIG.ux.TooltipManager.prototype.activate;
|
||||||
|
|
||||||
|
CONFIG.ux.TooltipManager.prototype.activate = async function(element, options = {}) {
|
||||||
|
if (element.dataset.tooltip?.startsWith('#dhcard#')) {
|
||||||
|
const uuid = element.dataset.tooltip.slice(8);
|
||||||
|
const item = await fromUuid(uuid);
|
||||||
|
if (item) {
|
||||||
|
// Use Daggerheart's enrichText to prepare the item's descriptions
|
||||||
|
if (typeof this.enrichText === 'function') {
|
||||||
|
await this.enrichText(item);
|
||||||
|
}
|
||||||
|
|
||||||
|
options.html = await renderTemplate('modules/dh-hotbar-cardview/templates/cardview.hbs', {
|
||||||
|
item: item,
|
||||||
|
description: item.system?.enrichedDescription ?? item.enrichedDescription ?? item.system?.description,
|
||||||
|
config: CONFIG.DH,
|
||||||
|
allDomains: CONFIG.DH.DOMAIN?.allDomains ? CONFIG.DH.DOMAIN.allDomains() : {}
|
||||||
|
});
|
||||||
|
|
||||||
|
options.direction = this.constructor.TOOLTIP_DIRECTIONS.UP;
|
||||||
|
options.cssClass = 'dh-hotbar-card-tooltip';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return originalActivate.call(this, element, options);
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
// Use a MutationObserver to aggressively update the hotbar tooltips as soon as the DOM changes.
|
||||||
|
// This ensures that the dataset is ready BEFORE the user hovers for the first time.
|
||||||
|
Hooks.once('ready', () => {
|
||||||
|
function processHotbar() {
|
||||||
|
const hotbarEls = document.querySelectorAll('#hotbar, .hotbar, [id^="hotbar"]');
|
||||||
|
for (const hotbarEl of hotbarEls) {
|
||||||
|
const slots = hotbarEl.querySelectorAll('[data-slot]');
|
||||||
|
for (const slotEl of slots) {
|
||||||
|
const slot = slotEl.dataset.slot;
|
||||||
|
if (!slot) continue;
|
||||||
|
|
||||||
|
const macroId = slotEl.dataset.macroId || game.user.hotbar[slot];
|
||||||
|
if (!macroId) continue;
|
||||||
|
|
||||||
|
const macro = game.macros.get(macroId);
|
||||||
|
if (macro && macro.command) {
|
||||||
|
const match = macro.command.match(/useItem\s*\(\s*["']([^"']+)["']\s*\)/);
|
||||||
|
if (match) {
|
||||||
|
const uuid = match[1];
|
||||||
|
const tooltipText = `#dhcard#${uuid}`;
|
||||||
|
|
||||||
|
// Overwrite the tooltip attribute on the slot and all inner elements
|
||||||
|
slotEl.dataset.tooltip = tooltipText;
|
||||||
|
const innerTooltips = slotEl.querySelectorAll('[data-tooltip]');
|
||||||
|
for (const inner of innerTooltips) {
|
||||||
|
inner.dataset.tooltip = tooltipText;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Process immediately
|
||||||
|
processHotbar();
|
||||||
|
|
||||||
|
// Re-process on hotbar changes
|
||||||
|
Hooks.on('updateUser', processHotbar);
|
||||||
|
Hooks.on('updateMacro', processHotbar);
|
||||||
|
Hooks.on('renderDhHotbar', processHotbar);
|
||||||
|
|
||||||
|
// Watch for any dynamic DOM changes inside the hotbar container
|
||||||
|
const observer = new MutationObserver((mutations) => {
|
||||||
|
let shouldProcess = false;
|
||||||
|
for (const mutation of mutations) {
|
||||||
|
if (mutation.addedNodes.length > 0 || mutation.type === 'attributes') {
|
||||||
|
shouldProcess = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (shouldProcess) processHotbar();
|
||||||
|
});
|
||||||
|
|
||||||
|
const hotbarWrapper = document.getElementById('ui-bottom') || document.body;
|
||||||
|
observer.observe(hotbarWrapper, { childList: true, subtree: true, attributes: true, attributeFilter: ['data-slot', 'data-macro-id'] });
|
||||||
|
});
|
||||||
179
styles/cardview.css
Normal file
179
styles/cardview.css
Normal file
|
|
@ -0,0 +1,179 @@
|
||||||
|
:root {
|
||||||
|
/* Use standard Foundry/System tooltip background, falling back to dark */
|
||||||
|
--dh-card-bg: var(--color-background-tooltip, rgba(0, 0, 0, 0.9));
|
||||||
|
--dh-card-text: var(--color-text-light-primary, #eeeeee);
|
||||||
|
/* Daggerheart gold */
|
||||||
|
--dh-card-title: var(--color-primary, #f2cf5b);
|
||||||
|
--dh-card-border: #444;
|
||||||
|
--dh-card-banner-border: #222;
|
||||||
|
}
|
||||||
|
|
||||||
|
.theme-dark,
|
||||||
|
.dark-mode {
|
||||||
|
--dh-card-bg: var(--color-background-tooltip, #1a1a1a);
|
||||||
|
--dh-card-text: var(--color-text-light-primary, #dddddd);
|
||||||
|
--dh-card-title: var(--color-primary, #f2cf5b);
|
||||||
|
--dh-card-border: #444;
|
||||||
|
--dh-card-banner-border: #222;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Base Tooltip Override */
|
||||||
|
.tooltip.dh-hotbar-card-tooltip,
|
||||||
|
#tooltip.dh-hotbar-card-tooltip,
|
||||||
|
.dh-hotbar-card-tooltip {
|
||||||
|
background: transparent !important;
|
||||||
|
border: none !important;
|
||||||
|
box-shadow: none !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
margin-bottom: 12px !important; /* Slightly increase distance to the hotbar */
|
||||||
|
max-width: none !important;
|
||||||
|
white-space: normal !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dh-hotbar-card {
|
||||||
|
width: 360px;
|
||||||
|
min-height: 504px; /* ~1:1.4 aspect ratio for playing card */
|
||||||
|
background: var(--dh-card-bg);
|
||||||
|
border: 2px solid var(--dh-card-border);
|
||||||
|
border-radius: 12px;
|
||||||
|
overflow: hidden;
|
||||||
|
color: var(--dh-card-text);
|
||||||
|
font-family: 'Signika', 'Helvetica Neue', sans-serif;
|
||||||
|
box-shadow: 0 8px 24px rgba(0,0,0,0.8);
|
||||||
|
text-align: left;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
white-space: normal;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dh-hotbar-card .card-art-container {
|
||||||
|
position: relative;
|
||||||
|
width: 100%;
|
||||||
|
height: 180px;
|
||||||
|
border-bottom: 4px solid #f2cf5b; /* Gold line */
|
||||||
|
background: #333;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
overflow: visible; /* To allow the banner to overlap */
|
||||||
|
}
|
||||||
|
|
||||||
|
.dh-hotbar-card .card-art {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
object-fit: cover;
|
||||||
|
object-position: top;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dh-hotbar-card .card-ribbon {
|
||||||
|
position: absolute;
|
||||||
|
top: -2px;
|
||||||
|
left: 20px;
|
||||||
|
width: 44px;
|
||||||
|
height: 80px;
|
||||||
|
background: #1a1a1a;
|
||||||
|
border: 2px solid #f2cf5b;
|
||||||
|
border-top: none;
|
||||||
|
clip-path: polygon(0 0, 100% 0, 100% 100%, 50% 85%, 0 100%);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
padding-top: 5px;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dh-hotbar-card .card-ribbon .level {
|
||||||
|
font-size: 22px;
|
||||||
|
font-weight: 800;
|
||||||
|
color: white;
|
||||||
|
margin-bottom: 2px;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dh-hotbar-card .card-ribbon img {
|
||||||
|
width: 28px;
|
||||||
|
height: 28px;
|
||||||
|
/* brightness(0) invert(1) makes the icon white */
|
||||||
|
filter: brightness(0) invert(1) drop-shadow(0 0 2px rgba(255,255,255,0.3));
|
||||||
|
border: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dh-hotbar-card .card-cost {
|
||||||
|
position: absolute;
|
||||||
|
top: 15px;
|
||||||
|
right: 15px;
|
||||||
|
width: 40px;
|
||||||
|
height: 40px;
|
||||||
|
background: #1a1a1a;
|
||||||
|
border: 2px solid #f2cf5b;
|
||||||
|
border-radius: 50%;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
color: white;
|
||||||
|
font-weight: 800;
|
||||||
|
font-size: 18px;
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dh-hotbar-card .card-cost i {
|
||||||
|
font-size: 12px;
|
||||||
|
margin-left: 2px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dh-hotbar-card .card-type-banner {
|
||||||
|
position: absolute;
|
||||||
|
bottom: -14px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
background: #1a1a1a;
|
||||||
|
color: white;
|
||||||
|
font-weight: 800;
|
||||||
|
font-size: 14px;
|
||||||
|
letter-spacing: 1px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
padding: 4px 24px;
|
||||||
|
clip-path: polygon(10px 0, calc(100% - 10px) 0, 100% 50%, calc(100% - 10px) 100%, 10px 100%, 0 50%);
|
||||||
|
z-index: 10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dh-hotbar-card .card-type-banner.ancestry {
|
||||||
|
left: auto;
|
||||||
|
right: 20px;
|
||||||
|
transform: none;
|
||||||
|
background: #f2cf5b;
|
||||||
|
color: #000;
|
||||||
|
border: 2px solid var(--dh-card-banner-border);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Adjust layout so content doesn't collide with the banner */
|
||||||
|
.dh-hotbar-card .card-content {
|
||||||
|
padding: 24px 20px 20px 20px;
|
||||||
|
background: var(--dh-card-bg);
|
||||||
|
flex-grow: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dh-hotbar-card .card-title {
|
||||||
|
font-family: 'Cinzel', serif;
|
||||||
|
font-size: 28px;
|
||||||
|
font-weight: 900;
|
||||||
|
margin: 0 0 10px 0;
|
||||||
|
color: var(--dh-card-title);
|
||||||
|
text-transform: uppercase;
|
||||||
|
border: none;
|
||||||
|
line-height: 1.1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dh-hotbar-card .card-description {
|
||||||
|
font-size: 14px;
|
||||||
|
line-height: 1.4;
|
||||||
|
color: var(--dh-card-text);
|
||||||
|
}
|
||||||
|
|
||||||
|
.dh-hotbar-card .card-description p {
|
||||||
|
margin: 0 0 8px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dh-hotbar-card .card-description strong {
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
34
templates/cardview.hbs
Normal file
34
templates/cardview.hbs
Normal file
|
|
@ -0,0 +1,34 @@
|
||||||
|
<div class="dh-hotbar-card">
|
||||||
|
<div class="card-art-container">
|
||||||
|
<img class="card-art" src="{{item.img}}" alt="{{item.name}}">
|
||||||
|
|
||||||
|
{{#if (eq item.type "domainCard")}}
|
||||||
|
<div class="card-ribbon">
|
||||||
|
<span class="level">{{item.system.level}}</span>
|
||||||
|
{{#with (lookup allDomains item.system.domain) as | domain |}}
|
||||||
|
<img src="{{domain.src}}" alt="{{domain.label}}">
|
||||||
|
{{/with}}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-cost">
|
||||||
|
<span>{{item.system.recallCost}}</span>
|
||||||
|
<i class="fa-solid fa-bolt"></i>
|
||||||
|
</div>
|
||||||
|
{{/if}}
|
||||||
|
|
||||||
|
<div class="card-type-banner {{item.type}}">
|
||||||
|
{{#if item.system.type}}
|
||||||
|
{{#with (lookup config.DOMAIN.cardTypes item.system.type) as | cardType |}}
|
||||||
|
{{localize cardType.label}}
|
||||||
|
{{/with}}
|
||||||
|
{{else}}
|
||||||
|
{{localize (concat 'TYPES.Item.' item.type)}}
|
||||||
|
{{/if}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-content">
|
||||||
|
<h2 class="card-title">{{item.name}}</h2>
|
||||||
|
<div class="card-description">{{{description}}}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
Loading…
Add table
Add a link
Reference in a new issue