feat: assuntos do cartão em árvore brasil › estado › cidade › tópico

This commit is contained in:
2026-09-30 19:08:06 +02:00
parent aedef870c1
commit 42d56ba092
+75 -10
View File
@@ -378,11 +378,18 @@
#card-title { font-size: 1.02rem; color: #f5f5f0; } #card-title { font-size: 1.02rem; color: #f5f5f0; }
#card-date { font-size: .85rem; color: #b9b9b0; margin-top: 2px; } #card-date { font-size: .85rem; color: #b9b9b0; margin-top: 2px; }
#card-contrib { font-size: .82rem; color: #cdd8e0; margin-top: 10px; } #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:empty { display: none; }
#card-tags span { #card-tags .tn { margin: 0; padding: 0 0 0 13px; border-left: 1px solid rgba(127,168,201,.28); }
font-family: system-ui, sans-serif; font-size: .64rem; letter-spacing: .02em; line-height: 1.7; #card-tags > .tn { padding-left: 0; border-left: 0; }
padding: 0 8px; border-radius: 999px; color: #b8c6d1; #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); background: rgba(127,168,201,.12); border: 1px solid rgba(127,168,201,.22);
} }
#card-footer { #card-footer {
@@ -1276,12 +1283,7 @@ function showCard(data) {
document.getElementById('card-contrib').textContent = meta.contributor ? meta.contributor : ''; document.getElementById('card-contrib').textContent = meta.contributor ? meta.contributor : '';
// this piece's dc.subject, one chip each (accent/case duplicates folded) // this piece's dc.subject, one chip each (accent/case duplicates folded)
const tagsEl = document.getElementById('card-tags'); const tagsEl = document.getElementById('card-tags');
tagsEl.replaceChildren(); renderSubjectTree(tagsEl, meta);
subjectsOf(meta).forEach(t => {
const chip = document.createElement('span');
chip.textContent = t;
tagsEl.appendChild(chip);
});
document.getElementById('card-handle').textContent = data.handle || ''; document.getElementById('card-handle').textContent = data.handle || '';
document.getElementById('card-link').href = data.handle ? sourceUrl(data.handle) : '#'; document.getElementById('card-link').href = data.handle ? sourceUrl(data.handle) : '#';
document.getElementById('card-rights').textContent = meta.rights ? `Direitos: ${meta.rights}` : ''; document.getElementById('card-rights').textContent = meta.rights ? `Direitos: ${meta.rights}` : '';
@@ -1418,6 +1420,69 @@ function subjectsOf(meta) {
return pt.length ? pt : all; 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) { function setupFilters(mapData) {
const entries = Object.values(mapData.map); const entries = Object.values(mapData.map);
const instCounts = new Map(), typeCounts = new Map(), authorGroups = new Map(), subjectGroups = new Map(); const instCounts = new Map(), typeCounts = new Map(), authorGroups = new Map(), subjectGroups = new Map();