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

De Wiki Gla
Ir para navegação Ir para pesquisar
m (ret)
m
 
(5 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 697: Linha 732:
         }
         }


         .gla-item[data-hidden="true"] .gla-item-main,
         /* Censura ABSOLUTA: enquanto não revelado, todo o conteúdo do card
         .gla-item[data-hidden="true"] .gla-item-title,
          fica click-through (pointer-events:none na .gla-item-main) E é
         .gla-item[data-hidden="true"] .gla-item-desc {
          coberto por um overlay transparente (::after no .gla-item) que
            transition: filter 0.25s ease, background-color 0.25s ease, color 0.25s ease;
          absorve hover/click e tira do alcance os tooltips nativos do
          browser (title/alt) — sem o overlay, alguns navegadores ainda
          exibem o tooltip de elementos com pointer-events:none.
          O ::after fica logo acima do conteúdo (z-index 6) e abaixo do
          ribbon/stamp (z-index 2). Como não tem title/alt nem children,
          hover sobre ele não gera tooltip nenhum. Click bubbla pro
          .gla-item e o listener global revela. */
        .gla-conquistas-root[data-reveal-mode="blur"] .gla-item[data-hidden="true"]:not(.is-revealed) .gla-item-main,
         .gla-conquistas-root[data-reveal-mode="redacted"] .gla-item[data-hidden="true"]:not(.is-revealed) .gla-item-main,
         .gla-conquistas-root[data-reveal-mode="placeholder"] .gla-item[data-hidden="true"]:not(.is-revealed) .gla-item-main,
        .gla-conquistas-root[data-reveal-mode="veil"] .gla-item[data-hidden="true"]:not(.is-revealed) .gla-item-main {
            pointer-events: none;
         }
         }


         /* — BLUR
        .gla-conquistas-root[data-reveal-mode="blur"] .gla-item[data-hidden="true"]:not(.is-revealed),
        .gla-conquistas-root[data-reveal-mode="redacted"] .gla-item[data-hidden="true"]:not(.is-revealed),
        .gla-conquistas-root[data-reveal-mode="placeholder"] .gla-item[data-hidden="true"]:not(.is-revealed),
        .gla-conquistas-root[data-reveal-mode="veil"] .gla-item[data-hidden="true"]:not(.is-revealed) {
            pointer-events: auto;
            position: relative;
        }
 
        /* Overlay transparente que captura hover/click — apenas em blur,
          redacted e placeholder. (Veil tem o seu próprio ::after estilizado
          logo abaixo, e o seletor mais específico de baixo sobrescreve.) */
        .gla-conquistas-root[data-reveal-mode="blur"] .gla-item[data-hidden="true"]:not(.is-revealed)::after,
        .gla-conquistas-root[data-reveal-mode="redacted"] .gla-item[data-hidden="true"]:not(.is-revealed)::after,
        .gla-conquistas-root[data-reveal-mode="placeholder"] .gla-item[data-hidden="true"]:not(.is-revealed)::after {
            content: "";
            position: absolute;
            inset: 0;
            background: transparent;
            pointer-events: auto;
            z-index: 6;
        }
 
        /* Anula QUALQUER interação dentro do card censurado: tooltip dos
          rewards (JS do Widget:Item), tooltip nativo de alt/title em imagens,
          seleção de texto, hover de links na descrição. pointer-events:none
          torna os filhos "transparentes" pra eventos do mouse — o click sobe
          pro .gla-item pai, que dispara o reveal. */
        .gla-conquistas-root[data-reveal-mode="blur"] .gla-item[data-hidden="true"]:not(.is-revealed) .gla-item-main,
        .gla-conquistas-root[data-reveal-mode="redacted"] .gla-item[data-hidden="true"]:not(.is-revealed) .gla-item-main,
        .gla-conquistas-root[data-reveal-mode="placeholder"] .gla-item[data-hidden="true"]:not(.is-revealed) .gla-item-main,
        .gla-conquistas-root[data-reveal-mode="veil"] .gla-item[data-hidden="true"]:not(.is-revealed) .gla-item-main {
            pointer-events: none;
            user-select: none;
            -webkit-user-select: none;
        }
 
        .gla-item[data-hidden="true"] .gla-item-main,
        .gla-item[data-hidden="true"] .gla-item-title,
        .gla-item[data-hidden="true"] .gla-item-desc {
            transition: filter 0.25s ease, background-color 0.25s ease, color 0.25s ease;
        }
 
         /* — BLUR
           Aplicado no .gla-item-main (pai que envolve icon + title + desc +
           Aplicado no .gla-item-main (pai que envolve icon + title + desc +
           reward + spoiler-toggle) — borra TUDO de uma vez. Antes só borrava
           reward + spoiler-toggle) — borra TUDO de uma vez. Antes só borrava
Linha 777: Linha 865:
             backdrop-filter: blur(4px);
             backdrop-filter: blur(4px);
             -webkit-backdrop-filter: blur(4px);
             -webkit-backdrop-filter: blur(4px);
             z-index: 4;
             z-index: 6;
             pointer-events: none;
            /* auto — overlay captura hover/click. Sem isso, mesmo coberto
              visualmente, os items embaixo ainda recebiam hover e o browser
              mostrava tooltip nativo (title/alt). */
             pointer-events: auto;
             transition: opacity 0.25s ease;
             transition: opacity 0.25s ease;
         }
         }
