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

De Wiki Gla
Ir para navegação Ir para pesquisar
m
m
 
(4 revisões intermediárias pelo mesmo usuário não estão sendo mostradas)
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 363: Linha 396:
             box-shadow: var(--gla-shadow);
             box-shadow: var(--gla-shadow);
             transition: border-color 0.15s, box-shadow 0.15s;
             transition: border-color 0.15s, box-shadow 0.15s;
            content-visibility: auto;
            contain-intrinsic-size: auto 88px;
         }
         }


Linha 976: Linha 1 011:
             z-index: 2;
             z-index: 2;
             pointer-events: none;
             pointer-events: none;
        }
        .gla-conquistas-root[data-hidden-style="ribbon"] .gla-item[data-hidden="true"] .gla-item-main {
            padding-left: 38px;
        }
        @media (max-width: 720px) {
            .gla-conquistas-root[data-hidden-style="ribbon"] .gla-item[data-hidden="true"] .gla-item-main {
                padding-left: 38px;
            }
         }
         }


Linha 1 207: Linha 1 232:


             var validTabs = {};
             var validTabs = {};
            var panelByTab = {};
            var tabByName = {};
             panels.forEach(function (p) {
             panels.forEach(function (p) {
                 var name = p.getAttribute("data-tab-content");
                 var name = p.getAttribute("data-tab-content");
                 if (name) validTabs[name] = p.querySelector(".gla-list");
                 if (name) {
                    validTabs[name] = p.querySelector(".gla-list");
                    panelByTab[name] = p;
                }
             });
             });
            tabs.forEach(function (t) {
                var name = t.getAttribute("data-tab");
                if (name) tabByName[name] = t;
            });
            function normalizeTabId(tab) {
                return (tab || "").toLowerCase();
            }
            function getCardTab(card) {
                var tag = normalizeTabId(card.getAttribute("data-tab"));
                return validTabs[tag] ? tag : "geral";
            }
            function mountCardToPanel(card) {
                if (!card) return;
                var tag = getCardTab(card);
                var list = validTabs[tag];
                if (list) list.appendChild(card);
            }


             // Move cards do invoke source para os painéis corretos
            function redistributeCardsToPanels() {
                root.querySelectorAll(".gla-item[data-tab]").forEach(function (card) {
                    var list = validTabs[getCardTab(card)];
                    if (list && card.parentNode !== list) list.appendChild(card);
                });
            }
 
             // Move cards do invoke source para os painéis corretos (sempre por data-tab)
             document.querySelectorAll(".gla-item[data-tab]").forEach(function (card) {
             document.querySelectorAll(".gla-item[data-tab]").forEach(function (card) {
                 if (root.contains(card)) return;
                 if (root.contains(card)) return;
                 var tag = (card.getAttribute("data-tab") || "").toLowerCase();
                 mountCardToPanel(card);
                var list = validTabs[tag] || validTabs["geral"];
                if (list) list.appendChild(card);
             });
             });


