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();