Add family tree page, linked from the NPC's family name

- /npcs/family/{family} (app_npc_family) renders a parent->child tree for
  everyone sharing that family name; co-parents who are both in the family
  are paired into one couple node, elders first
- FamilyTreeBuilder does the flat-list -> tree assembly
- the NPC detail page's family name (subtitle + Details row) now links there
- .family-tree CSS: nested list with connector lines, small portrait avatars

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Frank
2026-09-06 16:23:03 +02:00
co-authored by Claude Sonnet 5
parent d43746970b
commit 574d903715
5 changed files with 229 additions and 2 deletions
+50
View File
@@ -0,0 +1,50 @@
{% extends 'base.html.twig' %}
{% block title %}{{ family }} family · NPC's · dmtools{% endblock %}
{% macro person(npc, muted) %}
<a class="ft-person{{ muted ? ' text-body-secondary' : '' }}" href="{{ path('app_npc_show', {id: npc.id}) }}">
<img src="{{ asset(npc.portraitPath) }}" alt="" width="34" height="42"
class="rounded object-fit-cover" onerror="this.remove()">
<span class="ft-name">{{ npc.name }}</span>
<span class="ft-meta">{{ npc.age ?? '?' }}{% if npc.occupation %} · {{ npc.occupation }}{% endif %}</span>
</a>
{% endmacro %}
{% macro node(n) %}
{% import _self as tree %}
<li>
<div class="ft-couple">
{{ tree.person(n.npc) }}
{% if n.spouse %}
<span class="ft-amp">&amp;</span>
{{ tree.person(n.spouse) }}
{% endif %}
</div>
{% if n.children %}
<ul>
{% for c in n.children %}{{ tree.node(c) }}{% endfor %}
</ul>
{% endif %}
</li>
{% endmacro %}
{% import _self as tree %}
{% block body %}
<div class="d-flex justify-content-between align-items-start mb-3">
<div>
<a href="{{ path('app_npcs') }}" class="text-decoration-none small">&larr; All NPC's</a>
<h1 class="h3 mb-0">The {{ family }} family</h1>
<span class="text-body-secondary">{{ count }} member{{ count == 1 ? '' : 's' }}</span>
</div>
</div>
<div class="card shadow-sm">
<div class="card-body">
<ul class="family-tree">
{% for r in roots %}{{ tree.node(r) }}{% endfor %}
</ul>
</div>
</div>
{% endblock %}
+5 -2
View File
@@ -12,7 +12,9 @@
<div>
<a href="{{ path('app_npcs') }}" class="text-decoration-none small">&larr; All NPC's</a>
<h1 class="h3 mb-0">{{ npc.name }}</h1>
<span class="text-body-secondary">{{ npc.family }}{% if npc.occupation %} · {{ npc.occupation }}{% endif %}</span>
<span class="text-body-secondary">
<a href="{{ path('app_npc_family', {family: npc.family}) }}">{{ npc.family }}</a>{% if npc.occupation %} · {{ npc.occupation }}{% endif %}
</span>
</div>
</div>
<a href="{{ path('app_npc_edit', {id: npc.id}) }}" class="btn btn-outline-secondary btn-sm">Edit details</a>
@@ -24,7 +26,8 @@
<div class="card-header">Details</div>
<div class="card-body">
<dl class="row mb-0">
<dt class="col-5">Family</dt><dd class="col-7">{{ npc.family }}</dd>
<dt class="col-5">Family</dt>
<dd class="col-7"><a href="{{ path('app_npc_family', {family: npc.family}) }}">{{ npc.family }}</a> family tree</dd>
<dt class="col-5">Age</dt><dd class="col-7">{{ npc.age ?? '—' }}</dd>
<dt class="col-5">Race</dt><dd class="col-7">{{ npc.race.value }}</dd>
<dt class="col-5">Occupation</dt><dd class="col-7">{{ npc.occupation ?? '—' }}</dd>