Linha 922: Linha 1 013:
         }
         }


        .gla-conquistas-root[data-hidden-style="ribbon"] .gla-item[data-hidden="true"] .gla-item-main {
         /* ─── Variante D — STAMP ─────────────────────────────────────────────── */
            padding-left: 38px;
         .gla-conquistas-root[data-hidden-style="stamp"] .gla-item[data-hidden="true"]::before {
        }
 
        @media (max-width: 720px) {
            .gla-conquistas-root[data-hidden-style="ribbon"] .gla-item[data-hidden="true"] .gla-item-main {
                padding-left: 38px;
            }
        }
 
         /* ─── Variante D — STAMP ─────────────────────────────────────────────── */
         .gla-conquistas-root[data-hidden-style="stamp"] .gla-item[data-hidden="true"]::before {
             content: "OCULTA";
             content: "OCULTA";
             position: absolute;
             position: absolute;
Linha 1 151: 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;
             });
             });


             // Move cards do invoke source para os painéis corretos
            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);
            }
 
            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 168: 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 {};
                 }
                 }
            }
            var revealedSet = loadRevealed();


                if (tabName !== "coliseu" && hiddenCount === 0) {
            // Abas já com cards no HTML (ex.: renderAll legado) ficam marcadas.
                    if (currentFilter === "hidden" || currentFilter === "normal") {
            // Com renderTab|tab=geral só "geral" vem na primeira carga.
                        currentFilter = "all";
            var loadedTabs = {};
                    }
            var loadingTabs = {};
                    if (filterDefault) {
            Object.keys(validTabs).forEach(function (tabName) {
                        filterDefault.querySelectorAll(".gla-conquistas-filter").forEach(function (p) {
                var list = validTabs[tabName];
                            p.classList.toggle("is-active", p.getAttribute("data-filter") === "all");
                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";
             }
             }


             // Normalização pra busca: lowercase + remove acentos (NFD divide
             function fetchParse(wikitext) {
            // base+diacrítico, regex remove os diacríticos). Assim "voce"
                if (window.mw && mw.Api) {
            // acha "você", "missao" acha "missão", etc.
                    return new mw.Api().post({
            function normalize(s) {
                        action: "parse",
                 return (s || "")
                        text: wikitext,
                     .toLowerCase()
                        contentmodel: "wikitext",
                     .normalize("NFD")
                        disablelimitreport: true
                    .replace(/[̀-ͯ]/g, "");
                    }).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["*"];
                    });
             }
             }


            // Cacheia o "haystack" (title + desc normalizados) no próprio card
             function fetchTabHtml(tabName) {
            // pra não recalcular a cada keystroke. Recomputa só se o conteúdo
                 return fetchParse("{{#invoke:Conquistas|renderTab|tab=" + tabName + "}}");
            // 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() {
             var prefetchRestPromise = null;
                var panel = root.querySelector('.gla-conquistas-panel[data-tab-content="' + currentTab + '"]');
            var prefetchRestStarted = false;
                if (!panel) return;


                // Token-based search: divide a query em palavras e exige que
            function runRewardsDeferred(rootEl) {
                // TODAS apareçam no haystack (title + desc), em qualquer
                 if (!window.glaConquistasProcessRewards) return;
                 // ordem. "voce grand" → tokens ["voce", "grand"] → casa com
                 var run = function () { window.glaConquistasProcessRewards(rootEl); };
                 // "Você entrou na Grand Line pela primeira vez". Acentos e
                 if (window.requestIdleCallback) {
                // case são ignorados via normalize().
                    requestIdleCallback(run, { timeout: 1000 });
                 var tokens = normalize(currentSearch).split(/\s+/).filter(Boolean);
                } else {
                    setTimeout(run, 0);
                }
            }


                 panel.querySelectorAll(".gla-item").forEach(function (card) {
            function applyRevealedToCards(cards) {
                    var matchSearch = true;
                 if (!cards || !cards.length) return;
                    if (tokens.length > 0) {
                for (var i = 0; i < cards.length; i++) {
                        var hay = getHaystack(card);
                    var card = cards[i];
                        for (var i = 0; i < tokens.length; i++) {
                    if (card.getAttribute("data-hidden") !== "true") continue;
                            if (hay.indexOf(tokens[i]) === -1) { matchSearch = false; break; }
                    var id = card.getAttribute("data-id");
                         }
                    if (id && revealedSet[id]) card.classList.add("is-revealed");
                }
            }
 
            function mountCardsFromHtml(html, tabName) {
                var list = validTabs[tabName];
                if (!list || !html) return;
 
                var tmp = document.createElement("div");
                tmp.innerHTML = html;
 
                var moved = [];
                tmp.querySelectorAll(".gla-item").forEach(function (card) {
                    if (getCardTab(card) !== tabName) {
                        mountCardToPanel(card);
                         return;
                     }
                     }
                    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;
                loadedTabs[tabName] = true;
                    if (currentFilter === "normal") matchFilter = !hidden;
                buildTabCaches();
                    else if (currentFilter === "hidden") matchFilter = hidden;
                runRewardsDeferred(list);
                    else if (currentFilter === "onemany") matchFilter = subtype === "onemany";
            }
                    else if (currentFilter === "corrida") matchFilter = subtype === "corrida";


                    card.style.display = (matchSearch && matchFilter) ? "" : "none";
            function mountBundleFromHtml(html) {
                if (!html) return;
                var tmp = document.createElement("div");
                tmp.innerHTML = html;
                tmp.querySelectorAll(".gla-conquistas-source[data-gla-tab]").forEach(function (src) {
                    var tabName = src.getAttribute("data-gla-tab");
                    if (!tabName || !validTabs[tabName]) return;
                    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);
            }
            function startPrefetchRest() {
                if (prefetchRestStarted) return prefetchRestPromise;
                prefetchRestStarted = true;
                prefetchRestPromise = fetchParse("{{#invoke:Conquistas|renderRest|skip=geral}}")
                    .then(function (html) {
                        mountBundleFromHtml(html);
                    })
                    .catch(function () { /* falha silenciosa — aba carrega sob demanda */ });
                return prefetchRestPromise;
             }
             }


             // Busca com debounce
             function schedulePrefetchRest() {
            if (searchInput) {
                var run = function () { startPrefetchRest(); };
                 searchInput.addEventListener("input", function () {
                if (window.requestIdleCallback) {
                     clearTimeout(searchTimeout);
                    requestIdleCallback(run, { timeout: 2000 });
                     searchTimeout = setTimeout(function () {
                 } else {
                         currentSearch = searchInput.value.trim();
                    setTimeout(run, 350);
                         applyVisibility();
                }
                    }, 240);
            }
                });
 
            function loadTabNow(tabName) {
                var list = validTabs[tabName];
                if (list) {
                     list.classList.remove("gla-tab-error");
                     list.classList.add("gla-tab-loading");
                }
                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");
                        }
                    });
             }
             }


             // Filtros (normal / oculta / subtype)
             function ensureTabLoaded(tabName) {
            root.querySelectorAll(".gla-conquistas-filter").forEach(function (pill) {
                if (loadedTabs[tabName]) return Promise.resolve();
                 pill.addEventListener("click", function () {
                if (loadingTabs[tabName]) return loadingTabs[tabName];
                     pill.parentNode.querySelectorAll(".gla-conquistas-filter").forEach(function (p) {
 
                         p.classList.remove("is-active");
                 if (prefetchRestPromise) {
                     loadingTabs[tabName] = prefetchRestPromise.then(function () {
                         if (loadedTabs[tabName]) return;
                        return loadTabNow(tabName);
                     });
                     });
                     pill.classList.add("is-active");
                     return loadingTabs[tabName];
                    currentFilter = pill.getAttribute("data-filter");
                }
                     applyVisibility();
 
                loadingTabs[tabName] = loadTabNow(tabName).then(function () {
                     delete loadingTabs[tabName];
                 });
                 });
             });
                return loadingTabs[tabName];
             }


             // Spoiler — abre/fecha só no botão "Spoiler".
             // Estado global
             // O Lua emite <span role="button"> (MediaWiki não aceita <button>
            var currentSearch = "";
             // em wikitext), por isso aceitamos click + Enter/Space pra
             var currentFilter = "all";
             // preservar semântica de botão acessível.
             var currentTab = "geral";
             function toggleSpoiler(toggle) {
             var searchTimeout = null;
                var card = toggle.closest(".gla-item.has-spoiler");
 
                if (!card) return;
             var filterDefault = root.querySelector("#gla-filter-default");
                var isOpen = card.classList.contains("is-open");
            var filterColiseu = root.querySelector("#gla-filter-coliseu");
                root.querySelectorAll(".gla-item.is-open").forEach(function (c) {
            var searchInput = root.querySelector(".gla-conquistas-search");
                    c.classList.remove("is-open");
 
                });
            function countHiddenInTab(tabName) {
                root.querySelectorAll(".gla-item-spoiler-toggle").forEach(function (btn) {
                return hiddenCountByTab[tabName] || 0;
                    btn.setAttribute("aria-expanded", "false");
                });
                if (!isOpen) {
                    card.classList.add("is-open");
                    toggle.setAttribute("aria-expanded", "true");
                }
             }
             }


             root.addEventListener("click", function (e) {
             // Mostra a barra de filtros só quando faz sentido: na aba Coliseu mostra
                var toggle = e.target.closest(".gla-item-spoiler-toggle");
            // o subset (One Man Army / Corrida). Nas outras, só mostra o filtro
                if (!toggle) return;
            // normal/hidden se a aba tiver pelo menos uma conquista oculta —
                if (e.target.closest(".item-wrapper")) return;
            // se não tiver, esconde pra não poluir.
                e.preventDefault();
            function syncFilterBarForTab(tabName) {
                 toggleSpoiler(toggle);
                 var hiddenCount = countHiddenInTab(tabName);
            });


            root.addEventListener("keydown", function (e) {
                if (filterColiseu) {
                if (e.key !== "Enter" && e.key !== " ") return;
                    filterColiseu.style.display = tabName === "coliseu" ? "" : "none";
                var toggle = e.target.closest(".gla-item-spoiler-toggle");
                }
                if (!toggle) return;
                if (filterDefault) {
                 e.preventDefault();
                    if (tabName === "coliseu") {
                toggleSpoiler(toggle);
                        filterDefault.style.display = "none";
            });
                    } else {
                        filterDefault.style.display = hiddenCount > 0 ? "" : "none";
                    }
                 }


            // ─── Reveal + persistência ────────────────────────────────────────
                if (tabName !== "coliseu" && hiddenCount === 0) {
            // Click no card hidden remove a censura (anti-spoiler) e o estado
                    if (currentFilter === "hidden" || currentFilter === "normal") {
            // é salvo no localStorage. Próxima visita à página, conquistas que
                        currentFilter = "all";
            // o jogador já revelou voltam reveladas — não precisa clicar de novo.
                    }
            //
                    if (filterDefault) {
            // Chave: "glaConquistasRevealed" → JSON com array de data-id.
                        filterDefault.querySelectorAll(".gla-conquistas-filter").forEach(function (p) {
            // Robusto a localStorage indisponível (modo privado, cookies
                            p.classList.toggle("is-active", p.getAttribute("data-filter") === "all");
            // 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) {
            // Normalização pra busca: lowercase + remove acentos (NFD divide
                 try {
            // base+diacrítico, regex remove os diacríticos). Assim "voce"
                     var arr = Object.keys(set);
            // acha "você", "missao" acha "missão", etc.
                     window.localStorage.setItem(REVEAL_STORAGE_KEY, JSON.stringify(arr));
             function normalize(s) {
                } catch (e) { /* localStorage indisponível — ok */ }
                 return (s || "")
                     .toLowerCase()
                     .normalize("NFD")
                    .replace(/[̀-ͯ]/g, "");
             }
             }


             var revealedSet = loadRevealed();
             // Cacheia o "haystack" (title + desc normalizados) no próprio card
            // 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;
            }


             // Re-aplica .is-revealed nos cards que já tavam revelados em
             function applyVisibility() {
            // sessões anteriores. Roda agora (depois do JS já ter movido os
                var cards = cardsByTab[currentTab] || [];
            // cards do source pros painéis).
                if (!cards.length) return;
            root.querySelectorAll('.gla-item[data-hidden="true"][data-id]').forEach(function (card) {
 
                 if (revealedSet[card.getAttribute("data-id")]) {
                // Token-based search: divide a query em palavras e exige que
                    card.classList.add("is-revealed");
                // 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);


            // Click — revela e persiste. Só ativa se data-reveal-mode no root
                cards.forEach(function (card) {
            // estiver definido como blur/redacted/placeholder/veil. Ignora
                    if (getCardTab(card) !== currentTab) {
            // clicks em interativos dentro do card (botão de spoiler, ícones
                        card.style.display = "none";
            // de reward com tooltip, links na descrição, chip "+N" de overflow).
                        return;
            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;
                if (e.target.closest(".gla-item-spoiler-toggle, .item-wrapper, a, .reward-more-chip, .reward-overflow-popover")) return;
                card.classList.add("is-revealed");


                var id = card.getAttribute("data-id");
                    var matchSearch = true;
                if (id) {
                    if (tokens.length > 0) {
                    revealedSet[id] = true;
                        var hay = getHaystack(card);
                    saveRevealed(revealedSet);
                        for (var i = 0; i < tokens.length; i++) {
                }
                            if (hay.indexOf(tokens[i]) === -1) { matchSearch = false; break; }
            });
                        }
                    }


            function abrirAba(nome) {
                    var hidden = card.getAttribute("data-hidden") === "true";
                currentTab = nome;
                    var subtype = (card.getAttribute("data-subtype") || "").toLowerCase();
                currentFilter = "all";
                    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";


                tabs.forEach(function (t) { t.classList.remove("is-active"); });
                    card.style.display = (matchSearch && matchFilter) ? "" : "none";
                panels.forEach(function (p) { p.classList.remove("is-active"); });
                });
            }


                var tab = root.querySelector('.gla-conquistas-tab[data-tab="' + nome + '"]');
            // Busca com debounce
                var panel = root.querySelector('.gla-conquistas-panel[data-tab-content="' + nome + '"]');
            if (searchInput) {
                 if (tab) tab.classList.add("is-active");
                 searchInput.addEventListener("input", function () {
                if (panel) panel.classList.add("is-active");
                    clearTimeout(searchTimeout);
 
                    searchTimeout = setTimeout(function () {
                syncFilterBarForTab(nome);
                        currentSearch = searchInput.value.trim();
 
                         applyVisibility();
                root.querySelectorAll(".gla-conquistas-filter").forEach(function (p) {
                     }, 240);
                    if (p.offsetParent !== null && p.style.display !== "none") {
                         p.classList.toggle("is-active", p.getAttribute("data-filter") === "all");
                     }
                 });
                 });
                applyVisibility();
             }
             }


             tabs.forEach(function (tab) {
             // Filtros (normal / oculta / subtype)
                 tab.addEventListener("click", function () {
            root.querySelectorAll(".gla-conquistas-filter").forEach(function (pill) {
                     abrirAba(tab.getAttribute("data-tab"));
                 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();
                 });
             });
             });


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


    <!-- ─── Overflow das recompensas: "+M" chip + popover ─────────────────────
            function toggleSpoiler(toggle) {
        Para cada .reward-items com mais de MAX itens, esconde do (MAX+1)º em
                var card = toggle.closest(".gla-item.has-spoiler");
        diante, joga os escondidos num popover, e planta um chip "+M" no fim
                if (!card) return;
        da linha. Clique no chip abre o popover ancorado abaixo do chip.
                 var isOpen = (openSpoilerCard === card);
        Click fora fecha. Esc fecha.
                 closeOpenSpoiler();
        É responsabilidade do widget — o Lua não precisa mudar, já que
                 if (!isOpen) {
        .reward-items vem do Widget:Item igual à Predefinição:Reward.
                     card.classList.add("is-open");
    ─────────────────────────────────────────────────────────────────────── -->
                     toggle.setAttribute("aria-expanded", "true");
    <script>
                     openSpoilerCard = card;
        (function () {
                     openSpoilerToggle = toggle;
            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;
 
            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) {
             root.addEventListener("click", function (e) {
                 var rect = chip.getBoundingClientRect();
                 var toggle = e.target.closest(".gla-item-spoiler-toggle");
                var pw = popover.offsetWidth || 240;
                 if (!toggle) return;
                var vpW = window.innerWidth || document.documentElement.clientWidth;
                 if (e.target.closest(".item-wrapper")) return;
                // Alinha pela direita do chip; clampa pra não vazar viewport.
                 e.preventDefault();
                var left = rect.right - pw;
                 toggleSpoiler(toggle);
                 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) {
             root.addEventListener("keydown", function (e) {
                 if (reward.dataset.glaOverflow === "done") return;
                 if (e.key !== "Enter" && e.key !== " ") return;
                 var line = reward.querySelector(".reward-items");
                 var toggle = e.target.closest(".gla-item-spoiler-toggle");
                 if (!line) return;
                 if (!toggle) return;
 
                 e.preventDefault();
                var items = [];
                toggleSpoiler(toggle);
                 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);
            // ─── 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 */ }
            }


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


                var chip = document.createElement("button");
            // Click — revela e persiste. Só ativa se data-reveal-mode no root
                chip.type = "button";
            // estiver definido como blur/redacted/placeholder/veil.
                chip.className = "reward-more-chip";
            //
                chip.setAttribute("aria-expanded", "false");
            // O CSS aplica `pointer-events: none` no .gla-item-main enquanto
                 chip.setAttribute("aria-label", "Ver mais " + hidden.length + " recompensa(s)");
            // a censura está ativa — então click em qualquer ponto interno
                 chip.title = "Ver mais " + hidden.length;
            // (icon/title/desc/items/spoiler/chip+N) cai direto no .gla-item.
                 // Reticências midline (U+22EF) — sinaliza "tem mais" sem
            // Aqui não precisamos mais filtrar interativos: enquanto censurado,
                 // poluir visualmente com número. O aria-label e o title
            // tudo é "click pra revelar"; depois que revela, os filtros internos
                // informam a quantidade exata pra acessibilidade/hover.
            // voltam ao normal porque o seletor :not(.is-revealed) deixa de bater.
                chip.textContent = "";
            root.addEventListener("click", function (e) {
                 line.appendChild(chip);
                 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");


                 // Anexa o popover ao portal global (não ao card) — escapa
                 var id = card.getAttribute("data-id");
                 // overflow:hidden de qualquer ancestral.
                if (id) {
                 ensurePortal().appendChild(popover);
                    revealedSet[id] = true;
                    saveRevealed(revealedSet);
                }
            });
 
            function resetVisibilityAllTabs() {
                root.querySelectorAll(".gla-item[data-tab]").forEach(function (card) {
                    card.style.display = "";
                 });
            }
 
            function finishAbrirAba(nome) {
                syncFilterBarForTab(nome);
                 buildTabCaches();
                resetVisibilityAllTabs();


                 chip.addEventListener("click", function (e) {
                 root.querySelectorAll(".gla-conquistas-filter").forEach(function (p) {
                     e.stopPropagation();
                     if (p.offsetParent !== null && p.style.display !== "none") {
                    var open = chip.getAttribute("aria-expanded") === "true";
                         p.classList.toggle("is-active", p.getAttribute("data-filter") === "all");
                    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;
                     }
                     }
                 });
                 });


                 reward.dataset.glaOverflow = "done";
                 applyVisibility();
             }
             }


             function closeAll() {
             function abrirAba(nome) {
                 document.querySelectorAll(".reward-overflow-popover").forEach(function (p) {
                 currentTab = nome;
                    p.hidden = true;
                currentFilter = "all";
                     p.style.pointerEvents = "none";
                closeOpenSpoiler();
 
                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"));
                 });
                 });
                 document.querySelectorAll(".reward-more-chip").forEach(function (c) {
                 tab.addEventListener("mouseenter", function () {
                     c.setAttribute("aria-expanded", "false");
                     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;
             }
             }
 
 
             // Reposiciona qualquer popover aberto em scroll/resize.
             function positionPopover(popover, chip) {
             function repositionOpen() {
                var rect = chip.getBoundingClientRect();
                 document.querySelectorAll(".reward-more-chip[aria-expanded=\"true\"]").forEach(function (chip) {
                var pw = popover.offsetWidth || 240;
                     var pop = chip.__glaPopover;
                var vpW = window.innerWidth || document.documentElement.clientWidth;
                    if (pop && !pop.hidden) positionPopover(pop, chip);
                // 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