diff --git a/web/wall.html b/web/wall.html
index 4eea2dd..3202312 100644
--- a/web/wall.html
+++ b/web/wall.html
@@ -378,11 +378,18 @@
#card-title { font-size: 1.02rem; color: #f5f5f0; }
#card-date { font-size: .85rem; color: #b9b9b0; margin-top: 2px; }
#card-contrib { font-size: .82rem; color: #cdd8e0; margin-top: 10px; }
- #card-tags { display: flex; flex-wrap: wrap; gap: 5px; margin-top: 12px; }
+ #card-tags { margin-top: 12px; font-family: system-ui, sans-serif; font-size: .7rem; line-height: 1.55; color: #b8c6d1; }
#card-tags:empty { display: none; }
- #card-tags span {
- font-family: system-ui, sans-serif; font-size: .64rem; letter-spacing: .02em; line-height: 1.7;
- padding: 0 8px; border-radius: 999px; color: #b8c6d1;
+ #card-tags .tn { margin: 0; padding: 0 0 0 13px; border-left: 1px solid rgba(127,168,201,.28); }
+ #card-tags > .tn { padding-left: 0; border-left: 0; }
+ #card-tags .tl { padding: 1px 0; overflow-wrap: anywhere; }
+ #card-tags .tn.place > .tl { color: #dcebf6; font-weight: 600; }
+ #card-tags .tn.topic > .tl { color: #b8c6d1; }
+ #card-tags .tn > .tl::before { content: '└ '; color: rgba(127,168,201,.5); }
+ #card-tags > .tn > .tl::before { content: ''; }
+ #card-tags .to { display: flex; flex-wrap: wrap; gap: 5px; margin-top: 8px; }
+ #card-tags .to span {
+ font-size: .64rem; line-height: 1.7; padding: 0 8px; border-radius: 999px;
background: rgba(127,168,201,.12); border: 1px solid rgba(127,168,201,.22);
}
#card-footer {
@@ -1276,12 +1283,7 @@ function showCard(data) {
document.getElementById('card-contrib').textContent = meta.contributor ? meta.contributor : '';
// this piece's dc.subject, one chip each (accent/case duplicates folded)
const tagsEl = document.getElementById('card-tags');
- tagsEl.replaceChildren();
- subjectsOf(meta).forEach(t => {
- const chip = document.createElement('span');
- chip.textContent = t;
- tagsEl.appendChild(chip);
- });
+ renderSubjectTree(tagsEl, meta);
document.getElementById('card-handle').textContent = data.handle || '';
document.getElementById('card-link').href = data.handle ? sourceUrl(data.handle) : '#';
document.getElementById('card-rights').textContent = meta.rights ? `Direitos: ${meta.rights}` : '';
@@ -1418,6 +1420,69 @@ function subjectsOf(meta) {
return pt.length ? pt : all;
}
+// Brasil › estado › cidade › tópico, derived from the tag text: "Tópico - Cidade (UF)"
+// carries the whole path. Tags without a recognisable place stay out of the tree.
+const UF = { AC: 'Acre', AL: 'Alagoas', AP: 'Amapá', AM: 'Amazonas', BA: 'Bahia', CE: 'Ceará', DF: 'Distrito Federal',
+ ES: 'Espírito Santo', GO: 'Goiás', MA: 'Maranhão', MT: 'Mato Grosso', MS: 'Mato Grosso do Sul', MG: 'Minas Gerais',
+ PA: 'Pará', PB: 'Paraíba', PR: 'Paraná', PE: 'Pernambuco', PI: 'Piauí', RJ: 'Rio de Janeiro', RN: 'Rio Grande do Norte',
+ RS: 'Rio Grande do Sul', RO: 'Rondônia', RR: 'Roraima', SC: 'Santa Catarina', SP: 'São Paulo', SE: 'Sergipe', TO: 'Tocantins' };
+const STATE_BY_KEY = new Map(Object.values(UF).map(n => [deburr(n), n]));
+
+function subjectTree(meta) {
+ const root = { kids: new Map() };
+ const put = path => path.reduce((n, label) => {
+ if (!n.kids.has(label)) n.kids.set(label, { kids: new Map(), place: false });
+ return n.kids.get(label);
+ }, root);
+ const others = [];
+ let hasPlace = false;
+ subjectsOf(meta).forEach(tag => {
+ const key = deburr(tag).replace(/\s*\(estado\)$/, '');
+ if (key === 'brasil') { put(['Brasil']).place = true; hasPlace = true; return; }
+ if (STATE_BY_KEY.has(key)) {
+ const st = STATE_BY_KEY.get(key);
+ put(['Brasil']).place = true; put(['Brasil', st]).place = true; hasPlace = true; return;
+ }
+ const parts = tag.split(/\s+-\s+/);
+ // "Espírito Santo (Estado) - Colônias": a state carrying a topic
+ const sm = parts[0].match(/^(.+?)\s*\(Estado\)$/i);
+ if (sm && STATE_BY_KEY.has(deburr(sm[1]))) {
+ const path = ['Brasil', STATE_BY_KEY.get(deburr(sm[1]))];
+ path.forEach((_, i) => { put(path.slice(0, i + 1)).place = true; });
+ parts.slice(1).forEach((_, i) => put(path.concat(parts.slice(1, i + 2))));
+ hasPlace = true; return;
+ }
+ const m = parts[parts.length - 1].match(/^(.+?)\s*\(([A-Z]{2})\)$/);
+ if (!m || !UF[m[2]]) { others.push(tag); return; }
+ const path = ['Brasil', UF[m[2]], m[1]];
+ path.slice(0, 3).forEach((_, i) => { put(path.slice(0, i + 1)).place = true; });
+ parts.slice(0, -1).forEach((_, i) => { put(path.concat(parts.slice(0, i + 1))); });
+ hasPlace = true;
+ });
+ // a lone "Brasil" is no tree — it reads better as a chip with the rest
+ if (hasPlace && root.kids.get('Brasil').kids.size === 0) { root.kids.clear(); others.push('Brasil'); }
+ return { root, others };
+}
+
+function renderSubjectTree(el, meta) {
+ el.replaceChildren();
+ const { root, others } = subjectTree(meta);
+ const draw = (node, label, into) => {
+ const d = document.createElement('div');
+ d.className = 'tn ' + (node.place ? 'place' : 'topic');
+ const l = document.createElement('div'); l.className = 'tl'; l.textContent = label;
+ d.appendChild(l);
+ node.kids.forEach((kid, name) => draw(kid, name, d));
+ into.appendChild(d);
+ };
+ root.kids.forEach((kid, name) => draw(kid, name, el));
+ if (others.length) {
+ const o = document.createElement('div'); o.className = 'to';
+ others.forEach(t => { const c = document.createElement('span'); c.textContent = t; o.appendChild(c); });
+ el.appendChild(o);
+ }
+}
+
function setupFilters(mapData) {
const entries = Object.values(mapData.map);
const instCounts = new Map(), typeCounts = new Map(), authorGroups = new Map(), subjectGroups = new Map();