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>
This commit is contained in:
Disco DeDisco
2026-04-15 22:16:50 -04:00
parent 71ef3dcb7f
commit de4ac60aec
20 changed files with 520 additions and 29 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,7 +69,7 @@
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 = '';
}); });
} }

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,31 @@
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'),
}

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

@@ -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

@@ -105,10 +105,12 @@
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;
} }

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;
@@ -66,7 +67,8 @@
transform: translateX(-50%); transform: translateX(-50%);
} }
&:hover .token-tooltip { &:hover .token-tooltip,
&:hover .tt {
display: block; display: block;
} }
} }

View File

@@ -0,0 +1,69 @@
<div class="tt">
<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

@@ -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>
@@ -43,13 +43,13 @@
{% 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 %}
@@ -72,13 +72,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,10 +90,10 @@
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 %}