Linha 1 224: Linha 1 279:
             });
             });


             // Estado global
             // Cache por aba pra evitar querySelectorAll em toda busca/filtro.
             var currentSearch = "";
             var cardsByTab = {};
             var currentFilter = "all";
             var hiddenCountByTab = {};
            var currentTab = "geral";
            var searchTimeout = null;


             var filterDefault = root.querySelector("#gla-filter-default");
             function buildTabCaches() {
            var filterColiseu = root.querySelector("#gla-filter-coliseu");
                redistributeCardsToPanels();
            var searchInput = root.querySelector(".gla-conquistas-search");
                cardsByTab = {};
 
                hiddenCountByTab = {};
            function countHiddenInTab(tabName) {
                Object.keys(validTabs).forEach(function (tabName) {
                var panel = root.querySelector('.gla-conquistas-panel[data-tab-content="' + tabName + '"]');
                    var list = validTabs[tabName];
                if (!panel) return 0;
                    if (!list) {
                var n = 0;
                        cardsByTab[tabName] = [];
                panel.querySelectorAll(".gla-item").forEach(function (card) {
                        hiddenCountByTab[tabName] = 0;
                    if (card.getAttribute("data-hidden") === "true") n++;
                        return;
                    }
                    var cards = [];
                    var hiddenCount = 0;
                    for (var i = 0; i < list.children.length; i++) {
                        var el = list.children[i];
                        if (!(el.classList && el.classList.contains("gla-item"))) continue;
                        if (getCardTab(el) !== tabName) continue;
                        cards.push(el);
                        if (el.getAttribute("data-hidden") === "true") hiddenCount++;
                    }
                    cardsByTab[tabName] = cards;
                    hiddenCountByTab[tabName] = hiddenCount;
                 });
                 });
                return n;
             }
             }
            buildTabCaches();


             // Mostra a barra de filtros só quando faz sentido: na aba Coliseu mostra
             var REVEAL_STORAGE_KEY = "glaConquistasRevealed";
            // 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) {
            function loadRevealed() {
                     filterColiseu.style.display = tabName === "coliseu" ? "" : "none";
                try {
                }
                     var raw = window.localStorage.getItem(REVEAL_STORAGE_KEY);
                if (filterDefault) {
                    if (!raw) return {};
                     if (tabName === "coliseu") {
                    var arr = JSON.parse(raw);
                        filterDefault.style.display = "none";
                     if (!Array.isArray(arr)) return {};
                     } else {
                    var set = {};
                         filterDefault.style.display = hiddenCount > 0 ? "" : "none";
                     for (var i = 0; i < arr.length; i++) {
                         if (arr[i] != null) set[String(arr[i])] = true;
                     }
                     }
                    return set;
                } catch (e) {
                    return {};
                 }
                 }
            }


                if (tabName !== "coliseu" && hiddenCount === 0) {
            var revealedSet = loadRevealed();
                    if (currentFilter === "hidden" || currentFilter === "normal") {
 
                        currentFilter = "all";
            // Abas já com cards no HTML (ex.: renderAll legado) ficam marcadas.
                    }
            // Com renderTab|tab=geral só "geral" vem na primeira carga.
                    if (filterDefault) {
            var loadedTabs = {};
                        filterDefault.querySelectorAll(".gla-conquistas-filter").forEach(function (p) {
            var loadingTabs = {};
                            p.classList.toggle("is-active", p.getAttribute("data-filter") === "all");
            Object.keys(validTabs).forEach(function (tabName) {
                         });
                var list = validTabs[tabName];
                if (!list) return;
                for (var i = 0; i < list.children.length; i++) {
                    var el = list.children[i];
                    if (el.classList && el.classList.contains("gla-item") && getCardTab(el) === tabName) {
                        loadedTabs[tabName] = true;
                         break;
                     }
                     }
                 }
                 }
            });
            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";
                }
                return "/api.php";
            }
            function fetchParse(wikitext) {
                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["*"];
                    });
             }
             }


            // Normalização pra busca: lowercase + remove acentos (NFD divide
             function fetchTabHtml(tabName) {
            // base+diacrítico, regex remove os diacríticos). Assim "voce"
                 return fetchParse("{{#invoke:Conquistas|renderTab|tab=" + tabName + "}}");
            // acha "você", "missao" acha "missão", etc.
             function normalize(s) {
                 return (s || "")
                    .toLowerCase()
                    .normalize("NFD")
                    .replace(/[̀-ͯ]/g, "");
             }
             }


             // Cacheia o "haystack" (title + desc normalizados) no próprio card
             var prefetchRestPromise = null;
             // pra não recalcular a cada keystroke. Recomputa só se o conteúdo
             var prefetchRestStarted = false;
             // do card mudar (não acontece na vida do widget — é estático).
 
            function getHaystack(card) {
             function runRewardsDeferred(rootEl) {
                 var cached = card.__glaHaystack;
                if (!window.glaConquistasProcessRewards) return;
                 if (cached) return cached;
                var run = function () { window.glaConquistasProcessRewards(rootEl); };
                 var titleEl = card.querySelector(".gla-item-title");
                 if (window.requestIdleCallback) {
                var descEl = card.querySelector(".gla-item-desc");
                    requestIdleCallback(run, { timeout: 1000 });
                var titleText = titleEl ? titleEl.textContent : "";
                } else {
                var descText = descEl ? descEl.textContent : "";
                    setTimeout(run, 0);
                cached = normalize(titleText + " " + descText);
                }
                 card.__glaHaystack = cached;
            }
                return cached;
 
            function applyRevealedToCards(cards) {
                 if (!cards || !cards.length) return;
                 for (var i = 0; i < cards.length; i++) {
                    var card = cards[i];
                    if (card.getAttribute("data-hidden") !== "true") continue;
                    var id = card.getAttribute("data-id");
                    if (id && revealedSet[id]) card.classList.add("is-revealed");
                 }
             }
             }


             function applyVisibility() {
             function mountCardsFromHtml(html, tabName) {
                 var panel = root.querySelector('.gla-conquistas-panel[data-tab-content="' + currentTab + '"]');
                 var list = validTabs[tabName];
                 if (!panel) return;
                 if (!list || !html) return;


                 // Token-based search: divide a query em palavras e exige que
                 var tmp = document.createElement("div");
                // TODAS apareçam no haystack (title + desc), em qualquer
                 tmp.innerHTML = html;
                // 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);


                 panel.querySelectorAll(".gla-item").forEach(function (card) {
                 var moved = [];
                    var matchSearch = true;
                tmp.querySelectorAll(".gla-item").forEach(function (card) {
                     if (tokens.length > 0) {
                     if (getCardTab(card) !== tabName) {
                         var hay = getHaystack(card);
                         mountCardToPanel(card);
                         for (var i = 0; i < tokens.length; i++) {
                         return;
                            if (hay.indexOf(tokens[i]) === -1) { matchSearch = false; break; }
                    }
                        }
                    list.appendChild(card);
                    }
                    moved.push(card);
                });


                    var hidden = card.getAttribute("data-hidden") === "true";
                applyRevealedToCards(moved);
                    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";
                loadedTabs[tabName] = true;
                 });
                buildTabCaches();
                 runRewardsDeferred(list);
             }
             }


             // Busca com debounce
             function mountBundleFromHtml(html) {
            if (searchInput) {
                if (!html) return;
                 searchInput.addEventListener("input", function () {
                var tmp = document.createElement("div");
                     clearTimeout(searchTimeout);
                tmp.innerHTML = html;
                     searchTimeout = setTimeout(function () {
                 tmp.querySelectorAll(".gla-conquistas-source[data-gla-tab]").forEach(function (src) {
                         currentSearch = searchInput.value.trim();
                     var tabName = src.getAttribute("data-gla-tab");
                         applyVisibility();
                     if (!tabName || !validTabs[tabName]) return;
                     }, 240);
                    var list = validTabs[tabName];
                    var moved = [];
                    src.querySelectorAll(".gla-item").forEach(function (card) {
                         if (getCardTab(card) !== tabName) {
                            mountCardToPanel(card);
                            return;
                        }
                        list.appendChild(card);
                         moved.push(card);
                     });
                    applyRevealedToCards(moved);
                    loadedTabs[tabName] = true;
                 });
                 });
                buildTabCaches();
                runRewardsDeferred(root);
             }
             }


             // Filtros (normal / oculta / subtype)
             function startPrefetchRest() {
            root.querySelectorAll(".gla-conquistas-filter").forEach(function (pill) {
                if (prefetchRestStarted) return prefetchRestPromise;
                 pill.addEventListener("click", function () {
                prefetchRestStarted = true;
                     pill.parentNode.querySelectorAll(".gla-conquistas-filter").forEach(function (p) {
                 prefetchRestPromise = fetchParse("{{#invoke:Conquistas|renderRest|skip=geral}}")
                         p.classList.remove("is-active");
                     .then(function (html) {
                     });
                         mountBundleFromHtml(html);
                     pill.classList.add("is-active");
                     })
                    currentFilter = pill.getAttribute("data-filter");
                     .catch(function () { /* falha silenciosa — aba carrega sob demanda */ });
                    applyVisibility();
                 return prefetchRestPromise;
                 });
             }
             });


            // Spoiler — abre/fecha só no botão "Spoiler".
             function schedulePrefetchRest() {
            // O Lua emite <span role="button"> (MediaWiki não aceita <button>
                 var run = function () { startPrefetchRest(); };
            // em wikitext), por isso aceitamos click + Enter/Space pra
                 if (window.requestIdleCallback) {
            // preservar semântica de botão acessível.
                     requestIdleCallback(run, { timeout: 2000 });
             function toggleSpoiler(toggle) {
                 } else {
                 var card = toggle.closest(".gla-item.has-spoiler");
                     setTimeout(run, 350);
                if (!card) return;
                var isOpen = card.classList.contains("is-open");
                root.querySelectorAll(".gla-item.is-open").forEach(function (c) {
                    c.classList.remove("is-open");
                });
                 root.querySelectorAll(".gla-item-spoiler-toggle").forEach(function (btn) {
                     btn.setAttribute("aria-expanded", "false");
                 });
                if (!isOpen) {
                     card.classList.add("is-open");
                    toggle.setAttribute("aria-expanded", "true");
                 }
                 }
             }
             }


             root.addEventListener("click", function (e) {
             function loadTabNow(tabName) {
                 var toggle = e.target.closest(".gla-item-spoiler-toggle");
                 var list = validTabs[tabName];
                 if (!toggle) return;
                if (list) {
                if (e.target.closest(".item-wrapper")) return;
                    list.classList.remove("gla-tab-error");
                 e.preventDefault();
                    list.classList.add("gla-tab-loading");
                 toggleSpoiler(toggle);
                 }
            });
                return fetchTabHtml(tabName)
                    .then(function (html) {
                        mountCardsFromHtml(html, tabName);
                        if (list) list.classList.remove("gla-tab-loading");
                    })
                    .catch(function () {
                        if (list) {
                            list.classList.remove("gla-tab-loading");
                            list.classList.add("gla-tab-error");
                        }
                    });
            }
 
            function ensureTabLoaded(tabName) {
                 if (loadedTabs[tabName]) return Promise.resolve();
                 if (loadingTabs[tabName]) return loadingTabs[tabName];


            root.addEventListener("keydown", function (e) {
                if (prefetchRestPromise) {
                if (e.key !== "Enter" && e.key !== " ") return;
                    loadingTabs[tabName] = prefetchRestPromise.then(function () {
                var toggle = e.target.closest(".gla-item-spoiler-toggle");
                        if (loadedTabs[tabName]) return;
                if (!toggle) return;
                        return loadTabNow(tabName);
                e.preventDefault();
                    });
                 toggleSpoiler(toggle);
                    return loadingTabs[tabName];
            });
                 }


            // ─── Reveal + persistência ────────────────────────────────────────
                loadingTabs[tabName] = loadTabNow(tabName).then(function () {
            // Click no card hidden remove a censura (anti-spoiler) e o estado
                    delete loadingTabs[tabName];
            // é salvo no localStorage. Próxima visita à página, conquistas que
                });
            // o jogador já revelou voltam reveladas — não precisa clicar de novo.
                return loadingTabs[tabName];
            //
             }
            // 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() {
             // Estado global
                try {
            var currentSearch = "";
                    var raw = window.localStorage.getItem(REVEAL_STORAGE_KEY);
            var currentFilter = "all";
                    if (!raw) return {};
            var currentTab = "geral";
                    var arr = JSON.parse(raw);
            var searchTimeout = null;
                    if (!Array.isArray(arr)) return {};
 
                    var set = {};
            var filterDefault = root.querySelector("#gla-filter-default");
                    for (var i = 0; i < arr.length; i++) {
            var filterColiseu = root.querySelector("#gla-filter-coliseu");
                        if (arr[i] != null) set[String(arr[i])] = true;
            var searchInput = root.querySelector(".gla-conquistas-search");
                    }
                    return set;
                } catch (e) {
                    return {};
                }
            }


             function saveRevealed(set) {
             function countHiddenInTab(tabName) {
                 try {
                 return hiddenCountByTab[tabName] || 0;
                    var arr = Object.keys(set);
                    window.localStorage.setItem(REVEAL_STORAGE_KEY, JSON.stringify(arr));
                } catch (e) { /* localStorage indisponível — ok */ }
             }
             }


             var revealedSet = loadRevealed();
             // Mostra a barra de filtros só quando faz sentido: na aba Coliseu mostra
            // 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);


            // Re-aplica .is-revealed nos cards que já tavam revelados em
                if (filterColiseu) {
            // sessões anteriores. Roda agora (depois do JS já ter movido os
                    filterColiseu.style.display = tabName === "coliseu" ? "" : "none";
            // cards do source pros painéis).
                }
            root.querySelectorAll('.gla-item[data-hidden="true"][data-id]').forEach(function (card) {
                if (filterDefault) {
                if (revealedSet[card.getAttribute("data-id")]) {
                    if (tabName === "coliseu") {
                     card.classList.add("is-revealed");
                        filterDefault.style.display = "none";
                     } else {
                        filterDefault.style.display = hiddenCount > 0 ? "" : "none";
                    }
                 }
                 }
            });


            // Click — revela e persiste. Só ativa se data-reveal-mode no root
                if (tabName !== "coliseu" && hiddenCount === 0) {
            // estiver definido como blur/redacted/placeholder/veil.
                    if (currentFilter === "hidden" || currentFilter === "normal") {
            //
                        currentFilter = "all";
            // O CSS aplica `pointer-events: none` no .gla-item-main enquanto
                    }
            // a censura está ativa — então click em qualquer ponto interno
                    if (filterDefault) {
            // (icon/title/desc/items/spoiler/chip+N) cai direto no .gla-item.
                        filterDefault.querySelectorAll(".gla-conquistas-filter").forEach(function (p) {
            // Aqui não precisamos mais filtrar interativos: enquanto censurado,
                            p.classList.toggle("is-active", p.getAttribute("data-filter") === "all");
            // tudo é "click pra revelar"; depois que revela, os filtros internos
                        });
            // voltam ao normal porque o seletor :not(.is-revealed) deixa de bater.
                     }
            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");
                if (id) {
                    revealedSet[id] = true;
                     saveRevealed(revealedSet);
                 }
                 }
             });
             }


             function abrirAba(nome) {
             // Normalização pra busca: lowercase + remove acentos (NFD divide
                currentTab = nome;
            // base+diacrítico, regex remove os diacríticos). Assim "voce"
                currentFilter = "all";
            // acha "você", "missao" acha "missão", etc.
 
            function normalize(s) {
                tabs.forEach(function (t) { t.classList.remove("is-active"); });
                 return (s || "")
                panels.forEach(function (p) { p.classList.remove("is-active"); });
                    .toLowerCase()
 
                     .normalize("NFD")
                var tab = root.querySelector('.gla-conquistas-tab[data-tab="' + nome + '"]');
                    .replace(/[̀-ͯ]/g, "");
                var panel = root.querySelector('.gla-conquistas-panel[data-tab-content="' + nome + '"]');
                if (tab) tab.classList.add("is-active");
                if (panel) panel.classList.add("is-active");
 
                syncFilterBarForTab(nome);
 
                 root.querySelectorAll(".gla-conquistas-filter").forEach(function (p) {
                     if (p.offsetParent !== null && p.style.display !== "none") {
                        p.classList.toggle("is-active", p.getAttribute("data-filter") === "all");
                    }
                });
 
                applyVisibility();
             }
             }


             tabs.forEach(function (tab) {
             // Cacheia o "haystack" (title + desc normalizados) no próprio card
                 tab.addEventListener("click", function () {
            // pra não recalcular a cada keystroke. Recomputa só se o conteúdo
                    abrirAba(tab.getAttribute("data-tab"));
            // 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;
             }


             abrirAba("geral");
             function applyVisibility() {
        });
                var cards = cardsByTab[currentTab] || [];
    </script>
                if (!cards.length) return;


    <!-- ─── Overflow das recompensas: "+M" chip + popover ─────────────────────
                // Token-based search: divide a query em palavras e exige que
        Para cada .reward-items com mais de MAX itens, esconde do (MAX+1)º em
                // TODAS apareçam no haystack (title + desc), em qualquer
        diante, joga os escondidos num popover, e planta um chip "+M" no fim
                // ordem. "voce grand" → tokens ["voce", "grand"] → casa com
        da linha. Clique no chip abre o popover ancorado abaixo do chip.
                // "Você entrou na Grand Line pela primeira vez". Acentos e
        Click fora fecha. Esc fecha.
                // case são ignorados via normalize().
        É responsabilidade do widget — o Lua não precisa mudar, já que
                var tokens = normalize(currentSearch).split(/\s+/).filter(Boolean);
        .reward-items vem do Widget:Item igual à Predefinição:Reward.
 
    ─────────────────────────────────────────────────────────────────────── -->
                cards.forEach(function (card) {
    <script>
                    if (getCardTab(card) !== currentTab) {
        (function () {
                        card.style.display = "none";
            var MAX_VISIBLE = 4;
                        return;
            // 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;


            function ensurePortal() {
                    var matchSearch = true;
                if (portal && portal.isConnected) return portal;
                    if (tokens.length > 0) {
                portal = document.querySelector(".gla-conquistas-portal");
                        var hay = getHaystack(card);
                if (!portal) {
                        for (var i = 0; i < tokens.length; i++) {
                     portal = document.createElement("div");
                            if (hay.indexOf(tokens[i]) === -1) { matchSearch = false; break; }
                     portal.className = "gla-conquistas-portal";
                        }
                     portal.style.position = "absolute";
                    }
                     portal.style.top = "0";
 
                     portal.style.left = "0";
                     var hidden = card.getAttribute("data-hidden") === "true";
                     portal.style.width = "0";
                     var subtype = (card.getAttribute("data-subtype") || "").toLowerCase();
                    portal.style.height = "0";
                     var matchFilter = true;
                     portal.style.pointerEvents = "none";
                     if (currentFilter === "normal") matchFilter = !hidden;
                    portal.style.zIndex = "9998";
                     else if (currentFilter === "hidden") matchFilter = hidden;
                     document.body.appendChild(portal);
                     else if (currentFilter === "onemany") matchFilter = subtype === "onemany";
                 }
                     else if (currentFilter === "corrida") matchFilter = subtype === "corrida";
                return portal;
 
                     card.style.display = (matchSearch && matchFilter) ? "" : "none";
                 });
             }
             }


             function positionPopover(popover, chip) {
             // Busca com debounce
                 var rect = chip.getBoundingClientRect();
            if (searchInput) {
                var pw = popover.offsetWidth || 240;
                 searchInput.addEventListener("input", function () {
                var vpW = window.innerWidth || document.documentElement.clientWidth;
                    clearTimeout(searchTimeout);
                // Alinha pela direita do chip; clampa pra não vazar viewport.
                    searchTimeout = setTimeout(function () {
                var left = rect.right - pw;
                        currentSearch = searchInput.value.trim();
                if (left < 8) left = 8;
                        applyVisibility();
                if (left + pw > vpW - 8) left = vpW - 8 - pw;
                    }, 240);
                popover.style.left = left + "px";
                 });
                 popover.style.top = (rect.bottom + 6) + "px";
                popover.style.pointerEvents = "auto";
             }
             }


             function processOne(reward) {
             // Filtros (normal / oculta / subtype)
                 if (reward.dataset.glaOverflow === "done") return;
            root.querySelectorAll(".gla-conquistas-filter").forEach(function (pill) {
                var line = reward.querySelector(".reward-items");
                 pill.addEventListener("click", function () {
                 if (!line) return;
                    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();
                 });
            });
 
            // Spoiler — abre/fecha só no botão "Spoiler".
            // O Lua emite <span role="button"> (MediaWiki não aceita <button>
            // em wikitext), por isso aceitamos click + Enter/Space pra
            // preservar semântica de botão acessível.
            var openSpoilerCard = null;
            var openSpoilerToggle = null;


                var items = [];
            function closeOpenSpoiler() {
                 for (var i = 0; i < line.children.length; i++) {
                 if (openSpoilerCard) {
                     var c = line.children[i];
                     openSpoilerCard.classList.remove("is-open");
                    if (c.classList && c.classList.contains("item-wrapper")) items.push(c);
                    openSpoilerCard = null;
                 }
                 }
                 if (items.length <= MAX_VISIBLE) {
                 if (openSpoilerToggle) {
                     reward.dataset.glaOverflow = "done";
                     openSpoilerToggle.setAttribute("aria-expanded", "false");
                     return;
                     openSpoilerToggle = null;
                 }
                 }
            }


                var hidden = items.slice(MAX_VISIBLE);
            function toggleSpoiler(toggle) {
 
                 var card = toggle.closest(".gla-item.has-spoiler");
                 var popover = document.createElement("div");
                 if (!card) return;
                 popover.className = "reward-overflow-popover";
                 var isOpen = (openSpoilerCard === card);
                 popover.hidden = true;
                 closeOpenSpoiler();
                 hidden.forEach(function (el) { popover.appendChild(el); });
                if (!isOpen) {
                    card.classList.add("is-open");
                    toggle.setAttribute("aria-expanded", "true");
                    openSpoilerCard = card;
                    openSpoilerToggle = toggle;
                }
            }


                var chip = document.createElement("button");
            root.addEventListener("click", function (e) {
                 chip.type = "button";
                 var toggle = e.target.closest(".gla-item-spoiler-toggle");
                chip.className = "reward-more-chip";
                 if (!toggle) return;
                 chip.setAttribute("aria-expanded", "false");
                 if (e.target.closest(".item-wrapper")) return;
                 chip.setAttribute("aria-label", "Ver mais " + hidden.length + " recompensa(s)");
                 e.preventDefault();
                 chip.title = "Ver mais " + hidden.length;
                 toggleSpoiler(toggle);
                 // 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
            root.addEventListener("keydown", function (e) {
                 // overflow:hidden de qualquer ancestral.
                 if (e.key !== "Enter" && e.key !== " ") return;
                 ensurePortal().appendChild(popover);
                 var toggle = e.target.closest(".gla-item-spoiler-toggle");
                 if (!toggle) return;
                e.preventDefault();
                toggleSpoiler(toggle);
            });


                chip.addEventListener("click", function (e) {
            // ─── Reveal + persistência ────────────────────────────────────────
                    e.stopPropagation();
            function saveRevealed(set) {
                     var open = chip.getAttribute("aria-expanded") === "true";
                try {
                     closeAll();
                     var arr = Object.keys(set);
                    if (!open) {
                     window.localStorage.setItem(REVEAL_STORAGE_KEY, JSON.stringify(arr));
                        popover.hidden = false;
                } catch (e) { /* localStorage indisponível — ok */ }
                        // Render primeiro (offsetWidth precisa do layout),
            }
                        // depois posiciona.
                        positionPopover(popover, chip);
                        chip.setAttribute("aria-expanded", "true");
                        chip.__glaPopover = popover;
                    }
                });


                 reward.dataset.glaOverflow = "done";
            // Re-aplica .is-revealed nos cards que já tavam revelados em
             }
            // sessões anteriores. Roda agora (depois do JS já ter movido os
            // cards do source pros painéis).
            root.querySelectorAll('.gla-item[data-hidden="true"][data-id]').forEach(function (card) {
                 if (revealedSet[card.getAttribute("data-id")]) {
                    card.classList.add("is-revealed");
                }
             });


             function closeAll() {
             // Click — revela e persiste. Só ativa se data-reveal-mode no root
                 document.querySelectorAll(".reward-overflow-popover").forEach(function (p) {
            // estiver definido como blur/redacted/placeholder/veil.
                     p.hidden = true;
            //
                     p.style.pointerEvents = "none";
            // O CSS aplica `pointer-events: none` no .gla-item-main enquanto
            // a censura está ativa — então click em qualquer ponto interno
            // (icon/title/desc/items/spoiler/chip+N) cai direto no .gla-item.
            // Aqui não precisamos mais filtrar interativos: enquanto censurado,
            // tudo é "click pra revelar"; depois que revela, os filtros internos
            // voltam ao normal porque o seletor :not(.is-revealed) deixa de bater.
            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");
                if (id) {
                     revealedSet[id] = true;
                     saveRevealed(revealedSet);
                }
            });
 
            function resetVisibilityAllTabs() {
                root.querySelectorAll(".gla-item[data-tab]").forEach(function (card) {
                    card.style.display = "";
                 });
                 });
                 document.querySelectorAll(".reward-more-chip").forEach(function (c) {
            }
                     c.setAttribute("aria-expanded", "false");
 
            function finishAbrirAba(nome) {
                 syncFilterBarForTab(nome);
                buildTabCaches();
                resetVisibilityAllTabs();
 
                root.querySelectorAll(".gla-conquistas-filter").forEach(function (p) {
                     if (p.offsetParent !== null && p.style.display !== "none") {
                        p.classList.toggle("is-active", p.getAttribute("data-filter") === "all");
                    }
                 });
                 });
                applyVisibility();
             }
             }
 
 
             // Reposiciona qualquer popover aberto em scroll/resize.
             function abrirAba(nome) {
             function repositionOpen() {
                currentTab = nome;
                 document.querySelectorAll(".reward-more-chip[aria-expanded=\"true\"]").forEach(function (chip) {
                currentFilter = "all";
                     var pop = chip.__glaPopover;
                closeOpenSpoiler();
                    if (pop && !pop.hidden) positionPopover(pop, chip);
 
                tabs.forEach(function (t) { t.classList.remove("is-active"); });
                panels.forEach(function (p) { p.classList.remove("is-active"); });
 
                var tab = tabByName[nome];
                var panel = panelByTab[nome];
                if (tab) tab.classList.add("is-active");
                if (panel) panel.classList.add("is-active");
 
                ensureTabLoaded(nome).then(function () {
                    finishAbrirAba(nome);
                });
            }
 
            tabs.forEach(function (tab) {
                tab.addEventListener("click", function () {
                    abrirAba(tab.getAttribute("data-tab"));
                });
                tab.addEventListener("mouseenter", function () {
                    var nome = tab.getAttribute("data-tab");
                    if (nome && !loadedTabs[nome]) ensureTabLoaded(nome);
                });
            });
 
            schedulePrefetchRest();
            abrirAba("geral");
        });
    </script>
 
    <!-- ─── Overflow das recompensas: "+M" chip + popover ─────────────────────
        Para cada .reward-items com mais de MAX itens, esconde do (MAX+1)º em
        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 = null;
                    return;
                }
                // fallback defensivo (estado legado inesperado)
                document.querySelectorAll(".reward-overflow-popover").forEach(function (p) {
                    p.hidden = true;
                    p.style.pointerEvents = "none";
                });
                 document.querySelectorAll(".reward-more-chip[aria-expanded=\"true\"]").forEach(function (c) {
                     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();
            });
 
            function processRewardsIn(rootEl) {
                if (!rootEl) return;
                rootEl.querySelectorAll(".gla-item-reward").forEach(processOne);
            }
 
            window.glaConquistasProcessRewards = processRewardsIn;
 
            function scheduleProcessRewards(rootEl) {
                var run = function () { processRewardsIn(rootEl || document); };
                if (window.requestIdleCallback) {
                    requestIdleCallback(run, { timeout: 1200 });
                } else {
                    setTimeout(run, 0);
                }
            }
 
            if (document.readyState === "loading") {
                document.addEventListener("DOMContentLoaded", function () {
                    scheduleProcessRewards(document);
                 });
                 });
            }
            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();
            });
            function processAll() {
                document.querySelectorAll(".gla-item-reward").forEach(processOne);
            }
            if (document.readyState === "loading") {
                document.addEventListener("DOMContentLoaded", processAll);
             } else {
             } else {
                 processAll();
                 scheduleProcessRewards(document);
             }
             }


            document.addEventListener("click", function (e) {
                if (e.target.closest(".reward-overflow-popover, .reward-more-chip")) return;
                closeAll();
            });
            document.addEventListener("keydown", function (e) {
                if (e.key === "Escape") closeAll();
            });
         })();
         })();
     </script>
     </script>
</includeonly>
</includeonly>

Edição atual tal como às 04h21min de 21 de maio de 2026