Compare commits

...

4 Commits

Author SHA1 Message Date
Disco DeDisco
db9ac9cb24 GAME KIT: DON|DOFF equip system — portal tooltips, kit bag sync, btn-disabled fix
Some checks failed
ci/woodpecker/push/pyswiss Pipeline was successful
ci/woodpecker/push/main Pipeline failed
- DON/DOFF buttons on left edge of game kit applet portal tooltip (mirroring FLIP/FYI)
- equip-trinket/unequip-trinket/equip-deck/unequip-deck views + URLs
- Portal stays open after DON/DOFF; buttons swap state in-place (_setEquipState)
- _syncTokenButtons: updates all .tt DON/DOFF buttons after equip state change
- _syncKitBagDialog (DOFF): replaces card with grayed placeholder icon in-place
- _refreshKitDialog (DON): re-fetches kit content so newly-equipped card appears immediately
- kit-content-refreshed event: game-kit.js re-attaches card listeners after re-fetch
- Bounding box expanded 24px left so buttons at portal edge don't trigger close
- mini-portal pinned with right (not left) so text width changes grow/shrink leftward
- btn-disabled moved dead last in .btn block — wins by source order, no !important needed
- Kit bag panel: trinket + token sections always render (placeholder when empty)
- Backstage Pass in GameKitEquipTest setUp (is_staff, natural unequipped state)
- Portal padding 0.75rem / 1.5rem; tt-description/shoptalk smaller; tt-expiry --priRd
- Wallet tokens CSS hover rule for .tt removed (portal-only now)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-16 00:14:47 -04:00
Disco DeDisco
d3e4638233 TOOLTIPS: wallet tokens refactor — .token-tooltip → .tt + child classes
- _applet-wallet-tokens.html: all 6 token blocks (pass, coin, free×2, tithe×2)
  updated to .tt wrapper + .tt-title / .tt-description / .tt-shoptalk / .tt-expiry
  child classes; small→p.tt-shoptalk, p.expiry→p.tt-expiry
- wallet.js: querySelector('.token-tooltip') → querySelector('.tt') in initWalletTooltips

13 FTs green (trinket_carte_blanche + game_kit + gameboard)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-15 22:42:37 -04:00
Disco DeDisco
10a6809dcf TOOLTIPS: game kit applet refactor — .token-tooltip → .tt + double-tooltip fix
- _applet-game-kit.html: all 5 token blocks use .tt + child classes (.tt-title,
  .tt-description, .tt-shoptalk, .tt-expiry); removed .token-tooltip-body wrapper
- gameboard.js: 4× querySelector('.token-tooltip') → querySelector('.tt')
- _gameboard.scss: extend hover suppressor to .tt so CSS hover doesn't show inline
  .tt when JS portal is active (fixed double tooltip visual bug)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-15 22:39:01 -04:00
Disco DeDisco
de4ac60aec tooltips app TDD spike + kit bag refactor to .tt
- New apps.tooltips: TooltipContent model, {% tooltip data %} inclusion
  tag, _tooltip.html partial with .tt/.tt-title/.tt-description etc.
  class contract; 34 tests green
- Kit bag panel (_kit_bag_panel.html): .token-tooltip → .tt + child
  class renames (tt-title, tt-description, tt-shoptalk, tt-expiry)
- game-kit.js attachTooltip: .token-tooltip → .tt selector
- SCSS: .tt added alongside .token-tooltip for display:none default +
  hover rules in _wallet-tokens.scss and _game-kit.scss

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-15 22:16:50 -04:00
29 changed files with 1087 additions and 204 deletions

View File

