Mudanças entre as edições de "Widget:Conquistas"

De Wiki Gla
Ir para navegação Ir para pesquisar
m
m
Linha 6: Linha 6:
           {{#widget:Item}}
           {{#widget:Item}}
           {{#widget:Conquistas}}
           {{#widget:Conquistas}}
           {{#invoke:Conquistas|renderAll}}
           {{#invoke:Conquistas|renderTab|tab=geral}}


       Widget:Item é quem traz a CSS dos ícones (.reward-wrapper,
       Widget:Item é quem traz a CSS dos ícones (.reward-wrapper,
Linha 73: Linha 73:


         <!--
         <!--
           Painéis vazios. O Módulo:Conquistas (#invoke renderAll) emite os
           Painéis vazios. O Módulo:Conquistas (#invoke renderTab) emite os
           cards FORA do widget (Smarty não reparsa wikitext, então não dá pra
           cards FORA do widget (Smarty não reparsa wikitext, então não dá pra
           passar como parâmetro). O JS abaixo move cada card pro painel certo
           passar como parâmetro). O JS abaixo move cada card pro painel certo
Linha 80: Linha 80:
           Uso na página da wiki:
           Uso na página da wiki:
               {{#widget:Conquistas}}
               {{#widget:Conquistas}}
               {{#invoke:Conquistas|renderAll}}
               {{#invoke:Conquistas|renderTab|tab=geral}}
         -->
         -->
         <div class="gla-conquistas-panel is-active" data-tab-content="geral">
         <div class="gla-conquistas-panel is-active" data-tab-content="geral">
Linha 351: Linha 351:
             flex-direction: column;
             flex-direction: column;
             gap: 10px;
             gap: 10px;
            position: relative;
            min-height: 0;
        }
        .gla-list.gla-tab-loading {
            min-height: 88px;
            opacity: 0.55;
            pointer-events: none;
        }
        .gla-list.gla-tab-loading::after {
            content: "Carregando...";
            position: absolute;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            font-size: 13px;
            font-weight: 600;
            color: var(--gla-ink-2);
            letter-spacing: 0.04em;
        }
        .gla-list.gla-tab-error::after {
            content: "Não foi possível carregar esta aba. Recarregue a página.";
            position: absolute;
            top: 50%;
            left: 50%;
            transform: translate(-50%, -50%);
            width: min(320px, 90%);
            text-align: center;
            font-size: 13px;
            color: #b45309;
            line-height: 1.4;
         }
         }


Linha 1 251: Linha 1 284:
             buildTabCaches();
             buildTabCaches();


            // Estado global
             var REVEAL_STORAGE_KEY = "glaConquistasRevealed";
             var currentSearch = "";
            var currentFilter = "all";
            var currentTab = "geral";
            var searchTimeout = null;


             var filterDefault = root.querySelector("#gla-filter-default");
             function loadRevealed() {
            var filterColiseu = root.querySelector("#gla-filter-coliseu");
                try {
            var searchInput = root.querySelector(".gla-conquistas-search");
                    var raw = window.localStorage.getItem(REVEAL_STORAGE_KEY);
 
                    if (!raw) return {};
            function countHiddenInTab(tabName) {
                    var arr = JSON.parse(raw);
                return hiddenCountByTab[tabName] || 0;
                    if (!Array.isArray(arr)) return {};
                    var set = {};
                    for (var i = 0; i < arr.length; i++) {
                        if (arr[i] != null) set[String(arr[i])] = true;
                    }
                    return set;
                } catch (e) {
                    return {};
                }
             }
             }


             // Mostra a barra de filtros só quando faz sentido: na aba Coliseu mostra
             var revealedSet = loadRevealed();
            // o subset (One Man Army / Corrida). Nas outras, só mostra o filtro
            // normal/hidden se a aba tiver pelo menos uma conquista oculta —
            // se não tiver, esconde pra não poluir.
            function syncFilterBarForTab(tabName) {
                var hiddenCount = countHiddenInTab(tabName);


                 if (filterColiseu) {
            // Abas já com cards no HTML (ex.: renderAll legado) ficam marcadas.
                     filterColiseu.style.display = tabName === "coliseu" ? "" : "none";
            // Com renderTab|tab=geral só "geral" vem na primeira carga.
            var loadedTabs = {};
            var loadingTabs = {};
            Object.keys(validTabs).forEach(function (tabName) {
                var list = validTabs[tabName];
                 if (!list) return;
                for (var i = 0; i < list.children.length; i++) {
                     if (list.children[i].classList && list.children[i].classList.contains("gla-item")) {
                        loadedTabs[tabName] = true;
                        return;
                    }
                }
            });
 
            function getApiUrl() {
                if (window.mw && mw.util && mw.util.wikiScript) {
                    return mw.util.wikiScript("api");
                }
                var path = window.location.pathname || "";
                if (path.indexOf("/index.php") !== -1) {
                    return path.split("/index.php")[0] + "/api.php";
                 }
                 }
                 if (filterDefault) {
                 return "/api.php";
                    if (tabName === "coliseu") {
            }
                        filterDefault.style.display = "none";
 
                     } else {
            function fetchTabHtml(tabName) {
                         filterDefault.style.display = hiddenCount > 0 ? "" : "none";
                var wikitext = "{{#invoke:Conquistas|renderTab|tab=" + tabName + "}}";
                     }
                if (window.mw && mw.Api) {
                    return new mw.Api().post({
                        action: "parse",
                        text: wikitext,
                        contentmodel: "wikitext",
                        disablelimitreport: true
                     }).then(function (data) {
                         return data.parse.text["*"];
                     });
                 }
                 }
                var body = new URLSearchParams();
                body.set("action", "parse");
                body.set("format", "json");
                body.set("text", wikitext);
                body.set("contentmodel", "wikitext");
                body.set("disablelimitreport", "1");
                return fetch(getApiUrl(), {
                    method: "POST",
                    credentials: "same-origin",
                    headers: { "Content-Type": "application/x-www-form-urlencoded; charset=UTF-8" },
                    body: body.toString()
                })
                    .then(function (res) { return res.json(); })
                    .then(function (data) {
                        if (data.error) throw new Error(data.error.info || "API parse failed");
                        return data.parse.text["*"];
                    });
            }


                 if (tabName !== "coliseu" && hiddenCount === 0) {
            function applyRevealedToCards(cards) {
                     if (currentFilter === "hidden" || currentFilter === "normal") {
                 if (!cards || !cards.length) return;
                        currentFilter = "all";
                for (var i = 0; i < cards.length; i++) {
                     }
                    var card = cards[i];
                    if (filterDefault) {
                     if (card.getAttribute("data-hidden") !== "true") continue;
                        filterDefault.querySelectorAll(".gla-conquistas-filter").forEach(function (p) {
                     var id = card.getAttribute("data-id");
                            p.classList.toggle("is-active", p.getAttribute("data-filter") === "all");
                    if (id && revealedSet[id]) card.classList.add("is-revealed");
                        });
                    }
                 }
                 }
             }
             }


             // Normalização pra busca: lowercase + remove acentos (NFD divide
             function mountCardsFromHtml(html, tabName) {
            // base+diacrítico, regex remove os diacríticos). Assim "voce"
                var list = validTabs[tabName];
            // acha "você", "missao" acha "missão", etc.
                if (!list || !html) return;
            function normalize(s) {
 
                return (s || "")
                var tmp = document.createElement("div");
                     .toLowerCase()
                tmp.innerHTML = html;
                    .normalize("NFD")
 
                     .replace(/[̀-ͯ]/g, "");
                var moved = [];
            }
                tmp.querySelectorAll(".gla-item").forEach(function (card) {
                    list.appendChild(card);
                     moved.push(card);
                });
 
                applyRevealedToCards(moved);
 
                if (window.glaConquistasProcessRewards) {
                     window.glaConquistasProcessRewards(list);
                }


            // Cacheia o "haystack" (title + desc normalizados) no próprio card
                 buildTabCaches();
            // pra não recalcular a cada keystroke. Recomputa só se o conteúdo
            // do card mudar (não acontece na vida do widget — é estático).
            function getHaystack(card) {
                var cached = card.__glaHaystack;
                if (cached) return cached;
                var titleEl = card.querySelector(".gla-item-title");
                var descEl = card.querySelector(".gla-item-desc");
                 var titleText = titleEl ? titleEl.textContent : "";
                var descText = descEl ? descEl.textContent : "";
                cached = normalize(titleText + " " + descText);
                card.__glaHaystack = cached;
                return cached;
             }
             }


             function applyVisibility() {
             function ensureTabLoaded(tabName) {
                 var cards = cardsByTab[currentTab] || [];
                 if (loadedTabs[tabName]) return Promise.resolve();
                 if (!cards.length) return;
                 if (loadingTabs[tabName]) return loadingTabs[tabName];


                 // Token-based search: divide a query em palavras e exige que
                 var list = validTabs[tabName];
                 // TODAS apareçam no haystack (title + desc), em qualquer
                 if (list) {
                // ordem. "voce grand" → tokens ["voce", "grand"] → casa com
                    list.classList.remove("gla-tab-error");
                // "Você entrou na Grand Line pela primeira vez". Acentos e
                    list.classList.add("gla-tab-loading");
                // case são ignorados via normalize().
                }
                var tokens = normalize(currentSearch).split(/\s+/).filter(Boolean);


                 cards.forEach(function (card) {
                 loadingTabs[tabName] = fetchTabHtml(tabName)
                    var matchSearch = true;
                    .then(function (html) {
                    if (tokens.length > 0) {
                        mountCardsFromHtml(html, tabName);
                         var hay = getHaystack(card);
                        loadedTabs[tabName] = true;
                         for (var i = 0; i < tokens.length; i++) {
                        if (list) list.classList.remove("gla-tab-loading");
                             if (hay.indexOf(tokens[i]) === -1) { matchSearch = false; break; }
                         delete loadingTabs[tabName];
                    })
                    .catch(function () {
                         if (list) {
                             list.classList.remove("gla-tab-loading");
                            list.classList.add("gla-tab-error");
                         }
                         }
                    }
                        delete loadingTabs[tabName];
 
                     });
                    var hidden = card.getAttribute("data-hidden") === "true";
                     var subtype = (card.getAttribute("data-subtype") || "").toLowerCase();
                    var matchFilter = true;
                    if (currentFilter === "normal") matchFilter = !hidden;
                    else if (currentFilter === "hidden") matchFilter = hidden;
                    else if (currentFilter === "onemany") matchFilter = subtype === "onemany";
                    else if (currentFilter === "corrida") matchFilter = subtype === "corrida";


                    card.style.display = (matchSearch && matchFilter) ? "" : "none";
                 return loadingTabs[tabName];
                 });
             }
             }


             // Busca com debounce
             // Estado global
             if (searchInput) {
             var currentSearch = "";
                searchInput.addEventListener("input", function () {
            var currentFilter = "all";
                    clearTimeout(searchTimeout);
            var currentTab = "geral";
                    searchTimeout = setTimeout(function () {
            var searchTimeout = null;
                        currentSearch = searchInput.value.trim();
 
                        applyVisibility();
            var filterDefault = root.querySelector("#gla-filter-default");
                    }, 240);
            var filterColiseu = root.querySelector("#gla-filter-coliseu");
                 });
            var searchInput = root.querySelector(".gla-conquistas-search");
 
            function countHiddenInTab(tabName) {
                 return hiddenCountByTab[tabName] || 0;
             }
             }


             // Filtros (normal / oculta / subtype)
             // Mostra a barra de filtros só quando faz sentido: na aba Coliseu mostra
             root.querySelectorAll(".gla-conquistas-filter").forEach(function (pill) {
            // o subset (One Man Army / Corrida). Nas outras, só mostra o filtro
                 pill.addEventListener("click", function () {
            // normal/hidden se a aba tiver pelo menos uma conquista oculta
                    pill.parentNode.querySelectorAll(".gla-conquistas-filter").forEach(function (p) {
            // se não tiver, esconde pra não poluir.
                        p.classList.remove("is-active");
             function syncFilterBarForTab(tabName) {
                    });
                 var hiddenCount = countHiddenInTab(tabName);
                    pill.classList.add("is-active");
                    currentFilter = pill.getAttribute("data-filter");
                    applyVisibility();
                });
            });


            // Spoiler — abre/fecha só no botão "Spoiler".
                if (filterColiseu) {
            // O Lua emite <span role="button"> (MediaWiki não aceita <button>
                    filterColiseu.style.display = tabName === "coliseu" ? "" : "none";
            // em wikitext), por isso aceitamos click + Enter/Space pra
                }
            // preservar semântica de botão acessível.
                if (filterDefault) {
            var openSpoilerCard = null;
                    if (tabName === "coliseu") {
            var openSpoilerToggle = null;
                        filterDefault.style.display = "none";
 
                     } else {
            function closeOpenSpoiler() {
                        filterDefault.style.display = hiddenCount > 0 ? "" : "none";
                if (openSpoilerCard) {
                    }
                    openSpoilerCard.classList.remove("is-open");
                     openSpoilerCard = null;
                 }
                 }
                if (openSpoilerToggle) {
                    openSpoilerToggle.setAttribute("aria-expanded", "false");
                    openSpoilerToggle = null;
                }
            }


            function toggleSpoiler(toggle) {
                if (tabName !== "coliseu" && hiddenCount === 0) {
                var card = toggle.closest(".gla-item.has-spoiler");
                    if (currentFilter === "hidden" || currentFilter === "normal") {
                if (!card) return;
                        currentFilter = "all";
                var isOpen = (openSpoilerCard === card);
                    }
                closeOpenSpoiler();
                    if (filterDefault) {
                if (!isOpen) {
                        filterDefault.querySelectorAll(".gla-conquistas-filter").forEach(function (p) {
                    card.classList.add("is-open");
                            p.classList.toggle("is-active", p.getAttribute("data-filter") === "all");
                    toggle.setAttribute("aria-expanded", "true");
                        });
                    openSpoilerCard = card;
                     }
                     openSpoilerToggle = toggle;
                 }
                 }
             }
             }


             root.addEventListener("click", function (e) {
             // Normalização pra busca: lowercase + remove acentos (NFD divide
                 var toggle = e.target.closest(".gla-item-spoiler-toggle");
            // base+diacrítico, regex remove os diacríticos). Assim "voce"
                if (!toggle) return;
            // acha "você", "missao" acha "missão", etc.
                if (e.target.closest(".item-wrapper")) return;
            function normalize(s) {
                e.preventDefault();
                 return (s || "")
                toggleSpoiler(toggle);
                    .toLowerCase()
             });
                    .normalize("NFD")
                    .replace(/[̀-ͯ]/g, "");
             }


             root.addEventListener("keydown", function (e) {
             // Cacheia o "haystack" (title + desc normalizados) no próprio card
                if (e.key !== "Enter" && e.key !== " ") return;
             // pra não recalcular a cada keystroke. Recomputa só se o conteúdo
                var toggle = e.target.closest(".gla-item-spoiler-toggle");
             // do card mudar (não acontece na vida do widget — é estático).
                if (!toggle) return;
             function getHaystack(card) {
                e.preventDefault();
                 var cached = card.__glaHaystack;
                toggleSpoiler(toggle);
                if (cached) return cached;
            });
                var titleEl = card.querySelector(".gla-item-title");
 
                var descEl = card.querySelector(".gla-item-desc");
            // ─── Reveal + persistência ────────────────────────────────────────
                var titleText = titleEl ? titleEl.textContent : "";
            // Click no card hidden remove a censura (anti-spoiler) e o estado
                var descText = descEl ? descEl.textContent : "";
             // é salvo no localStorage. Próxima visita à página, conquistas que
                cached = normalize(titleText + " " + descText);
            // o jogador já revelou voltam reveladas — não precisa clicar de novo.
                card.__glaHaystack = cached;
             //
                 return cached;
            // Chave: "glaConquistasRevealed" → JSON com array de data-id.
            // Robusto a localStorage indisponível (modo privado, cookies
            // bloqueados) — só falha silenciosamente.
            var REVEAL_STORAGE_KEY = "glaConquistasRevealed";
 
             function loadRevealed() {
                 try {
                    var raw = window.localStorage.getItem(REVEAL_STORAGE_KEY);
                    if (!raw) return {};
                    var arr = JSON.parse(raw);
                    if (!Array.isArray(arr)) return {};
                    var set = {};
                    for (var i = 0; i < arr.length; i++) {
                        if (arr[i] != null) set[String(arr[i])] = true;
                    }
                    return set;
                 } catch (e) {
                    return {};
                }
             }
             }


             function saveRevealed(set) {
             function applyVisibility() {
                 try {
                 var cards = cardsByTab[currentTab] || [];
                    var arr = Object.keys(set);
                if (!cards.length) return;
                    window.localStorage.setItem(REVEAL_STORAGE_KEY, JSON.stringify(arr));
                } catch (e) { /* localStorage indisponível — ok */ }
            }


            var revealedSet = loadRevealed();
                // Token-based search: divide a query em palavras e exige que
                // TODAS apareçam no haystack (title + desc), em qualquer
                // ordem. "voce grand" → tokens ["voce", "grand"] → casa com
                // "Você entrou na Grand Line pela primeira vez". Acentos e
                // case são ignorados via normalize().
                var tokens = normalize(currentSearch).split(/\s+/).filter(Boolean);


            // Re-aplica .is-revealed nos cards que já tavam revelados em
                cards.forEach(function (card) {
            // sessões anteriores. Roda agora (depois do JS já ter movido os
                    var matchSearch = true;
            // cards do source pros painéis).
                    if (tokens.length > 0) {
            root.querySelectorAll('.gla-item[data-hidden="true"][data-id]').forEach(function (card) {
                        var hay = getHaystack(card);
                if (revealedSet[card.getAttribute("data-id")]) {
                        for (var i = 0; i < tokens.length; i++) {
                    card.classList.add("is-revealed");
                            if (hay.indexOf(tokens[i]) === -1) { matchSearch = false; break; }
                }
                        }
            });
                    }


            // Click — revela e persiste. Só ativa se data-reveal-mode no root
                    var hidden = card.getAttribute("data-hidden") === "true";
            // estiver definido como blur/redacted/placeholder/veil.
                    var subtype = (card.getAttribute("data-subtype") || "").toLowerCase();
            //
                    var matchFilter = true;
            // O CSS aplica `pointer-events: none` no .gla-item-main enquanto
                    if (currentFilter === "normal") matchFilter = !hidden;
            // a censura está ativa — então click em qualquer ponto interno
                    else if (currentFilter === "hidden") matchFilter = hidden;
            // (icon/title/desc/items/spoiler/chip+N) cai direto no .gla-item.
                    else if (currentFilter === "onemany") matchFilter = subtype === "onemany";
            // Aqui não precisamos mais filtrar interativos: enquanto censurado,
                    else if (currentFilter === "corrida") matchFilter = subtype === "corrida";
            // tudo é "click pra revelar"; depois que revela, os filtros internos
 
            // voltam ao normal porque o seletor :not(.is-revealed) deixa de bater.
                    card.style.display = (matchSearch && matchFilter) ? "" : "none";
            root.addEventListener("click", function (e) {
                });
                var mode = root.getAttribute("data-reveal-mode") || "none";
            }
                if (mode === "none") return;
                var card = e.target.closest('.gla-item[data-hidden="true"]');
                if (!card) return;
                if (card.classList.contains("is-revealed")) return;
                card.classList.add("is-revealed");


                 var id = card.getAttribute("data-id");
            // Busca com debounce
                 if (id) {
            if (searchInput) {
                     revealedSet[id] = true;
                 searchInput.addEventListener("input", function () {
                     saveRevealed(revealedSet);
                    clearTimeout(searchTimeout);
                 }
                    searchTimeout = setTimeout(function () {
                        currentSearch = searchInput.value.trim();
                        applyVisibility();
                    }, 240);
                });
            }
 
            // Filtros (normal / oculta / subtype)
            root.querySelectorAll(".gla-conquistas-filter").forEach(function (pill) {
                 pill.addEventListener("click", function () {
                    pill.parentNode.querySelectorAll(".gla-conquistas-filter").forEach(function (p) {
                        p.classList.remove("is-active");
                     });
                    pill.classList.add("is-active");
                    currentFilter = pill.getAttribute("data-filter");
                     applyVisibility();
                 });
             });
             });


             function abrirAba(nome) {
             // Spoiler — abre/fecha só no botão "Spoiler".
                currentTab = nome;
            // O Lua emite <span role="button"> (MediaWiki não aceita <button>
                currentFilter = "all";
            // em wikitext), por isso aceitamos click + Enter/Space pra
                closeOpenSpoiler();
            // preservar semântica de botão acessível.
            var openSpoilerCard = null;
            var openSpoilerToggle = null;


                 tabs.forEach(function (t) { t.classList.remove("is-active"); });
            function closeOpenSpoiler() {
                 panels.forEach(function (p) { p.classList.remove("is-active"); });
                 if (openSpoilerCard) {
                    openSpoilerCard.classList.remove("is-open");
                    openSpoilerCard = null;
                }
                 if (openSpoilerToggle) {
                    openSpoilerToggle.setAttribute("aria-expanded", "false");
                    openSpoilerToggle = null;
                }
            }


                var tab = tabByName[nome];
            function toggleSpoiler(toggle) {
                 var panel = panelByTab[nome];
                 var card = toggle.closest(".gla-item.has-spoiler");
                if (tab) tab.classList.add("is-active");
                 if (!card) return;
                 if (panel) panel.classList.add("is-active");
                 var isOpen = (openSpoilerCard === card);
 
                 closeOpenSpoiler();
                 syncFilterBarForTab(nome);
                if (!isOpen) {
 
                    card.classList.add("is-open");
                 root.querySelectorAll(".gla-conquistas-filter").forEach(function (p) {
                    toggle.setAttribute("aria-expanded", "true");
                    if (p.offsetParent !== null && p.style.display !== "none") {
                     openSpoilerCard = card;
                        p.classList.toggle("is-active", p.getAttribute("data-filter") === "all");
                    openSpoilerToggle = toggle;
                     }
                 }
                });
 
                 applyVisibility();
             }
             }


             tabs.forEach(function (tab) {
             root.addEventListener("click", function (e) {
                 tab.addEventListener("click", function () {
                 var toggle = e.target.closest(".gla-item-spoiler-toggle");
                    abrirAba(tab.getAttribute("data-tab"));
                if (!toggle) return;
                 });
                if (e.target.closest(".item-wrapper")) return;
                 e.preventDefault();
                toggleSpoiler(toggle);
             });
             });


             abrirAba("geral");
             root.addEventListener("keydown", function (e) {
        });
                if (e.key !== "Enter" && e.key !== " ") return;
    </script>
                var toggle = e.target.closest(".gla-item-spoiler-toggle");
                if (!toggle) return;
                e.preventDefault();
                toggleSpoiler(toggle);
            });
 
            // ─── Reveal + persistência ────────────────────────────────────────
            function saveRevealed(set) {
                try {
                    var arr = Object.keys(set);
                    window.localStorage.setItem(REVEAL_STORAGE_KEY, JSON.stringify(arr));
                } catch (e) { /* localStorage indisponível — ok */ }
            }


    <!-- ─── Overflow das recompensas: "+M" chip + popover ─────────────────────
            // Re-aplica .is-revealed nos cards que já tavam revelados em
        Para cada .reward-items com mais de MAX itens, esconde do (MAX+1)º em
            // sessões anteriores. Roda agora (depois do JS ter movido os
        diante, joga os escondidos num popover, e planta um chip "+M" no fim
             // cards do source pros painéis).
        da linha. Clique no chip abre o popover ancorado abaixo do chip.
             root.querySelectorAll('.gla-item[data-hidden="true"][data-id]').forEach(function (card) {
        Click fora fecha. Esc fecha.
                 if (revealedSet[card.getAttribute("data-id")]) {
        É responsabilidade do widget — o Lua não precisa mudar, que
                     card.classList.add("is-revealed");
        .reward-items vem do Widget:Item igual à Predefinição:Reward.
    ─────────────────────────────────────────────────────────────────────── -->
    <script>
        (function () {
            var MAX_VISIBLE = 4;
             // Container portal: vive direto em document.body, fora de
             // qualquer .gla-item / .gla-item-reward. Necessário pra que
            // cards com overflow:hidden (ex.: variante "ribbon") não
            // recortem o popover nem os tooltips dos itens dentro dele.
            var portal = null;
            var activeChip = null;
            var activePopover = null;
 
            function ensurePortal() {
                 if (portal && portal.isConnected) return portal;
                portal = document.querySelector(".gla-conquistas-portal");
                if (!portal) {
                     portal = document.createElement("div");
                    portal.className = "gla-conquistas-portal";
                    portal.style.position = "absolute";
                    portal.style.top = "0";
                    portal.style.left = "0";
                    portal.style.width = "0";
                    portal.style.height = "0";
                    portal.style.pointerEvents = "none";
                    portal.style.zIndex = "9998";
                    document.body.appendChild(portal);
                 }
                 }
                return portal;
             });
             }


             function positionPopover(popover, chip) {
             // Click — revela e persiste. Só ativa se data-reveal-mode no root
                var rect = chip.getBoundingClientRect();
            // estiver definido como blur/redacted/placeholder/veil.
                 var pw = popover.offsetWidth || 240;
            //
                 var vpW = window.innerWidth || document.documentElement.clientWidth;
            // O CSS aplica `pointer-events: none` no .gla-item-main enquanto
                // Alinha pela direita do chip; clampa pra não vazar viewport.
            // a censura está ativa — então click em qualquer ponto interno
                 var left = rect.right - pw;
            // (icon/title/desc/items/spoiler/chip+N) cai direto no .gla-item.
                 if (left < 8) left = 8;
            // Aqui não precisamos mais filtrar interativos: enquanto censurado,
                 if (left + pw > vpW - 8) left = vpW - 8 - pw;
            // tudo é "click pra revelar"; depois que revela, os filtros internos
                popover.style.left = left + "px";
            // voltam ao normal porque o seletor :not(.is-revealed) deixa de bater.
                 popover.style.top = (rect.bottom + 6) + "px";
            root.addEventListener("click", function (e) {
                popover.style.pointerEvents = "auto";
                 var mode = root.getAttribute("data-reveal-mode") || "none";
            }
                 if (mode === "none") return;
                 var card = e.target.closest('.gla-item[data-hidden="true"]');
                 if (!card) return;
                 if (card.classList.contains("is-revealed")) return;
                 card.classList.add("is-revealed");


            function processOne(reward) {
                 var id = card.getAttribute("data-id");
                if (reward.dataset.glaOverflow === "done") return;
                 if (id) {
                 var line = reward.querySelector(".reward-items");
                     revealedSet[id] = true;
                 if (!line) return;
                     saveRevealed(revealedSet);
 
                var items = [];
                for (var i = 0; i < line.children.length; i++) {
                     var c = line.children[i];
                     if (c.classList && c.classList.contains("item-wrapper")) items.push(c);
                }
                if (items.length <= MAX_VISIBLE) {
                    reward.dataset.glaOverflow = "done";
                    return;
                 }
                 }
            });


                 var hidden = items.slice(MAX_VISIBLE);
            function finishAbrirAba(nome) {
                 syncFilterBarForTab(nome);


                 var popover = document.createElement("div");
                 root.querySelectorAll(".gla-conquistas-filter").forEach(function (p) {
                popover.className = "reward-overflow-popover";
                    if (p.offsetParent !== null && p.style.display !== "none") {
                 popover.hidden = true;
                        p.classList.toggle("is-active", p.getAttribute("data-filter") === "all");
                 hidden.forEach(function (el) { popover.appendChild(el); });
                    }
                 });
 
                 applyVisibility();
            }


                var chip = document.createElement("button");
            function abrirAba(nome) {
                 chip.type = "button";
                 currentTab = nome;
                chip.className = "reward-more-chip";
                 currentFilter = "all";
                 chip.setAttribute("aria-expanded", "false");
                 closeOpenSpoiler();
                chip.setAttribute("aria-label", "Ver mais " + hidden.length + " recompensa(s)");
                chip.title = "Ver mais " + hidden.length;
                // Reticências midline (U+22EF) — sinaliza "tem mais" sem
                // poluir visualmente com número. O aria-label e o title
                // informam a quantidade exata pra acessibilidade/hover.
                chip.textContent = "";
                 line.appendChild(chip);


                 // Anexa o popover ao portal global (não ao card) — escapa
                 tabs.forEach(function (t) { t.classList.remove("is-active"); });
                 // overflow:hidden de qualquer ancestral.
                 panels.forEach(function (p) { p.classList.remove("is-active"); });
                ensurePortal().appendChild(popover);


                 chip.addEventListener("click", function (e) {
                 var tab = tabByName[nome];
                    e.stopPropagation();
                var panel = panelByTab[nome];
                    var open = chip.getAttribute("aria-expanded") === "true";
                if (tab) tab.classList.add("is-active");
                    closeAll();
                if (panel) panel.classList.add("is-active");
                    if (!open) {
 
                        popover.hidden = false;
                ensureTabLoaded(nome).then(function () {
                        // Render primeiro (offsetWidth precisa do layout),
                    finishAbrirAba(nome);
                        // depois posiciona.
                        positionPopover(popover, chip);
                        chip.setAttribute("aria-expanded", "true");
                        chip.__glaPopover = popover;
                        activeChip = chip;
                        activePopover = popover;
                    }
                 });
                 });
            }


                 reward.dataset.glaOverflow = "done";
            tabs.forEach(function (tab) {
             }
                 tab.addEventListener("click", function () {
                    abrirAba(tab.getAttribute("data-tab"));
                });
             });


             function closeAll() {
            abrirAba("geral");
                 if (activePopover) {
        });
                     activePopover.hidden = true;
    </script>
                     activePopover.style.pointerEvents = "none";
 
                     activePopover = null;
    <!-- ─── Overflow das recompensas: "+M" chip + popover ─────────────────────
                 }
        Para cada .reward-items com mais de MAX itens, esconde do (MAX+1)º em
                 if (activeChip) {
        diante, joga os escondidos num popover, e planta um chip "+M" no fim
        da linha. Clique no chip abre o popover ancorado abaixo do chip.
        Click fora fecha. Esc fecha.
        É responsabilidade do widget — o Lua não precisa mudar, já que
        .reward-items vem do Widget:Item igual à Predefinição:Reward.
    ─────────────────────────────────────────────────────────────────────── -->
    <script>
        (function () {
            var MAX_VISIBLE = 4;
            // Container portal: vive direto em document.body, fora de
            // qualquer .gla-item / .gla-item-reward. Necessário pra que
            // cards com overflow:hidden (ex.: variante "ribbon") não
            // recortem o popover nem os tooltips dos itens dentro dele.
            var portal = null;
            var activeChip = null;
            var activePopover = null;
 
            function ensurePortal() {
                if (portal && portal.isConnected) return portal;
                portal = document.querySelector(".gla-conquistas-portal");
                if (!portal) {
                    portal = document.createElement("div");
                    portal.className = "gla-conquistas-portal";
                    portal.style.position = "absolute";
                    portal.style.top = "0";
                    portal.style.left = "0";
                    portal.style.width = "0";
                    portal.style.height = "0";
                    portal.style.pointerEvents = "none";
                    portal.style.zIndex = "9998";
                    document.body.appendChild(portal);
                }
                return portal;
            }
 
            function positionPopover(popover, chip) {
                var rect = chip.getBoundingClientRect();
                var pw = popover.offsetWidth || 240;
                var vpW = window.innerWidth || document.documentElement.clientWidth;
                // Alinha pela direita do chip; clampa pra não vazar viewport.
                var left = rect.right - pw;
                if (left < 8) left = 8;
                if (left + pw > vpW - 8) left = vpW - 8 - pw;
                popover.style.left = left + "px";
                popover.style.top = (rect.bottom + 6) + "px";
                popover.style.pointerEvents = "auto";
            }
 
            function processOne(reward) {
                if (reward.dataset.glaOverflow === "done") return;
                var line = reward.querySelector(".reward-items");
                if (!line) return;
 
                var items = [];
                for (var i = 0; i < line.children.length; i++) {
                    var c = line.children[i];
                    if (c.classList && c.classList.contains("item-wrapper")) items.push(c);
                }
                if (items.length <= MAX_VISIBLE) {
                    reward.dataset.glaOverflow = "done";
                    return;
                }
 
                var hidden = items.slice(MAX_VISIBLE);
 
                var popover = document.createElement("div");
                popover.className = "reward-overflow-popover";
                popover.hidden = true;
                hidden.forEach(function (el) { popover.appendChild(el); });
 
                var chip = document.createElement("button");
                chip.type = "button";
                chip.className = "reward-more-chip";
                chip.setAttribute("aria-expanded", "false");
                chip.setAttribute("aria-label", "Ver mais " + hidden.length + " recompensa(s)");
                chip.title = "Ver mais " + hidden.length;
                // Reticências midline (U+22EF) — sinaliza "tem mais" sem
                // poluir visualmente com número. O aria-label e o title
                // informam a quantidade exata pra acessibilidade/hover.
                chip.textContent = "⋯";
                line.appendChild(chip);
 
                // Anexa o popover ao portal global (não ao card) — escapa
                // overflow:hidden de qualquer ancestral.
                ensurePortal().appendChild(popover);
 
                chip.addEventListener("click", function (e) {
                    e.stopPropagation();
                    var open = chip.getAttribute("aria-expanded") === "true";
                    closeAll();
                    if (!open) {
                        popover.hidden = false;
                        // Render primeiro (offsetWidth precisa do layout),
                        // depois posiciona.
                        positionPopover(popover, chip);
                        chip.setAttribute("aria-expanded", "true");
                        chip.__glaPopover = popover;
                        activeChip = chip;
                        activePopover = popover;
                    }
                });
 
                reward.dataset.glaOverflow = "done";
            }
 
             function closeAll() {
                 if (activePopover) {
                     activePopover.hidden = true;
                     activePopover.style.pointerEvents = "none";
                     activePopover = null;
                 }
                 if (activeChip) {
                     activeChip.setAttribute("aria-expanded", "false");
                     activeChip.setAttribute("aria-expanded", "false");
                     activeChip = null;
                     activeChip = null;
                     return;
                     return;
                 }
                 }
                 // fallback defensivo (estado legado inesperado)
                 // fallback defensivo (estado legado inesperado)
                 document.querySelectorAll(".reward-overflow-popover").forEach(function (p) {
                 document.querySelectorAll(".reward-overflow-popover").forEach(function (p) {
                     p.hidden = true;
                     p.hidden = true;
                     p.style.pointerEvents = "none";
                     p.style.pointerEvents = "none";
                 });
                 });
                 document.querySelectorAll(".reward-more-chip[aria-expanded=\"true\"]").forEach(function (c) {
                 document.querySelectorAll(".reward-more-chip[aria-expanded=\"true\"]").forEach(function (c) {
                     c.setAttribute("aria-expanded", "false");
                     c.setAttribute("aria-expanded", "false");
                 });
                 });
             }
             }
 
            // Reposiciona qualquer popover aberto em scroll/resize.
            function repositionOpen() {
                if (activeChip && activePopover && !activePopover.hidden) {
                    positionPopover(activePopover, activeChip);
                }
            }
            window.addEventListener("resize", repositionOpen);
            window.addEventListener("scroll", repositionOpen, true);
 
            // Click fora do chip e do popover fecha tudo.
            document.addEventListener("click", function (e) {
                if (e.target.closest(".reward-more-chip")) return;
                if (e.target.closest(".reward-overflow-popover")) return;
                closeAll();
            });
            document.addEventListener("keydown", function (e) {
                if (e.key === "Escape") closeAll();
            });


            // Reposiciona qualquer popover aberto em scroll/resize.
             function processRewardsIn(rootEl) {
             function repositionOpen() {
                 if (!rootEl) return;
                 if (activeChip && activePopover && !activePopover.hidden) {
                rootEl.querySelectorAll(".gla-item-reward").forEach(processOne);
                    positionPopover(activePopover, activeChip);
                }
             }
             }
            window.addEventListener("resize", repositionOpen);
            window.addEventListener("scroll", repositionOpen, true);


             // Click fora do chip e do popover fecha tudo.
             window.glaConquistasProcessRewards = processRewardsIn;
            document.addEventListener("click", function (e) {
                if (e.target.closest(".reward-more-chip")) return;
                if (e.target.closest(".reward-overflow-popover")) return;
                closeAll();
            });
            document.addEventListener("keydown", function (e) {
                if (e.key === "Escape") closeAll();
            });


             function processAll() {
             function processAll() {
                 document.querySelectorAll(".gla-item-reward").forEach(processOne);
                 processRewardsIn(document);
             }
             }



Edição das 04h17min de 21 de maio de 2026