@@ -60,7 +60,7 @@
function attachTooltip(el) { function attachTooltip(el) {
el.addEventListener('mouseenter', function () { el.addEventListener('mouseenter', function () {
var tooltip = el.querySelector('.token-tooltip'); var tooltip = el.querySelector('.tt');
if (!tooltip) return; if (!tooltip) return;
var rect = el.getBoundingClientRect(); var rect = el.getBoundingClientRect();
tooltip.style.position = 'fixed'; tooltip.style.position = 'fixed';
@@ -69,11 +69,16 @@
tooltip.style.display = 'block'; tooltip.style.display = 'block';
}); });
el.addEventListener('mouseleave', function () { el.addEventListener('mouseleave', function () {
var tooltip = el.querySelector('.token-tooltip'); var tooltip = el.querySelector('.tt');
if (tooltip) tooltip.style.display = ''; if (tooltip) tooltip.style.display = '';
}); });
} }
// gameboard.js re-fetches dialog content after DON and fires this event.
dialog.addEventListener('kit-content-refreshed', function () {
attachCardListeners();
});
function attachCardListeners() { function attachCardListeners() {
dialog.querySelectorAll('.token[data-token-id]').forEach(function (card) { dialog.querySelectorAll('.token[data-token-id]').forEach(function (card) {
card.addEventListener('click', function () { card.addEventListener('click', function () {

View File

@@ -69,7 +69,7 @@ function initWalletTooltips() {
if (!portal) return; if (!portal) return;
document.querySelectorAll('.wallet-tokens .token').forEach(token => { document.querySelectorAll('.wallet-tokens .token').forEach(token => {
const tooltip = token.querySelector('.token-tooltip'); const tooltip = token.querySelector('.tt');
if (!tooltip) return; if (!tooltip) return;
token.addEventListener('mouseenter', () => { token.addEventListener('mouseenter', () => {

View File

@@ -14,7 +14,6 @@ function initGameKitTooltips() {
portal.style.display = 'none'; portal.style.display = 'none';
miniPortal.style.display = 'none'; miniPortal.style.display = 'none';
let equippedId = gameKit.dataset.equippedId || '';
let activeToken = null; let activeToken = null;
let equipping = false; let equipping = false;
@@ -32,7 +31,16 @@ function initGameKitTooltips() {
document.addEventListener('mousemove', (e) => { document.addEventListener('mousemove', (e) => {
if (portal.classList.contains('active') && activeToken) { if (portal.classList.contains('active') && activeToken) {
const rects = [activeToken.getBoundingClientRect(), portal.getBoundingClientRect()]; const tokenRect = activeToken.getBoundingClientRect();
const portalRect = portal.getBoundingClientRect();
// Expand left to cover button overflow outside portal edge
const expandedPortalRect = {
left: portalRect.left - 24,
top: portalRect.top,
right: portalRect.right,
bottom: portalRect.bottom,
};
const rects = [tokenRect, expandedPortalRect];
if (miniPortal.classList.contains('active')) rects.push(miniPortal.getBoundingClientRect()); if (miniPortal.classList.contains('active')) rects.push(miniPortal.getBoundingClientRect());
const left = Math.min(...rects.map(r => r.left)); const left = Math.min(...rects.map(r => r.left));
const top = Math.min(...rects.map(r => r.top)); const top = Math.min(...rects.map(r => r.top));
@@ -41,7 +49,7 @@ function initGameKitTooltips() {
if (!inRect(e.clientX, e.clientY, { left, top, right, bottom })) closePortals(); if (!inRect(e.clientX, e.clientY, { left, top, right, bottom })) closePortals();
} else if (!portal.classList.contains('active')) { } else if (!portal.classList.contains('active')) {
for (const tokenEl of gameKit.querySelectorAll('.token')) { for (const tokenEl of gameKit.querySelectorAll('.token')) {
if (!tokenEl.querySelector('.token-tooltip')) continue; if (!tokenEl.querySelector('.tt')) continue;
if (inRect(e.clientX, e.clientY, tokenEl.getBoundingClientRect())) { if (inRect(e.clientX, e.clientY, tokenEl.getBoundingClientRect())) {
showPortals(tokenEl); showPortals(tokenEl);
break; break;
@@ -50,73 +58,161 @@ function initGameKitTooltips() {
} }
}); });
// buildMiniContent takes the full element so it can inspect data-deck-id vs data-token-id. // buildMiniContent — text-only status; DON/DOFF buttons live in the main portal.
function buildMiniContent(token) { function buildMiniContent(token) {
const deckId = token.dataset.deckId; const deckId = token.dataset.deckId;
const tokenId = token.dataset.tokenId; const tokenId = token.dataset.tokenId;
const equippedId = gameKit.dataset.equippedId || '';
const equippedDeckId = gameKit.dataset.equippedDeckId || '';
if (deckId) { if (deckId) {
const equippedDeckId = gameKit.dataset.equippedDeckId || ''; miniPortal.textContent = (equippedDeckId && deckId === equippedDeckId) ? 'Equipped' : 'Not Equipped';
if (equippedDeckId && deckId === equippedDeckId) {
miniPortal.textContent = 'Equipped';
} else {
const btn = document.createElement('button');
btn.className = 'equip-deck-btn';
btn.textContent = 'Equip Deck?';
btn.addEventListener('click', (e) => {
e.stopPropagation();
equipping = true;
gameKit.dataset.equippedDeckId = deckId;
fetch(`/gameboard/equip-deck/${deckId}/`, {
method: 'POST',
headers: {'X-CSRFToken': getCsrfToken()},
}).then(r => {
if (r.ok && equipping) {
equipping = false;
closePortals();
} else {
equipping = false;
}
});
});
miniPortal.innerHTML = '';
miniPortal.appendChild(btn);
}
} else if (tokenId) { } else if (tokenId) {
if (equippedId && tokenId === equippedId) { miniPortal.textContent = (equippedId && tokenId === equippedId) ? 'Equipped' : 'Not Equipped';
miniPortal.textContent = 'Equipped';
} else {
const btn = document.createElement('button');
btn.className = 'equip-trinket-btn';
btn.dataset.tokenId = tokenId;
btn.textContent = 'Equip Trinket?';
btn.addEventListener('click', (e) => {
e.stopPropagation();
equipping = true;
equippedId = tokenId;
gameKit.dataset.equippedId = equippedId;
fetch(`/gameboard/equip-trinket/${tokenId}/`, {
method: 'POST',
headers: {'X-CSRFToken': getCsrfToken()},
}).then(r => {
if (r.ok && equipping) {
equipping = false;
closePortals();
} else {
equipping = false;
}
});
});
miniPortal.innerHTML = '';
miniPortal.appendChild(btn);
}
} }
} }
// Update DON/DOFF button pair to reflect new equipped state.
function _setEquipState(donBtn, doffBtn, isEquipped) {
if (isEquipped) {
donBtn.classList.add('btn-disabled'); donBtn.textContent = '×';
doffBtn.classList.remove('btn-disabled'); doffBtn.textContent = 'DOFF';
} else {
donBtn.classList.remove('btn-disabled'); donBtn.textContent = 'DON';
doffBtn.classList.add('btn-disabled'); doffBtn.textContent = '×';
}
}
// Sync all tokens' .tt DON/DOFF buttons after an equip state change.
function _syncTokenButtons(kind, newEquippedId) {
const selector = kind === 'deck' ? '[data-deck-id]' : '[data-token-id]';
gameKit.querySelectorAll('.token' + selector).forEach(tokenEl => {
const id = kind === 'deck' ? tokenEl.dataset.deckId : tokenEl.dataset.tokenId;
const tt = tokenEl.querySelector('.tt');
if (!tt) return;
const don = tt.querySelector('.btn-equip');
const doff = tt.querySelector('.btn-unequip');
if (!don || !doff) return;
_setEquipState(don, doff, id === newEquippedId);
});
}
// If the kit bag dialog is open, re-fetch its content (used after DON so the
// newly-equipped card appears without the user having to close+reopen).
function _refreshKitDialog() {
const dialog = document.getElementById('id_kit_bag_dialog');
const kitBtn = document.getElementById('id_kit_btn');
if (!dialog || !dialog.hasAttribute('open') || !kitBtn || !kitBtn.dataset.kitUrl) return;
fetch(kitBtn.dataset.kitUrl, {
headers: { 'X-Requested-With': 'XMLHttpRequest' },
})
.then(r => r.text())
.then(html => {
dialog.innerHTML = html;
dialog.dispatchEvent(new CustomEvent('kit-content-refreshed'));
});
}
// If the kit bag dialog is open, replace the matching card with a placeholder.
function _syncKitBagDialog(kind, id) {
const dialog = document.getElementById('id_kit_bag_dialog');
if (!dialog || !dialog.hasAttribute('open')) return;
const selector = kind === 'deck'
? `.kit-bag-deck[data-deck-id="${id}"]`
: `.token[data-token-id="${id}"]`;
const card = dialog.querySelector(selector);
if (!card) return;
const placeholder = document.createElement('div');
placeholder.className = 'kit-bag-placeholder';
const icon = card.querySelector('i');
if (icon) placeholder.innerHTML = icon.outerHTML;
card.parentNode.insertBefore(placeholder, card);
card.remove();
}
function wireDonDoff(token) {
const donBtn = portal.querySelector('.btn-equip');
const doffBtn = portal.querySelector('.btn-unequip');
if (!donBtn || !doffBtn) return;
donBtn.addEventListener('click', (e) => {
e.stopPropagation();
if (donBtn.classList.contains('btn-disabled') || equipping) return;
equipping = true;
const tokenId = donBtn.dataset.tokenId;
const deckId = donBtn.dataset.deckId;
if (tokenId) {
gameKit.dataset.equippedId = tokenId;
fetch(`/gameboard/equip-trinket/${tokenId}/`, {
method: 'POST',
headers: {'X-CSRFToken': getCsrfToken()},
}).then(r => {
equipping = false;
if (r.ok) {
_setEquipState(donBtn, doffBtn, true);
_syncTokenButtons('trinket', tokenId);
buildMiniContent(token);
_refreshKitDialog();
}
});
} else if (deckId) {
gameKit.dataset.equippedDeckId = deckId;
fetch(`/gameboard/equip-deck/${deckId}/`, {
method: 'POST',
headers: {'X-CSRFToken': getCsrfToken()},
}).then(r => {
equipping = false;
if (r.ok) {
_setEquipState(donBtn, doffBtn, true);
_syncTokenButtons('deck', deckId);
buildMiniContent(token);
_refreshKitDialog();
}
});
}
});
doffBtn.addEventListener('click', (e) => {
e.stopPropagation();
if (doffBtn.classList.contains('btn-disabled') || equipping) return;
equipping = true;
const tokenId = doffBtn.dataset.tokenId;
const deckId = doffBtn.dataset.deckId;
if (tokenId) {
gameKit.dataset.equippedId = '';
fetch(`/gameboard/unequip-trinket/${tokenId}/`, {
method: 'POST',
headers: {'X-CSRFToken': getCsrfToken()},
}).then(r => {
equipping = false;
if (r.ok) {
_setEquipState(donBtn, doffBtn, false);
_syncTokenButtons('trinket', '');
buildMiniContent(token);
_syncKitBagDialog('token', tokenId);
}
});
} else if (deckId) {
gameKit.dataset.equippedDeckId = '';
fetch(`/gameboard/unequip-deck/${deckId}/`, {
method: 'POST',
headers: {'X-CSRFToken': getCsrfToken()},
}).then(r => {
equipping = false;
if (r.ok) {
_setEquipState(donBtn, doffBtn, false);
_syncTokenButtons('deck', '');
buildMiniContent(token);
_syncKitBagDialog('deck', deckId);
}
});
}
});
}
function showPortals(token) { function showPortals(token) {
equipping = false; equipping = false;
activeToken = token; activeToken = token;
const tooltip = token.querySelector('.token-tooltip'); const tooltip = token.querySelector('.tt');
portal.innerHTML = tooltip.innerHTML; portal.innerHTML = tooltip.innerHTML;
portal.classList.add('active'); portal.classList.add('active');
portal.style.display = 'block'; portal.style.display = 'block';
@@ -129,6 +225,7 @@ function initGameKitTooltips() {
miniPortal.classList.add('active'); miniPortal.classList.add('active');
miniPortal.style.display = 'block'; miniPortal.style.display = 'block';
miniHeight = miniPortal.offsetHeight + 4; miniHeight = miniPortal.offsetHeight + 4;
wireDonDoff(token);
} else { } else {
miniPortal.classList.remove('active'); miniPortal.classList.remove('active');
miniPortal.style.display = 'none'; miniPortal.style.display = 'none';
@@ -154,21 +251,24 @@ function initGameKitTooltips() {
if (isEquippable) { if (isEquippable) {
const mainRect = portal.getBoundingClientRect(); const mainRect = portal.getBoundingClientRect();
miniPortal.style.left = (mainRect.right - miniPortal.offsetWidth) + 'px'; // Pin the right edge of the mini-portal to the right edge of the main portal.
// Using `right` (not `left`) means text width changes grow/shrink leftward.
miniPortal.style.left = '';
miniPortal.style.right = Math.round(window.innerWidth - mainRect.right) + 'px';
miniPortal.style.top = (mainRect.bottom + 4) + 'px'; miniPortal.style.top = (mainRect.bottom + 4) + 'px';
} }
} }
document.addEventListener('mouseover', (e) => { document.addEventListener('mouseover', (e) => {
const tokenEl = e.target.closest('#id_game_kit .token'); const tokenEl = e.target.closest('#id_game_kit .token');
if (!tokenEl || !tokenEl.querySelector('.token-tooltip')) return; if (!tokenEl || !tokenEl.querySelector('.tt')) return;
if (!portal.classList.contains('active') || activeToken !== tokenEl) { if (!portal.classList.contains('active') || activeToken !== tokenEl) {
showPortals(tokenEl); showPortals(tokenEl);
} }
}); });
gameKit.querySelectorAll('.token').forEach(tokenEl => { gameKit.querySelectorAll('.token').forEach(tokenEl => {
if (!tokenEl.querySelector('.token-tooltip')) return; if (!tokenEl.querySelector('.tt')) return;
tokenEl.addEventListener('mouseenter', () => { tokenEl.addEventListener('mouseenter', () => {
if (!portal.classList.contains('active') || activeToken !== tokenEl) { if (!portal.classList.contains('active') || activeToken !== tokenEl) {
showPortals(tokenEl); showPortals(tokenEl);

View File

@@ -8,6 +8,8 @@ urlpatterns = [
path('toggle-applets', views.toggle_game_applets, name='toggle_game_applets'), path('toggle-applets', views.toggle_game_applets, name='toggle_game_applets'),
path('equip-trinket/<int:token_id>/', views.equip_trinket, name='equip_trinket'), path('equip-trinket/<int:token_id>/', views.equip_trinket, name='equip_trinket'),
path('equip-deck/<int:deck_id>/', views.equip_deck, name='equip_deck'), path('equip-deck/<int:deck_id>/', views.equip_deck, name='equip_deck'),
path('unequip-trinket/<int:token_id>/', views.unequip_trinket, name='unequip_trinket'),
path('unequip-deck/<int:deck_id>/', views.unequip_deck, name='unequip_deck'),
path('game-kit/', views.game_kit, name='game_kit'), path('game-kit/', views.game_kit, name='game_kit'),
path('game-kit/toggle-sections', views.toggle_game_kit_sections, name='toggle_game_kit_sections'), path('game-kit/toggle-sections', views.toggle_game_kit_sections, name='toggle_game_kit_sections'),
path('game-kit/deck/<int:deck_id>/', views.tarot_fan, name='tarot_fan'), path('game-kit/deck/<int:deck_id>/', views.tarot_fan, name='tarot_fan'),

View File

@@ -30,8 +30,8 @@ def gameboard(request):
"pass_token": pass_token, "pass_token": pass_token,
"coin": coin, "coin": coin,
"carte": carte, "carte": carte,
"equipped_trinket_id": str(request.user.equipped_trinket_id or ""), "equipped_trinket_id": request.user.equipped_trinket_id,
"equipped_deck_id": str(request.user.equipped_deck_id or ""), "equipped_deck_id": request.user.equipped_deck_id,
"deck_variants": list(request.user.unlocked_decks.all()), "deck_variants": list(request.user.unlocked_decks.all()),
"free_tokens": free_tokens, "free_tokens": free_tokens,
"free_count": len(free_tokens), "free_count": len(free_tokens),
@@ -60,8 +60,8 @@ def toggle_game_applets(request):
"pass_token": request.user.tokens.filter(token_type=Token.PASS).first() if request.user.is_staff else None, "pass_token": request.user.tokens.filter(token_type=Token.PASS).first() if request.user.is_staff else None,
"coin": request.user.tokens.filter(token_type=Token.COIN).first(), "coin": request.user.tokens.filter(token_type=Token.COIN).first(),
"carte": request.user.tokens.filter(token_type=Token.CARTE).first(), "carte": request.user.tokens.filter(token_type=Token.CARTE).first(),
"equipped_trinket_id": str(request.user.equipped_trinket_id or ""), "equipped_trinket_id": request.user.equipped_trinket_id,
"equipped_deck_id": str(request.user.equipped_deck_id or ""), "equipped_deck_id": request.user.equipped_deck_id,
"deck_variants": list(request.user.unlocked_decks.all()), "deck_variants": list(request.user.unlocked_decks.all()),
"free_tokens": list(request.user.tokens.filter( "free_tokens": list(request.user.tokens.filter(
token_type=Token.FREE, expires_at__gt=timezone.now() token_type=Token.FREE, expires_at__gt=timezone.now()
@@ -102,6 +102,28 @@ def equip_deck(request, deck_id):
return HttpResponse(status=405) return HttpResponse(status=405)
@login_required(login_url="/")
def unequip_trinket(request, token_id):
token = get_object_or_404(Token, pk=token_id, user=request.user)
if request.method == "POST":
if request.user.equipped_trinket_id == token.pk:
request.user.equipped_trinket = None
request.user.save(update_fields=["equipped_trinket"])
return HttpResponse(status=204)
return HttpResponse(status=405)
@login_required(login_url="/")
def unequip_deck(request, deck_id):
get_object_or_404(DeckVariant, pk=deck_id)
if request.method == "POST":
if request.user.equipped_deck_id == deck_id:
request.user.equipped_deck = None
request.user.save(update_fields=["equipped_deck"])
return HttpResponse(status=204)
return HttpResponse(status=405)
def _game_kit_context(user): def _game_kit_context(user):
coin = user.tokens.filter(token_type=Token.COIN).first() coin = user.tokens.filter(token_type=Token.COIN).first()
pass_token = user.tokens.filter(token_type=Token.PASS).first() if user.is_staff else None pass_token = user.tokens.filter(token_type=Token.PASS).first() if user.is_staff else None

View File

View File

@@ -0,0 +1,10 @@
from django.contrib import admin
from .models import TooltipContent
@admin.register(TooltipContent)
class TooltipContentAdmin(admin.ModelAdmin):
list_display = ('slug', 'title', 'type_label', 'symbol')
search_fields = ('slug', 'title', 'type_label')
list_filter = ('type_label',)

View File

@@ -0,0 +1,6 @@
from django.apps import AppConfig
class TooltipsConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'apps.tooltips'

View File

@@ -0,0 +1,33 @@
# Generated by Django 6.0 on 2026-04-16 00:38
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='TooltipContent',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('slug', models.SlugField(unique=True)),
('title', models.CharField(max_length=200)),
('type_label', models.CharField(blank=True, max_length=100)),
('symbol', models.CharField(blank=True, max_length=10)),
('degree_str', models.CharField(blank=True, max_length=40)),
('description', models.TextField(blank=True)),
('shoptalk', models.TextField(blank=True)),
('expiry', models.CharField(blank=True, max_length=200)),
('effect', models.TextField(blank=True)),
('extras', models.JSONField(default=dict)),
],
options={
'ordering': ['slug'],
},
),
]

View File

View File

@@ -0,0 +1,38 @@
from django.db import models
class TooltipContent(models.Model):
"""
Static tooltip content for game objects — planets, signs, houses, cards, etc.
Required: slug (unique lookup key), title (display name).
All other fields are optional; omitted fields render nothing in the template.
extras (JSONField) stores structured data that varies by tooltip type:
dignities — dict {role: body/sign string, …}
aspects — list [{symbol, type, body, orb}, …]
keywords_up / keywords_rev — list of strings (card upright/reversed keywords)
cautions — list [{title, type_label, shoptalk, effect}, …]
nav — dict {prv: slug, nxt: slug} for PRV/NXT navigation
"""
slug = models.SlugField(unique=True)
title = models.CharField(max_length=200)
# Optional display fields
type_label = models.CharField(max_length=100, blank=True)
symbol = models.CharField(max_length=10, blank=True)
degree_str = models.CharField(max_length=40, blank=True)
description = models.TextField(blank=True)
shoptalk = models.TextField(blank=True)
expiry = models.CharField(max_length=200, blank=True)
effect = models.TextField(blank=True)
# Structured data for tabular / list / nav sections
extras = models.JSONField(default=dict)
class Meta:
ordering = ['slug']
def __str__(self):
return self.title

View File

@@ -0,0 +1,32 @@
from django import template
register = template.Library()
@register.inclusion_tag('apps/tooltips/_tooltip.html')
def tooltip(data):
"""Render the unified tooltip partial from a data dict.
Minimum required key: 'title'.
All other keys are optional; missing keys render nothing.
extras (dict) may contain: dignities, aspects, keywords_up,
keywords_rev, cautions, nav.
"""
extras = data.get('extras', {}) or {}
return {
'title': data.get('title', ''),
'type_label': data.get('type_label', ''),
'symbol': data.get('symbol', ''),
'degree_str': data.get('degree_str', ''),
'description': data.get('description', ''),
'shoptalk': data.get('shoptalk', ''),
'expiry': data.get('expiry', ''),
'effect': data.get('effect', ''),
'dignities': extras.get('dignities'),
'aspects': extras.get('aspects'),
'keywords_up': extras.get('keywords_up'),
'keywords_rev':extras.get('keywords_rev'),
'cautions': extras.get('cautions'),
'nav': extras.get('nav'),
'equippable': data.get('equippable'),
}

View File

View File

@@ -0,0 +1,127 @@
from django.test import TestCase
from django.db import IntegrityError
from apps.tooltips.models import TooltipContent
class TooltipContentModelTest(TestCase):
# ── Required fields ───────────────────────────────────────────────────────
def test_can_create_with_slug_and_title_only(self):
tt = TooltipContent.objects.create(slug='sun', title='Sun')
self.assertEqual(TooltipContent.objects.count(), 1)
self.assertEqual(tt.slug, 'sun')
self.assertEqual(tt.title, 'Sun')
def test_slug_is_unique(self):
TooltipContent.objects.create(slug='sun', title='Sun')
with self.assertRaises(IntegrityError):
TooltipContent.objects.create(slug='sun', title='Another Sun')
def test_str_returns_title(self):
tt = TooltipContent(slug='moon', title='Moon')
self.assertEqual(str(tt), 'Moon')
# ── Optional text fields ──────────────────────────────────────────────────
def test_optional_text_fields_default_blank(self):
tt = TooltipContent.objects.create(slug='mars', title='Mars')
for field in ('type_label', 'symbol', 'degree_str',
'description', 'shoptalk', 'expiry', 'effect'):
self.assertEqual(getattr(tt, field), '', msg=f'{field} should default blank')
def test_can_set_all_text_fields(self):
tt = TooltipContent.objects.create(
slug='jupiter',
title='Jupiter',
type_label='Planet',
symbol='',
degree_str='14° 22 Scorpio',
description='Planet of expansion and fortune.',
shoptalk='Ruler of Sagittarius.',
expiry='',
effect='Amplifies whichever house it occupies.',
)
self.assertEqual(tt.type_label, 'Planet')
self.assertEqual(tt.symbol, '')
self.assertEqual(tt.degree_str, '14° 22 Scorpio')
# ── extras JSONField ──────────────────────────────────────────────────────
def test_extras_defaults_to_empty_dict(self):
tt = TooltipContent.objects.create(slug='venus', title='Venus')
self.assertEqual(tt.extras, {})
def test_extras_stores_dignities(self):
tt = TooltipContent.objects.create(
slug='saturn',
title='Saturn',
extras={
'dignities': {
'Domicile': 'Capricorn / Aquarius',
'Exalted': 'Libra',
'Exile': 'Cancer / Leo',
'Fallen': 'Aries',
},
},
)
tt.refresh_from_db()
self.assertEqual(tt.extras['dignities']['Exalted'], 'Libra')
def test_extras_stores_aspects(self):
tt = TooltipContent.objects.create(
slug='mercury',
title='Mercury',
extras={
'aspects': [
{'symbol': '', 'type': 'Trine', 'body': 'Venus', 'orb': '2° 14'},
{'symbol': '', 'type': 'Square', 'body': 'Mars', 'orb': '0° 42'},
],
},
)
tt.refresh_from_db()
self.assertEqual(len(tt.extras['aspects']), 2)
self.assertEqual(tt.extras['aspects'][0]['type'], 'Trine')
def test_extras_stores_keywords(self):
tt = TooltipContent.objects.create(
slug='the-schizo',
title='The Schizo, Leavened',
extras={
'keywords_up': ['willpower', 'skill', 'resourcefulness'],
'keywords_rev': ['hubris', 'overreach'],
},
)
tt.refresh_from_db()
self.assertIn('willpower', tt.extras['keywords_up'])
def test_extras_stores_cautions(self):
tt = TooltipContent.objects.create(
slug='the-schizo-cautions',
title='The Schizo, Leavened',
extras={
'cautions': [
{
'title': 'Caution!',
'type_label': 'Rival Interaction',
'shoptalk': '[Shoptalk forthcoming]',
'effect': 'This card will reverse into I. The Pervert...',
},
],
},
)
tt.refresh_from_db()
self.assertEqual(tt.extras['cautions'][0]['type_label'], 'Rival Interaction')
def test_extras_stores_nav(self):
"""PRV/NXT nav dict for paired or sequenced tooltips."""
tt = TooltipContent.objects.create(
slug='house-01',
title='House of Self',
extras={
'nav': {'prv': 'house-12', 'nxt': 'house-02'},
},
)
tt.refresh_from_db()
self.assertEqual(tt.extras['nav']['nxt'], 'house-02')

View File

@@ -0,0 +1,172 @@
from django.template import Context, Template
from django.test import TestCase
def render_tooltip(data):
"""Helper: render the {% tooltip %} tag with the given data dict."""
tpl = Template("{% load tooltip_tags %}{% tooltip data %}")
return tpl.render(Context({'data': data}))
class TooltipTagRequiredFieldsTest(TestCase):
def test_renders_title(self):
html = render_tooltip({'title': 'Sun'})
self.assertIn('Sun', html)
self.assertIn('class="tt-title"', html)
def test_wrapper_has_tt_class(self):
html = render_tooltip({'title': 'Moon'})
self.assertIn('class="tt"', html)
class TooltipTagOptionalTextFieldsTest(TestCase):
def test_renders_type_label_when_present(self):
html = render_tooltip({'title': 'Sun', 'type_label': 'Planet'})
self.assertIn('Planet', html)
self.assertIn('tt-type', html)
def test_omits_type_label_when_absent(self):
html = render_tooltip({'title': 'Sun'})
self.assertNotIn('tt-type', html)
def test_renders_symbol_when_present(self):
html = render_tooltip({'title': 'Sun', 'symbol': ''})
self.assertIn('', html)
self.assertIn('tt-symbol', html)
def test_omits_symbol_when_absent(self):
html = render_tooltip({'title': 'Sun'})
self.assertNotIn('tt-symbol', html)
def test_renders_degree_str_when_present(self):
html = render_tooltip({'title': 'Sun', 'degree_str': '14° 22 Scorpio'})
self.assertIn('14° 22 Scorpio', html)
self.assertIn('tt-degree', html)
def test_omits_degree_str_when_absent(self):
html = render_tooltip({'title': 'Sun'})
self.assertNotIn('tt-degree', html)
def test_renders_description_when_present(self):
html = render_tooltip({'title': 'Sun', 'description': 'Planet of vitality.'})
self.assertIn('Planet of vitality.', html)
self.assertIn('tt-description', html)
def test_renders_shoptalk_in_em_when_present(self):
html = render_tooltip({'title': 'Sun', 'shoptalk': 'Ruler of Leo.'})
self.assertIn('Ruler of Leo.', html)
self.assertIn('tt-shoptalk', html)
self.assertIn('<em>', html)
def test_renders_expiry_when_present(self):
html = render_tooltip({'title': 'Pass', 'expiry': 'no expiry'})
self.assertIn('no expiry', html)
self.assertIn('tt-expiry', html)
def test_renders_effect_when_present(self):
html = render_tooltip({'title': 'Pass', 'effect': 'Admit All Entry'})
self.assertIn('Admit All Entry', html)
self.assertIn('tt-effect', html)
class TooltipTagExtrasTest(TestCase):
def test_renders_dignities_table_when_present(self):
data = {
'title': 'Saturn',
'extras': {
'dignities': {
'Domicile': 'Capricorn',
'Exalted': 'Libra',
},
},
}
html = render_tooltip(data)
self.assertIn('tt-table--dignities', html)
self.assertIn('Capricorn', html)
self.assertIn('Exalted', html)
def test_omits_dignities_when_absent(self):
html = render_tooltip({'title': 'Sun', 'extras': {}})
self.assertNotIn('tt-table--dignities', html)
def test_renders_aspects_list_when_present(self):
data = {
'title': 'Mercury',
'extras': {
'aspects': [
{'symbol': '', 'type': 'Trine', 'body': 'Venus', 'orb': '2° 14'},
],
},
}
html = render_tooltip(data)
self.assertIn('tt-aspects', html)
self.assertIn('Trine', html)
self.assertIn('Venus', html)
def test_omits_aspects_when_absent(self):
html = render_tooltip({'title': 'Sun', 'extras': {}})
self.assertNotIn('tt-aspects', html)
def test_renders_keyword_lists_when_present(self):
data = {
'title': 'The Schizo',
'extras': {
'keywords_up': ['willpower', 'skill'],
'keywords_rev': ['hubris'],
},
}
html = render_tooltip(data)
self.assertIn('tt-keywords', html)
self.assertIn('willpower', html)
self.assertIn('hubris', html)
def test_omits_keywords_when_absent(self):
html = render_tooltip({'title': 'Sun', 'extras': {}})
self.assertNotIn('tt-keywords', html)
def test_renders_fyi_cautions_section_when_present(self):
data = {
'title': 'The Schizo',
'extras': {
'cautions': [
{
'title': 'Caution!',
'type_label': 'Rival Interaction',
'shoptalk': '[Shoptalk forthcoming]',
'effect': 'This card will reverse...',
},
],
},
}
html = render_tooltip(data)
self.assertIn('tt-fyi', html)
self.assertIn('tt-fyi--cautions', html)
self.assertIn('Rival Interaction', html)
def test_omits_cautions_section_when_absent(self):
html = render_tooltip({'title': 'Sun', 'extras': {}})
self.assertNotIn('tt-fyi--cautions', html)
def test_renders_nav_when_present(self):
data = {
'title': 'House of Self',
'extras': {
'nav': {'prv': 'house-12', 'nxt': 'house-02'},
},
}
html = render_tooltip(data)
self.assertIn('tt-nav', html)
self.assertIn('house-02', html)
self.assertIn('house-12', html)
def test_omits_nav_when_absent(self):
html = render_tooltip({'title': 'Sun', 'extras': {}})
self.assertNotIn('tt-nav', html)
def test_no_extras_key_does_not_error(self):
"""Tag should handle data with no 'extras' key gracefully."""
html = render_tooltip({'title': 'Sun'})
self.assertIn('Sun', html)

View File

@@ -63,6 +63,7 @@ INSTALLED_APPS = [
'apps.epic', 'apps.epic',
'apps.drama', 'apps.drama',
# Custom apps # Custom apps
'apps.tooltips',
'apps.ap', 'apps.ap',
'apps.api', 'apps.api',
'apps.applets', 'apps.applets',

View File

@@ -105,7 +105,7 @@ class GameKitTest(FunctionalTest):
"arguments[0].dispatchEvent(new Event('mouseenter'))", deck_el "arguments[0].dispatchEvent(new Event('mouseenter'))", deck_el
) )
tooltip = self.browser.find_element( tooltip = self.browser.find_element(
By.CSS_SELECTOR, "#id_kit_bag_dialog .kit-bag-deck .token-tooltip" By.CSS_SELECTOR, "#id_kit_bag_dialog .kit-bag-deck .tt"
) )
self.wait_for(lambda: self.assertTrue(tooltip.is_displayed())) self.wait_for(lambda: self.assertTrue(tooltip.is_displayed()))
text = tooltip.text text = tooltip.text

View File

@@ -4,6 +4,8 @@ from selenium.webdriver.common.by import By
from .base import FunctionalTest from .base import FunctionalTest
from apps.applets.models import Applet from apps.applets.models import Applet
from apps.epic.models import DeckVariant, Room
from apps.lyric.models import Token, User
class GameboardNavigationTest(FunctionalTest): class GameboardNavigationTest(FunctionalTest):
@@ -156,3 +158,139 @@ class GameboardAppletMenuTest(FunctionalTest):
) )
# 7. Assert no full page reload occurred # 7. Assert no full page reload occurred
self.assertTrue(self.browser.execute_script("return window.__no_reload_marker === true")) self.assertTrue(self.browser.execute_script("return window.__no_reload_marker === true"))
class GameKitEquipTest(FunctionalTest):
"""DON|DOFF equip buttons in the game kit applet portal tooltip."""
def setUp(self):
super().setUp()
self.earthman, _ = DeckVariant.objects.get_or_create(
slug="earthman",
defaults={"name": "Earthman Deck", "card_count": 108, "is_default": True},
)
Applet.objects.get_or_create(slug="game-kit", defaults={"name": "Game Kit", "context": "gameboard"})
self.create_pre_authenticated_session("gamer@equip.io")
self.gamer = User.objects.get(email="gamer@equip.io")
# Promote to staff so the pass_token appears in the game kit applet.
self.gamer.is_staff = True
self.gamer.save(update_fields=["is_staff"])
self.gamer.unlocked_decks.add(self.earthman)
self.coin = self.gamer.tokens.filter(token_type=Token.COIN).first()
# Create a PASS token manually — starts unequipped (coin is auto-equipped).
self.pass_token = Token.objects.create(user=self.gamer, token_type=Token.PASS)
self.browser.set_window_size(1200, 900)
self.browser.get(self.live_server_url + "/gameboard/")
def _hover_game_kit_token(self, token_el):
"""Hover token, wait for portal, return portal element."""
ActionChains(self.browser).move_to_element(token_el).perform()
self.wait_for(
lambda: self.assertTrue(
self.browser.find_element(By.ID, "id_tooltip_portal").is_displayed()
)
)
return self.browser.find_element(By.ID, "id_tooltip_portal")
def test_unequipped_token_shows_don_active_doff_disabled(self):
"""Backstage Pass — naturally unequipped (coin is auto-equipped): DON active, DOFF is ×."""
pass_el = self.wait_for(
lambda: self.browser.find_element(By.ID, "id_kit_pass")
)
portal = self._hover_game_kit_token(pass_el)
don = portal.find_element(By.CSS_SELECTOR, ".btn-equip")
doff = portal.find_element(By.CSS_SELECTOR, ".btn-unequip")
self.assertNotIn("btn-disabled", don.get_attribute("class"))
self.assertIn("btn-disabled", doff.get_attribute("class"))
self.assertEqual(doff.text, "×")
def test_equipped_token_shows_doff_active_don_disabled(self):
"""Auto-equipped coin: DOFF active, DON is ×; mini-portal says Equipped."""
coin = self.wait_for(
lambda: self.browser.find_element(By.ID, "id_kit_coin_on_a_string")
)
portal = self._hover_game_kit_token(coin)
don = portal.find_element(By.CSS_SELECTOR, ".btn-equip")
doff = portal.find_element(By.CSS_SELECTOR, ".btn-unequip")
self.assertIn("btn-disabled", don.get_attribute("class"))
self.assertEqual(don.text, "×")
self.assertNotIn("btn-disabled", doff.get_attribute("class"))
mini = self.browser.find_element(By.ID, "id_mini_tooltip_portal")
self.assertEqual(mini.text, "Equipped")
self.assertEqual(len(mini.find_elements(By.CSS_SELECTOR, "button")), 0)
def test_doff_then_don_roundtrip(self):
"""Full roundtrip: DOFF unequips (portal stays open, buttons swap, mini updates);
DON re-equips (buttons swap back, mini updates back, DB confirms)."""
coin = self.wait_for(
lambda: self.browser.find_element(By.ID, "id_kit_coin_on_a_string")
)
portal = self._hover_game_kit_token(coin)
# — DOFF —
portal.find_element(By.CSS_SELECTOR, ".btn-unequip").click()
self.wait_for(
lambda: self.assertIn(
"btn-disabled",
portal.find_element(By.CSS_SELECTOR, ".btn-unequip").get_attribute("class"),
)
)
self.assertEqual(portal.find_element(By.CSS_SELECTOR, ".btn-unequip").text, "×")
self.assertNotIn(
"btn-disabled",
portal.find_element(By.CSS_SELECTOR, ".btn-equip").get_attribute("class"),
)
self.assertEqual(
self.browser.find_element(By.ID, "id_mini_tooltip_portal").text, "Not Equipped"
)
self.gamer.refresh_from_db()
self.assertIsNone(self.gamer.equipped_trinket)
# — DON —
portal.find_element(By.CSS_SELECTOR, ".btn-equip").click()
self.wait_for(
lambda: self.assertIn(
"btn-disabled",
portal.find_element(By.CSS_SELECTOR, ".btn-equip").get_attribute("class"),
)
)
self.assertEqual(portal.find_element(By.CSS_SELECTOR, ".btn-equip").text, "×")
self.assertNotIn(
"btn-disabled",
portal.find_element(By.CSS_SELECTOR, ".btn-unequip").get_attribute("class"),
)
self.assertEqual(
self.browser.find_element(By.ID, "id_mini_tooltip_portal").text, "Equipped"
)
self.gamer.refresh_from_db()
self.assertEqual(self.gamer.equipped_trinket, self.coin)
def test_doff_updates_open_kit_bag_dialog(self):
"""DOFF from game kit applet replaces trinket card in currently-open kit bag dialog."""
self.gamer.equipped_trinket = self.coin
self.gamer.save(update_fields=["equipped_trinket"])
self.browser.refresh()
# Open kit bag dialog
self.browser.find_element(By.ID, "id_kit_btn").click()
self.wait_for(
lambda: self.browser.find_element(
By.CSS_SELECTOR,
f'#id_kit_bag_dialog [data-token-id="{self.coin.id}"]',
)
)
# While dialog is open, hover coin and DOFF from game kit
coin = self.wait_for(
lambda: self.browser.find_element(By.ID, "id_kit_coin_on_a_string")
)
portal = self._hover_game_kit_token(coin)
portal.find_element(By.CSS_SELECTOR, ".btn-unequip").click()
# Dialog updates: trinket card replaced with placeholder
self.wait_for(
lambda: self.assertEqual(
len(self.browser.find_elements(
By.CSS_SELECTOR,
f'#id_kit_bag_dialog [data-token-id="{self.coin.id}"]',
)), 0
)
)
self.browser.find_element(By.CSS_SELECTOR, "#id_kit_bag_dialog .kit-bag-placeholder")

View File

@@ -235,7 +235,7 @@ class CarteBlancheTest(FunctionalTest):
# relying on hover visibility in headless Firefox. # relying on hover visibility in headless Firefox.
self.assertIn( self.assertIn(
"The Long Room", "The Long Room",
carte_in_bag.find_element(By.CSS_SELECTOR, ".token-tooltip").get_attribute("textContent"), carte_in_bag.find_element(By.CSS_SELECTOR, ".tt").get_attribute("textContent"),
) )
# Close kit bag # Close kit bag
self.browser.find_element(By.ID, "id_kit_btn").click() self.browser.find_element(By.ID, "id_kit_btn").click()
@@ -265,7 +265,7 @@ class CarteBlancheTest(FunctionalTest):
) )
self.assertNotIn( self.assertNotIn(
"The Long Room", "The Long Room",
carte_in_bag.find_element(By.CSS_SELECTOR, ".token-tooltip").get_attribute("textContent"), carte_in_bag.find_element(By.CSS_SELECTOR, ".tt").get_attribute("textContent"),
) )
self.browser.find_element(By.ID, "id_kit_btn").click() self.browser.find_element(By.ID, "id_kit_btn").click()

View File

@@ -286,44 +286,6 @@
font-size: 0.75rem; // 0.63rem × 1.2 font-size: 0.75rem; // 0.63rem × 1.2
} }
&.btn-disabled {
cursor: default !important;
pointer-events: none;
font-size: 1.2rem;
padding-bottom: 0.1rem;
color: rgba(var(--secUser), 0.25) !important;
background-color: rgba(var(--priUser), 1) !important;
border-color: rgba(var(--secUser), 0.25) !important;
box-shadow:
0.1rem 0.1rem 0.12rem rgba(var(--priUser), 0.5),
0.12rem 0.12rem 0.25rem rgba(0, 0, 0, 0.25),
0.25rem 0.25rem 0.25rem rgba(var(--secUser), 0.12)
;
&:hover {
text-shadow:
0.1rem 0.1rem 0.1rem rgba(0, 0, 0, 0.25),
0 0 1rem rgba(var(--priUser), 1)
;
box-shadow:
0.12rem 0.12rem 0.5rem rgba(0, 0, 0, 0.25),
0 0 0.5rem rgba(var(--priUser), 0.12)
;
}
&:active {
text-shadow:
-0.1rem -0.1rem 0.25rem rgba(0, 0, 0, 0.25),
0 0 0.12rem rgba(var(--priUser), 1)
;
box-shadow:
-0.1rem -0.1rem 0.12rem rgba(var(--priUser), 0.75),
-0.1rem -0.1rem 0.12rem rgba(0, 0, 0, 0.25),
0 0 0.5rem rgba(var(--secUser), 0.12)
;
}
}
&.btn-nav-left { &.btn-nav-left {
color: rgba(var(--priFs), 1); color: rgba(var(--priFs), 1);
border-color: rgba(var(--priFs), 1); border-color: rgba(var(--priFs), 1);
@@ -394,6 +356,76 @@
} }
} }
&.btn-equip {
color: rgba(var(--priTk), 1);
border-color: rgba(var(--priTk), 1);
background-color: rgba(var(--terTk), 1);
box-shadow:
0.1rem 0.1rem 0.12rem rgba(var(--terTk), 0.25),
0.12rem 0.12rem 0.25rem rgba(0, 0, 0, 0.25),
0.25rem 0.25rem 0.25rem rgba(var(--terTk), 0.12)
;
&:hover {
text-shadow:
0.1rem 0.1rem 0.1rem rgba(0, 0, 0, 0.25),
0 0 1rem rgba(var(--priTk), 1)
;
box-shadow:
0.12rem 0.12rem 0.5rem rgba(0, 0, 0, 0.25),
0 0 0.5rem rgba(var(--priTk), 0.12)
;
}
&:active {
border: 0.18rem solid rgba(var(--priTk), 1);
text-shadow:
-0.1rem -0.1rem 0.25rem rgba(0, 0, 0, 0.25),
0 0 0.12rem rgba(var(--priTk), 1)
;
box-shadow:
-0.1rem -0.1rem 0.12rem rgba(var(--terTk), 0.25),
-0.1rem -0.1rem 0.12rem rgba(0, 0, 0, 0.25),
0 0 0.5rem rgba(var(--priTk), 0.12)
;
}
}
&.btn-unequip {
color: rgba(var(--priMe), 1);
border-color: rgba(var(--priMe), 1);
background-color: rgba(var(--terMe), 1);
box-shadow:
0.1rem 0.1rem 0.12rem rgba(var(--terMe), 0.25),
0.12rem 0.12rem 0.25rem rgba(0, 0, 0, 0.25),
0.25rem 0.25rem 0.25rem rgba(var(--terMe), 0.12)
;
&:hover {
text-shadow:
0.1rem 0.1rem 0.1rem rgba(0, 0, 0, 0.25),
0 0 1rem rgba(var(--priMe), 1)
;
box-shadow:
0.12rem 0.12rem 0.5rem rgba(0, 0, 0, 0.25),
0 0 0.5rem rgba(var(--priMe), 0.12)
;
}
&:active {
border: 0.18rem solid rgba(var(--priMe), 1);
text-shadow:
-0.1rem -0.1rem 0.25rem rgba(0, 0, 0, 0.25),
0 0 0.12rem rgba(var(--priMe), 1)
;
box-shadow:
-0.1rem -0.1rem 0.12rem rgba(var(--terMe), 0.25),
-0.1rem -0.1rem 0.12rem rgba(0, 0, 0, 0.25),
0 0 0.5rem rgba(var(--priMe), 0.12)
;
}
}
&.btn-reverse { &.btn-reverse {
color: rgba(var(--priCy), 1); color: rgba(var(--priCy), 1);
border-color: rgba(var(--priCy), 1); border-color: rgba(var(--priCy), 1);
@@ -463,4 +495,43 @@
; ;
} }
} }
// Dead last — wins over all color modifiers by source order.
&.btn-disabled {
cursor: default !important;
pointer-events: none;
font-size: 1.2rem;
padding-bottom: 0.1rem;
color: rgba(var(--secUser), 0.25) !important;
background-color: rgba(var(--priUser), 1) !important;
border-color: rgba(var(--secUser), 0.25) !important;
box-shadow:
0.1rem 0.1rem 0.12rem rgba(var(--priUser), 0.5),
0.12rem 0.12rem 0.25rem rgba(0, 0, 0, 0.25),
0.25rem 0.25rem 0.25rem rgba(var(--secUser), 0.12)
;
&:hover {
text-shadow:
0.1rem 0.1rem 0.1rem rgba(0, 0, 0, 0.25),
0 0 1rem rgba(var(--priUser), 1)
;
box-shadow:
0.12rem 0.12rem 0.5rem rgba(0, 0, 0, 0.25),
0 0 0.5rem rgba(var(--priUser), 0.12)
;
}
&:active {
text-shadow:
-0.1rem -0.1rem 0.25rem rgba(0, 0, 0, 0.25),
0 0 0.12rem rgba(var(--priUser), 1)
;
box-shadow:
-0.1rem -0.1rem 0.12rem rgba(var(--priUser), 0.75),
-0.1rem -0.1rem 0.12rem rgba(0, 0, 0, 0.25),
0 0 0.5rem rgba(var(--secUser), 0.12)
;
}
}
} }

View File

@@ -105,11 +105,26 @@
transition: filter 0.15s; transition: filter 0.15s;
padding: 0 0.125rem; padding: 0 0.125rem;
&:hover .token-tooltip { display: none; } // JS positions these as fixed &:hover .token-tooltip,
&:hover .tt { display: none; } // JS positions these as fixed
} }
.token-tooltip { .token-tooltip,
.tt {
z-index: 9999; z-index: 9999;
// Buttons positioned on left edge of the fixed inline tooltip
.tt-equip-btns {
position: absolute;
left: -1rem;
top: 0;
display: flex;
flex-direction: column;
gap: 1.25rem;
z-index: 1;
.btn { margin: 0; }
}
} }
.kit-bag-deck { .kit-bag-deck {

View File

@@ -78,7 +78,8 @@ body.page-gameboard {
.token { position: static; } .token { position: static; }
.token:hover .token-tooltip { display: none; } .token:hover .token-tooltip,
.token:hover .tt { display: none; } // JS portal handles show/hide
.token, .token,
.kit-item { font-size: 1.5rem; } .kit-item { font-size: 1.5rem; }
@@ -121,6 +122,25 @@ body.page-gameboard {
position: fixed; position: fixed;
z-index: 9999; z-index: 9999;
padding: 0.75rem 1.5rem;
.tt-title { font-size: 1rem; }
.tt-description { padding: 0.125rem; font-size: 0.75rem; }
.tt-shoptalk { font-size: 0.75rem; opacity: 0.75; }
.tt-expiry { font-size: 1rem; color: rgba(var(--priRd), 1); }
.tt-equip-btns {
position: absolute;
left: -1rem;
top: -1rem;
display: flex;
flex-direction: column;
gap: 0.25rem;
z-index: 1;
.btn { margin: 0; }
}
&.active { display: block; } &.active { display: block; }
} }

View File

@@ -1,4 +1,5 @@
.token-tooltip { .token-tooltip,
.tt {
display: none; display: none;
width: 16rem; width: 16rem;
max-width: 16rem; max-width: 16rem;
@@ -67,7 +68,7 @@
} }
&:hover .token-tooltip { &:hover .token-tooltip {
display: block; display: block; // legacy fallback; .tt is JS-portal-only (no CSS hover)
} }
} }

View File

@@ -3,49 +3,52 @@
style="--applet-cols: {{ entry.applet.grid_cols }}; --applet-rows: {{ entry.applet.grid_rows }};" style="--applet-cols: {{ entry.applet.grid_cols }}; --applet-rows: {{ entry.applet.grid_rows }};"
> >
<h2><a href="{% url 'game_kit' %}">Game Kit</a></h2> <h2><a href="{% url 'game_kit' %}">Game Kit</a></h2>
<div id="id_game_kit" data-equipped-id="{{ equipped_trinket_id }}" data-equipped-deck-id="{{ equipped_deck_id }}"> <div id="id_game_kit" data-equipped-id="{{ equipped_trinket_id|default:'' }}" data-equipped-deck-id="{{ equipped_deck_id|default:'' }}">
{% if pass_token %} {% if pass_token %}
<div id="id_kit_pass" class="token" data-token-id="{{ pass_token.pk }}"> <div id="id_kit_pass" class="token" data-token-id="{{ pass_token.pk }}">
<i class="fa-solid fa-clipboard"></i> <i class="fa-solid fa-clipboard"></i>
<div class="token-tooltip"> <div class="tt">
<div class="token-tooltip-body"> <div class="tt-equip-btns">
<h4>{{ pass_token.tooltip_name }}</h4> {% if pass_token.pk == equipped_trinket_id %}<button class="btn btn-equip btn-disabled" data-token-id="{{ pass_token.pk }}">×</button><button class="btn btn-unequip" data-token-id="{{ pass_token.pk }}">DOFF</button>{% else %}<button class="btn btn-equip" data-token-id="{{ pass_token.pk }}">DON</button><button class="btn btn-unequip btn-disabled" data-token-id="{{ pass_token.pk }}">×</button>{% endif %}
<p>{{ pass_token.tooltip_description }}</p>
{% if pass_token.tooltip_shoptalk %}
<small><em>{{ pass_token.tooltip_shoptalk }}</em></small>
{% endif %}
<p class="expiry">{{ pass_token.tooltip_expiry }}</p>
</div> </div>
<h4 class="tt-title">{{ pass_token.tooltip_name }}</h4>
<p class="tt-description">{{ pass_token.tooltip_description }}</p>
{% if pass_token.tooltip_shoptalk %}
<p class="tt-shoptalk"><em>{{ pass_token.tooltip_shoptalk }}</em></p>
{% endif %}
<p class="tt-expiry">{{ pass_token.tooltip_expiry }}</p>
</div> </div>
</div> </div>
{% endif %} {% endif %}
{% if carte %} {% if carte %}
<div id="id_kit_carte_blanche" class="token" data-token-id="{{ carte.pk }}"> <div id="id_kit_carte_blanche" class="token" data-token-id="{{ carte.pk }}">
<i class="fa-solid fa-money-check"></i> <i class="fa-solid fa-money-check"></i>
<div class="token-tooltip"> <div class="tt">
<div class="token-tooltip-body"> <div class="tt-equip-btns">
<h4>{{ carte.tooltip_name }}</h4> {% if carte.pk == equipped_trinket_id %}<button class="btn btn-equip btn-disabled" data-token-id="{{ carte.pk }}">×</button><button class="btn btn-unequip" data-token-id="{{ carte.pk }}">DOFF</button>{% else %}<button class="btn btn-equip" data-token-id="{{ carte.pk }}">DON</button><button class="btn btn-unequip btn-disabled" data-token-id="{{ carte.pk }}">×</button>{% endif %}
<p>{{ carte.tooltip_description }}</p>
{% if carte.tooltip_shoptalk %}
<small><em>{{ carte.tooltip_shoptalk }}</em></small>
{% endif %}
<p class="expiry">{{ carte.tooltip_expiry }}</p>
</div> </div>
<h4 class="tt-title">{{ carte.tooltip_name }}</h4>
<p class="tt-description">{{ carte.tooltip_description }}</p>
{% if carte.tooltip_shoptalk %}
<p class="tt-shoptalk"><em>{{ carte.tooltip_shoptalk }}</em></p>
{% endif %}
<p class="tt-expiry">{{ carte.tooltip_expiry }}</p>
</div> </div>
</div> </div>
{% endif %} {% endif %}
{% if coin %} {% if coin %}
<div id="id_kit_coin_on_a_string" class="token" data-token-id="{{ coin.pk }}"> <div id="id_kit_coin_on_a_string" class="token" data-token-id="{{ coin.pk }}">
<i class="fa-solid fa-medal"></i> <i class="fa-solid fa-medal"></i>
<div class="token-tooltip"> <div class="tt">
<div class="token-tooltip-body"> <div class="tt-equip-btns">
<h4>{{ coin.tooltip_name }}</h4> {% if coin.pk == equipped_trinket_id %}<button class="btn btn-equip btn-disabled" data-token-id="{{ coin.pk }}">×</button><button class="btn btn-unequip" data-token-id="{{ coin.pk }}">DOFF</button>{% else %}<button class="btn btn-equip" data-token-id="{{ coin.pk }}">DON</button><button class="btn btn-unequip btn-disabled" data-token-id="{{ coin.pk }}">×</button>{% endif %}
<p>{{ coin.tooltip_description }}</p>
{% if coin.tooltip_shoptalk %}
<small><em>{{ coin.tooltip_shoptalk }}</em></small>
{% endif %}
<p class="expiry">{{ coin.tooltip_expiry }}</p>
</div> </div>
<h4 class="tt-title">{{ coin.tooltip_name }}</h4>
<p class="tt-description">{{ coin.tooltip_description }}</p>
{% if coin.tooltip_shoptalk %}
<p class="tt-shoptalk"><em>{{ coin.tooltip_shoptalk }}</em></p>
{% endif %}
<p class="tt-expiry">{{ coin.tooltip_expiry }}</p>
</div> </div>
</div> </div>
{% endif %} {% endif %}
@@ -53,15 +56,13 @@
{% with free_tokens.0 as token %} {% with free_tokens.0 as token %}
<div id="id_kit_free_token" class="token"> <div id="id_kit_free_token" class="token">
<i class="fa-solid fa-coins"></i> <i class="fa-solid fa-coins"></i>
<div class="token-tooltip"> <div class="tt">
<div class="token-tooltip-body"> <h4 class="tt-title">{{ token.tooltip_name }}{% if free_count > 1 %} <span class="token-count">(×{{ free_count }})</span>{% endif %}</h4>
<h4>{{ token.tooltip_name }}{% if free_count > 1 %} <span class="token-count">(×{{ free_count }})</span>{% endif %}</h4> <p class="tt-description">{{ token.tooltip_description }}</p>
<p>{{ token.tooltip_description }}</p> {% if token.tooltip_shoptalk %}
{% if token.tooltip_shoptalk %} <p class="tt-shoptalk"><em>{{ token.tooltip_shoptalk }}</em></p>
<small><em>{{ token.tooltip_shoptalk }}</em></small> {% endif %}
{% endif %} <p class="tt-expiry">{{ token.tooltip_expiry }}</p>
<p class="expiry">{{ token.tooltip_expiry }}</p>
</div>
</div> </div>
</div> </div>
{% endwith %} {% endwith %}
@@ -69,11 +70,12 @@
{% for deck in deck_variants %} {% for deck in deck_variants %}
<div id="id_kit_{{ deck.short_key }}_deck" class="token deck-variant" data-deck-id="{{ deck.pk }}"> <div id="id_kit_{{ deck.short_key }}_deck" class="token deck-variant" data-deck-id="{{ deck.pk }}">
<i class="fa-regular fa-id-badge"></i> <i class="fa-regular fa-id-badge"></i>
<div class="token-tooltip"> <div class="tt">
<div class="token-tooltip-body"> <div class="tt-equip-btns">
<h4>{{ deck.name }}</h4> {% if deck.pk == equipped_deck_id %}<button class="btn btn-equip btn-disabled" data-deck-id="{{ deck.pk }}">×</button><button class="btn btn-unequip" data-deck-id="{{ deck.pk }}">DOFF</button>{% else %}<button class="btn btn-equip" data-deck-id="{{ deck.pk }}">DON</button><button class="btn btn-unequip btn-disabled" data-deck-id="{{ deck.pk }}">×</button>{% endif %}
<p>{{ deck.card_count }} cards</p>
</div> </div>
<h4 class="tt-title">{{ deck.name }}</h4>
<p class="tt-description">{{ deck.card_count }} cards</p>
</div> </div>
</div> </div>
{% empty %} {% empty %}

View File

@@ -0,0 +1,81 @@
<div class="tt">
{% if equippable %}
<div class="tt-equip-btns">
{% if equippable.is_equipped %}
<button class="btn btn-equip btn-disabled"{% if equippable.token_id %} data-token-id="{{ equippable.token_id }}"{% endif %}{% if equippable.deck_id %} data-deck-id="{{ equippable.deck_id }}"{% endif %}>×</button>
<button class="btn btn-unequip"{% if equippable.token_id %} data-token-id="{{ equippable.token_id }}"{% endif %}{% if equippable.deck_id %} data-deck-id="{{ equippable.deck_id }}"{% endif %}>DOFF</button>
{% else %}
<button class="btn btn-equip"{% if equippable.token_id %} data-token-id="{{ equippable.token_id }}"{% endif %}{% if equippable.deck_id %} data-deck-id="{{ equippable.deck_id }}"{% endif %}>DON</button>
<button class="btn btn-unequip btn-disabled"{% if equippable.token_id %} data-token-id="{{ equippable.token_id }}"{% endif %}{% if equippable.deck_id %} data-deck-id="{{ equippable.deck_id }}"{% endif %}>×</button>
{% endif %}
</div>
{% endif %}
<h4 class="tt-title">{{ title }}</h4>
{% if type_label %}<p class="tt-type">{{ type_label }}</p>{% endif %}
{% if symbol %}<span class="tt-symbol">{{ symbol }}</span>{% endif %}
{% if degree_str %}<p class="tt-degree">{{ degree_str }}</p>{% endif %}
{% if description %}<p class="tt-description">{{ description }}</p>{% endif %}
{% if shoptalk %}<p class="tt-shoptalk"><em>{{ shoptalk }}</em></p>{% endif %}
{% if expiry %}<p class="tt-expiry">{{ expiry }}</p>{% endif %}
{% if effect %}<p class="tt-effect">{{ effect }}</p>{% endif %}
{% if dignities %}
<table class="tt-table tt-table--dignities">
{% for key, value in dignities.items %}
<tr><th>{{ key }}</th><td>{{ value }}</td></tr>
{% endfor %}
</table>
{% endif %}
{% if aspects %}
<ul class="tt-aspects">
{% for asp in aspects %}
<li><span class="tt-asp-symbol">{{ asp.symbol }}</span> {{ asp.type }} {{ asp.body }} <span class="tt-asp-orb">{{ asp.orb }}</span></li>
{% endfor %}
</ul>
{% endif %}
{% if keywords_up or keywords_rev %}
<div class="tt-keywords">
{% if keywords_up %}
<ul class="tt-keywords-up">
{% for kw in keywords_up %}<li>{{ kw }}</li>{% endfor %}
</ul>
{% endif %}
{% if keywords_rev %}
<ul class="tt-keywords-rev">
{% for kw in keywords_rev %}<li>{{ kw }}</li>{% endfor %}
</ul>
{% endif %}
</div>
{% endif %}
{% if cautions %}
<div class="tt-fyi tt-fyi--cautions">
{% for caution in cautions %}
<div class="tt-fyi-item">
{% if caution.type_label %}<p class="tt-fyi-type">{{ caution.type_label }}</p>{% endif %}
{% if caution.shoptalk %}<p class="tt-fyi-shoptalk">{{ caution.shoptalk }}</p>{% endif %}
{% if caution.effect %}<p class="tt-fyi-effect">{{ caution.effect }}</p>{% endif %}
</div>
{% endfor %}
</div>
{% endif %}
{% if nav %}
<div class="tt-nav">
<span class="tt-nav-prv">{{ nav.prv }}</span>
<span class="tt-nav-nxt">{{ nav.nxt }}</span>
</div>
{% endif %}
</div>

View File

@@ -7,25 +7,25 @@
{% if pass_token %} {% if pass_token %}
<div id="id_pass_token" class="token"> <div id="id_pass_token" class="token">
<i class="fa-solid fa-clipboard"></i> <i class="fa-solid fa-clipboard"></i>
<div class="token-tooltip"> <div class="tt">
<h4>{{ pass_token.tooltip_name }}</h4> <h4 class="tt-title">{{ pass_token.tooltip_name }}</h4>
<p>{{ pass_token.tooltip_description }}</p> <p class="tt-description">{{ pass_token.tooltip_description }}</p>
{% if pass_token.tooltip_shoptalk %} {% if pass_token.tooltip_shoptalk %}
<small><em>{{ pass_token.tooltip_shoptalk }}</em></small> <p class="tt-shoptalk"><em>{{ pass_token.tooltip_shoptalk }}</em></p>
{% endif %} {% endif %}
<p class="expiry">{{ pass_token.tooltip_expiry }}</p> <p class="tt-expiry">{{ pass_token.tooltip_expiry }}</p>
</div> </div>
</div> </div>
{% elif coin %} {% elif coin %}
<div id="id_coin_on_a_string" class="token"> <div id="id_coin_on_a_string" class="token">
<i class="fa-solid fa-medal"></i> <i class="fa-solid fa-medal"></i>
<div class="token-tooltip"> <div class="tt">
<h4>{{ coin.tooltip_name }}</h4> <h4 class="tt-title">{{ coin.tooltip_name }}</h4>
<p>{{ coin.tooltip_description }}</p> <p class="tt-description">{{ coin.tooltip_description }}</p>
{% if coin.tooltip_shoptalk %} {% if coin.tooltip_shoptalk %}
<small><em>{{ coin.tooltip_shoptalk }}</em></small> <p class="tt-shoptalk"><em>{{ coin.tooltip_shoptalk }}</em></p>
{% endif %} {% endif %}
<p class="expiry">{{ coin.tooltip_expiry }}</p> <p class="tt-expiry">{{ coin.tooltip_expiry }}</p>
</div> </div>
</div> </div>
{% endif %} {% endif %}
@@ -33,23 +33,23 @@
{% with free_tokens.0 as token %} {% with free_tokens.0 as token %}
<div id="id_free_token" class="token"> <div id="id_free_token" class="token">
<i class="fa-solid fa-coins"></i> <i class="fa-solid fa-coins"></i>
<div class="token-tooltip"> <div class="tt">
<h4>{{ token.tooltip_name }}{% if free_count > 1 %} <span class="token-count">(×{{ free_count }})</span>{% endif %}</h4> <h4 class="tt-title">{{ token.tooltip_name }}{% if free_count > 1 %} <span class="token-count">(×{{ free_count }})</span>{% endif %}</h4>
<p>{{ token.tooltip_description }}</p> <p class="tt-description">{{ token.tooltip_description }}</p>
{% if token.tooltip_shoptalk %} {% if token.tooltip_shoptalk %}
<small><em>{{ token.tooltip_shoptalk }}</em></small> <p class="tt-shoptalk"><em>{{ token.tooltip_shoptalk }}</em></p>
{% endif %} {% endif %}
<p class="expiry">{{ token.tooltip_expiry }}</p> <p class="tt-expiry">{{ token.tooltip_expiry }}</p>
</div> </div>
</div> </div>
{% endwith %} {% endwith %}
{% else %} {% else %}
<div id="id_free_token_empty" class="token token--empty"> <div id="id_free_token_empty" class="token token--empty">
<i class="fa-solid fa-coins"></i> <i class="fa-solid fa-coins"></i>
<div class="token-tooltip"> <div class="tt">
<h4>Free Token</h4> <h4 class="tt-title">Free Token</h4>
<p>0 owned</p> <p class="tt-description">0 owned</p>
<p class="expiry">find one around</p> <p class="tt-expiry">find one around</p>
</div> </div>
</div> </div>
{% endif %} {% endif %}
@@ -57,20 +57,20 @@
{% with tithe_tokens.0 as token %} {% with tithe_tokens.0 as token %}
<div id="id_tithe_token" class="token"> <div id="id_tithe_token" class="token">
<i class="fa-solid fa-piggy-bank"></i> <i class="fa-solid fa-piggy-bank"></i>
<div class="token-tooltip"> <div class="tt">
<h4>{{ token.tooltip_name }}{% if tithe_count > 1 %} <span class="token-count">(×{{ tithe_count }})</span>{% endif %}</h4> <h4 class="tt-title">{{ token.tooltip_name }}{% if tithe_count > 1 %} <span class="token-count">(×{{ tithe_count }})</span>{% endif %}</h4>
<p>{{ token.tooltip_description }}</p> <p class="tt-description">{{ token.tooltip_description }}</p>
<p class="expiry">{{ token.tooltip_expiry }}</p> <p class="tt-expiry">{{ token.tooltip_expiry }}</p>
</div> </div>
</div> </div>
{% endwith %} {% endwith %}
{% else %} {% else %}
<div id="id_tithe_token_empty" class="token token--empty"> <div id="id_tithe_token_empty" class="token token--empty">
<i class="fa-solid fa-piggy-bank"></i> <i class="fa-solid fa-piggy-bank"></i>
<div class="token-tooltip"> <div class="tt">
<h4>Tithe Token</h4> <h4 class="tt-title">Tithe Token</h4>
<p>0 owned</p> <p class="tt-description">0 owned</p>
<p class="expiry">purchase one above</p> <p class="tt-expiry">purchase one above</p>
</div> </div>
</div> </div>
{% endif %} {% endif %}

View File

@@ -4,12 +4,12 @@
<div class="kit-bag-row"> <div class="kit-bag-row">
<div class="kit-bag-deck" data-deck-id="{{ equipped_deck.pk }}"> <div class="kit-bag-deck" data-deck-id="{{ equipped_deck.pk }}">
<i class="fa-regular fa-id-badge"></i> <i class="fa-regular fa-id-badge"></i>
<div class="token-tooltip"> <div class="tt">
<h4>{{ equipped_deck.name }}{% if equipped_deck.is_default %} <span class="token-count">(Default)</span>{% endif %}</h4> <h4 class="tt-title">{{ equipped_deck.name }}{% if equipped_deck.is_default %} <span class="token-count">(Default)</span>{% endif %}</h4>
<p>{{ equipped_deck.card_count }}-card Tarot deck</p> <p class="tt-description">{{ equipped_deck.card_count }}-card Tarot deck</p>
<small><em>placeholder comment</em></small> <p class="tt-shoptalk"><em>placeholder comment</em></p>
<p class="availability">active</p> <p class="tt-effect">active</p>
<p class="stock-version">Stock version</p> <p class="tt-expiry">Stock version</p>
</div> </div>
</div> </div>
</div> </div>
@@ -25,10 +25,10 @@
</div> </div>
</div> </div>
{% if equipped_trinket %}
<div class="kit-bag-section"> <div class="kit-bag-section">
<span class="kit-bag-label">Trinket</span> <span class="kit-bag-label">Trinket</span>
<div class="kit-bag-row"> <div class="kit-bag-row">
{% if equipped_trinket %}
{% with token=equipped_trinket %} {% with token=equipped_trinket %}
<div <div
class="token" class="token"
@@ -43,24 +43,27 @@
{% else %} {% else %}
<i class="fa-solid fa-clipboard"></i> <i class="fa-solid fa-clipboard"></i>
{% endif %} {% endif %}
<div class="token-tooltip"> <div class="tt">
<h4>{{ token.tooltip_name }}</h4> <h4 class="tt-title">{{ token.tooltip_name }}</h4>
<p>{{ token.tooltip_description }}</p> <p class="tt-description">{{ token.tooltip_description }}</p>
{% if token.tooltip_shoptalk %} {% if token.tooltip_shoptalk %}
<small><em>{{ token.tooltip_shoptalk }}</em></small> <p class="tt-shoptalk"><em>{{ token.tooltip_shoptalk }}</em></p>
{% endif %} {% endif %}
<p class="expiry">{{ token.tooltip_expiry }}</p> <p class="tt-expiry">{{ token.tooltip_expiry }}</p>
{% with room_html=token.tooltip_room_html %} {% with room_html=token.tooltip_room_html %}
{% if room_html %}{{ room_html|safe }}{% endif %} {% if room_html %}{{ room_html|safe }}{% endif %}
{% endwith %} {% endwith %}
</div> </div>
</div> </div>
{% endwith %} {% endwith %}
{% else %}
<div class="kit-bag-placeholder">
<i class="fa-solid fa-medal"></i>
</div>
{% endif %}
</div> </div>
</div> </div>
{% endif %}
{% if free_token or tithe_token %}
<div class="kit-bag-section kit-bag-section--tokens"> <div class="kit-bag-section kit-bag-section--tokens">
<span class="kit-bag-label">Tokens</span> <span class="kit-bag-label">Tokens</span>
<div class="kit-bag-row kit-bag-row--scroll"> <div class="kit-bag-row kit-bag-row--scroll">
@@ -72,13 +75,13 @@
data-token-type="{{ free_token.token_type }}" data-token-type="{{ free_token.token_type }}"
> >
<i class="fa-solid fa-coins"></i> <i class="fa-solid fa-coins"></i>
<div class="token-tooltip"> <div class="tt">
<h4>{{ free_token.tooltip_name }}{% if free_count > 1 %} <span class="token-count">(×{{ free_count }})</span>{% endif %}</h4> <h4 class="tt-title">{{ free_token.tooltip_name }}{% if free_count > 1 %} <span class="token-count">(×{{ free_count }})</span>{% endif %}</h4>
<p>{{ free_token.tooltip_description }}</p> <p class="tt-description">{{ free_token.tooltip_description }}</p>
{% if free_token.tooltip_shoptalk %} {% if free_token.tooltip_shoptalk %}
<small><em>{{ free_token.tooltip_shoptalk }}</em></small> <p class="tt-shoptalk"><em>{{ free_token.tooltip_shoptalk }}</em></p>
{% endif %} {% endif %}
<p class="expiry">{{ free_token.tooltip_expiry }}</p> <p class="tt-expiry">{{ free_token.tooltip_expiry }}</p>
</div> </div>
</div> </div>
{% endif %} {% endif %}
@@ -90,13 +93,17 @@
data-token-type="{{ tithe_token.token_type }}" data-token-type="{{ tithe_token.token_type }}"
> >
<i class="fa-solid fa-piggy-bank"></i> <i class="fa-solid fa-piggy-bank"></i>
<div class="token-tooltip"> <div class="tt">
<h4>{{ tithe_token.tooltip_name }}{% if tithe_count > 1 %} <span class="token-count">(×{{ tithe_count }})</span>{% endif %}</h4> <h4 class="tt-title">{{ tithe_token.tooltip_name }}{% if tithe_count > 1 %} <span class="token-count">(×{{ tithe_count }})</span>{% endif %}</h4>
<p>{{ tithe_token.tooltip_description }}</p> <p class="tt-description">{{ tithe_token.tooltip_description }}</p>
<p class="expiry">{{ tithe_token.tooltip_expiry }}</p> <p class="tt-expiry">{{ tithe_token.tooltip_expiry }}</p>
</div> </div>
</div> </div>
{% endif %} {% endif %}
{% if not free_token and not tithe_token %}
<div class="kit-bag-placeholder">
<i class="fa-solid fa-coins"></i>
</div>
{% endif %}
</div> </div>
</div> </div>
{% endif %}