Star Signs funcionam como árvores de talentos únicas, permitindo que os jogadores desbloqueiem e combinem diferentes constelações para criar estilos de progressão e habilidades personalizadas.
Os pontos de constelação são compartilhados entre todos os personagens da conta, porém o jogador tem total liberdade para criar a sua própria build, conforme preferir.
Pontos de Constelação
Atualmente, estão disponíveis 45 pontos de talento para serem utilizados, além de 1 ponto especial, que é desbloqueado ao investir 30 pontos de talento na mesma árvore.
Podem ser obtidos através de:
Completar as três primeiras Rocket Quests de Johto = 1 ponto de constelação.
Completar a quarta Rocket Quest de Johto, a Time Traveler Quest = 3 pontos de constelação.
Completar a quinta Rocket Quest de Johto = 3 pontos de constelação.
Completar a The Rainbow Hero Quest = 5 pontos de constelação.
Completar o Regional level de Johto (level 200) = 2 pontos de constelação.
Completar a Echoes of Mt. Silver Quest = 1 ponto de constelação.
Completar a Liga Pokémon de Johto = 3 pontos de constelação.
Completar todos os unown trials de Johto = 1 Ponto de constelação.
Gastar energia azul de Mystery Dungeon (até 7 etapas) = 1 Ponto de constelação em cada etapa (totalizando 7 ao final).
Coletar metade dos Rainbow Orbs = 1 ponto de constelação.
Coletar todos Rainbow Orbs = 1 ponto de constelação.
Derrotar todos os World Bosses de Johto = 1 ponto de constelação.
Alcançar Level 605 = 2 pontos de constelação.
Alcançar Level 610 = 3 pontos de constelação.
Alcançar Level 615 = 3 pontos de constelação.
Alcançar Level 620 = 3 pontos de constelação.
Alcançar Level 625 = 3 pontos de constelação.
Red, The Greatest Trainer = 2 ponto de constelação.
Observação: Lembra-se que as conquistas relacionadas aos pontos de constelação, ao serem concluídas pela primeira vez na conta, desbloqueiam automaticamente os pontos de constelação para todos os personagens dessa conta.
Star Paths
Todas as contas começam com um Star Path, e o jogador poderá adquirir páginas extras na Diamond Shop para ter ainda mais opções.
Será possível adquirir até 2 Star Paths adicionais na Diamond Shop, pelos valores de 20 e 35 Diamonds, respectivamente. Dessa forma, cada conta poderá possuir até 3 Star Paths no total.
O jogador poderá montar livremente sua build, especializando-se em diferentes estilos de jogo, como combate contra Bosses ou captura de Pokémon.
Caso possua um personagem secundário e um segundo Star Path na conta, poderá criar uma build totalmente distinta, por exemplo, focada em ganho de experiência, facilitando o gerenciamento de diferentes estilos de jogo simultaneamente.
Observação: Será possível resetar completamente a constelação pagando 20k por ponto distribuído. Não é possível resetar parcialmente uma constelação.
Star Signs Builder
A seguir, encontrará uma ferramenta que permite simular e planejar a distribuição de pontos no sistema de talentos de Star Signs.
Por meio de uma interface interativa, os jogadores podem explorar as árvores de talentos, atribuir pontos manualmente e visualizar em tempo real os bônus e estatísticas obtidos com cada combinação.
O objetivo desta ferramenta é oferecer uma visão completa do sistema, permitindo comparar rotas e descobrir qual caminho se adapta melhor ao estilo de jogo de cada jogador.
`;
// Assign a predictable ID to the grid for CSS targeting from external config
// The grid ID created above is random, but we can add a secondary ID or Class
// Better approach: We set the ID of the grid element to be predictable based on container
// But we already used random ID. Let's just update the grid creation.
this.dom.filter = document.getElementById(this.ids.filter);
this.dom.grid = document.getElementById(this.ids.grid);
// Assign predictable ID for CSS targeting
this.dom.grid.id = `${this.containerId}-grid`;
this.dom.modal = document.getElementById(this.ids.modal);
this.dom.modalContent = this.dom.modal.querySelector('.fw-modal-content');
this.dom.closeBtn = this.dom.modal.querySelector('.fw-close-btn');
this.renderFilters();
this.applyFilters();
this.setupEvents();
}
renderFilters() {
this.dom.filter.innerHTML = '';
this.filtersConfig.forEach(filter => {
const group = document.createElement('div');
group.className = 'fw-filter-group';
const label = document.createElement('label');
label.className = 'fw-filter-label';
label.textContent = filter.label;
group.appendChild(label);
if (filter.type === 'text') {
const input = document.createElement('input');
input.className = 'fw-input';
input.placeholder = filter.placeholder || '...';
input.addEventListener('input', (e) => this.handleFilter(filter.key, e.target.value, 'text', filter));
group.appendChild(input);
}
else if (filter.type === 'number-range') {
const wrap = document.createElement('div');
wrap.style.display = 'flex'; wrap.style.gap = '0.5rem';
const createInput = (placeholder) => {
const inp = document.createElement('input');
inp.className = 'fw-input';
inp.placeholder = placeholder;
inp.type = 'number';
inp.min = 0;
inp.max = 625;
// Clamp value on blur
inp.addEventListener('blur', () => {
let val = parseInt(inp.value);
if (isNaN(val)) return;
if (val 625) inp.value = 625;
update(); // Trigger filter update after clamp
});
return inp;
};
const min = createInput('Min');
const max = createInput('Max');
const update = () => this.handleFilter(filter.key, { min: min.value, max: max.value }, 'range', filter);
min.addEventListener('input', update);
max.addEventListener('input', update);
wrap.append(min, max);
group.appendChild(wrap);
}
else if (filter.type === 'boolean') {
const wrap = document.createElement('label'); wrap.className = 'fw-checkbox-wrapper';
const check = document.createElement('input'); check.type = 'checkbox';
const span = document.createElement('span'); span.textContent = filter.checkboxLabel;
if (filter.expectedValue !== undefined) {
check.dataset.expected = filter.expectedValue;
}
check.addEventListener('change', (e) => this.handleFilter(filter.key, e.target.checked, 'boolean', filter));
wrap.append(check, span);
group.appendChild(wrap);
}
else if (filter.type === 'toggle') {
const wrap = document.createElement('label');
wrap.className = 'fw-custom-toggle-container';
const check = document.createElement('input');
check.type = 'checkbox';
check.className = 'fw-toggle-input';
if (filter.expectedValue !== undefined) {
check.dataset.expected = filter.expectedValue;
}
if (filter.defaultValue === true) {
check.checked = true;
this.activeFilters[filter.key] = { value: true, type: 'toggle', filterDef: filter };
}
const switchVisual = document.createElement('span');
switchVisual.className = 'fw-toggle-switch';
const text = document.createElement('span');
text.className = 'fw-toggle-text';
text.textContent = check.checked ? (filter.textOn || 'SIM') : (filter.textOff || 'NÃO');
check.addEventListener('change', (e) => {
text.textContent = e.target.checked ? (filter.textOn || 'SIM') : (filter.textOff || 'NÃO');
this.handleFilter(filter.key, e.target.checked, 'toggle', filter);
});
wrap.append(check, switchVisual, text);
group.appendChild(wrap);
}
else if (filter.type === 'multi-select') {
const wrapper = document.createElement('div');
wrapper.className = 'fw-custom-select';
// Extract options if not provided
let options = filter.options;
if (!options) {
const unique = new Set();
this.data.forEach(d => {
if (d[filter.key]) unique.add(d[filter.key]);
});
options = Array.from(unique).sort();
}
// Trigger
const trigger = document.createElement('div');
trigger.className = 'fw-select-trigger';
trigger.textContent = filter.placeholder || 'Selecionar...';
// Options Container
const optsContainer = document.createElement('div');
optsContainer.className = 'fw-select-options';
const selectedValues = new Set();
// Close dropdown on click outside
document.addEventListener('click', (e) => {
if (!wrapper.contains(e.target)) optsContainer.classList.remove('open');
});
trigger.addEventListener('click', () => {
optsContainer.classList.toggle('open');
});
options.forEach(opt => {
const label = document.createElement('label');
label.className = 'fw-select-option';
const checkbox = document.createElement('input');
checkbox.type = 'checkbox';
checkbox.className = 'fw-select-checkbox';
checkbox.value = opt;
checkbox.addEventListener('change', () => {
if (checkbox.checked) selectedValues.add(opt);
else selectedValues.delete(opt);
// Update Trigger Text
if (selectedValues.size === 0) trigger.textContent = filter.placeholder || 'Selecionar...';
else if (selectedValues.size === 1) trigger.textContent = Array.from(selectedValues)[0];
else trigger.textContent = `${selectedValues.size} seleccionados`;
this.handleFilter(filter.key, Array.from(selectedValues), 'multi-select', filter);
});
const span = document.createElement('span');
span.textContent = opt;
label.append(checkbox, span);
optsContainer.appendChild(label);
});
wrapper.append(trigger, optsContainer);
group.appendChild(wrapper);
}
this.dom.filter.appendChild(group);
});
// === SORTING UI ===
if (this.sortsConfig.length > 0) {
const sortGroup = document.createElement('div');
sortGroup.className = 'fw-filter-group';
// Label
const label = document.createElement('label');
label.className = 'fw-filter-label';
label.textContent = 'Ordenar Por';
sortGroup.appendChild(label);
// Sort Wrapper
const sortWrap = document.createElement('div');
sortWrap.style.display = 'flex';
sortWrap.style.gap = '0.5rem';
// Select
const sortSelect = document.createElement('select');
sortSelect.className = 'fw-input';
sortSelect.style.cursor = 'pointer';
// Default Option
const defOpt = document.createElement('option');
defOpt.value = "";
defOpt.textContent = "Padrão";
sortSelect.appendChild(defOpt);
this.sortsConfig.forEach(s => {
const opt = document.createElement('option');
opt.value = s.key;
opt.textContent = s.label;
sortSelect.appendChild(opt);
});
// Direction Button
const dirBtn = document.createElement('button');
dirBtn.className = 'fw-toggle-btn'; // Reuse style
dirBtn.style.width = '48px'; // Ensure square
dirBtn.innerHTML = 'arrow_downward';
dirBtn.title = 'Descendente';
dirBtn.style.cursor = 'pointer';
// Events
sortSelect.addEventListener('change', (e) => {
const key = e.target.value;
// Find default direction for this key
const constr = this.sortsConfig.find(sc => sc.key === key);
const dir = constr ? (constr.defaultDirection || 'asc') : 'asc';
this.handleSort(key, dir);
// Update visual icon for new default
dirBtn.dataset.dir = dir;
dirBtn.innerHTML = `${dir === 'asc' ? 'arrow_upward' : 'arrow_downward'}`;
});
dirBtn.addEventListener('click', () => {
const currentDir = dirBtn.dataset.dir || 'desc';
const newDir = currentDir === 'asc' ? 'desc' : 'asc';
dirBtn.dataset.dir = newDir;
dirBtn.innerHTML = `${newDir === 'asc' ? 'arrow_upward' : 'arrow_downward'}`;
dirBtn.title = newDir === 'asc' ? 'Ascendente' : 'Descendente';
// Apply with current key (if any)
this.handleSort(this.activeSort.key, newDir);
});
sortWrap.appendChild(sortSelect);
sortWrap.appendChild(dirBtn);
sortGroup.appendChild(sortWrap);
this.dom.filter.appendChild(sortGroup);
}
// Add View Toggle ONLY if both templates exist
if (this.templates.item && this.templates.itemList) {
const toggleGroup = document.createElement('div');
toggleGroup.className = 'fw-view-toggle';
toggleGroup.innerHTML = `
`;
const btns = toggleGroup.querySelectorAll('.fw-toggle-btn');
btns.forEach(btn => {
btn.addEventListener('click', () => {
btns.forEach(b => b.classList.remove('active'));
btn.classList.add('active');
this.setViewMode(btn.dataset.mode);
});
});
this.dom.filter.appendChild(toggleGroup);
this.dom.filter.appendChild(toggleGroup);
}
// === HIDE COMPLETED BUTTON (Conditional) ===
if (this.selectable && this.showHideCompletedButton) {
const hideGroup = document.createElement('div');
hideGroup.className = 'fw-filter-group';
const label = document.createElement('label');
label.className = 'fw-filter-label';
label.textContent = 'COMPLETADOS';
hideGroup.appendChild(label);
const hideBtn = document.createElement('button');
// Match input styling but make it clickable
hideBtn.className = 'fw-input';
hideBtn.style.cursor = 'pointer';
hideBtn.style.justifyContent = 'start';
hideBtn.style.color = '#64748b';
hideBtn.style.fontWeight = '600';
hideBtn.title = 'Ocultar Completados';
const updateBtnState = () => {
if (this.hideCompleted) {
hideBtn.style.background = '#f0fdf4'; // Light green for active/hidden state logic or just active
// Actually user image shows check-like or eye. Let's keep it clean.
// If "Hidden", maybe show "Show"?
// Logic: Button says what it DOES or what IS?
// User image says "Ocultar" (Hide) with an eye-slash. This implies clicking it will hide.
// So currently they are SHOWING.
// If they are HIDDEN, button should say "Mostrar" (Show).
// Let's stick to previous logic just better style.
hideBtn.style.background = '#9f4eea';
hideBtn.style.color = 'white';
hideBtn.style.borderColor = '#9f4eea';
hideBtn.innerHTML = `
visibilityMostrar
`;
} else {
hideBtn.style.background = '#f8fafc';
hideBtn.style.color = '#64748b';
hideBtn.style.borderColor = '#e2e8f0';
hideBtn.innerHTML = `
visibility_offOcultar
`;
}
};
updateBtnState(); // Initial state
hideBtn.addEventListener('click', () => {
this.hideCompleted = !this.hideCompleted;
updateBtnState();
this.applyFilters();
});
// Match hover effect to css
hideBtn.addEventListener('mouseenter', () => {
if (!this.hideCompleted) {
hideBtn.style.background = '#ffffff';
hideBtn.style.borderColor = '#cbd5e1';
}
});
hideBtn.addEventListener('mouseleave', () => {
if (!this.hideCompleted) {
hideBtn.style.background = '#f8fafc';
hideBtn.style.borderColor = '#e2e8f0';
}
});
hideGroup.appendChild(hideBtn);
// Insert before view toggles if possible.
const viewToggleGroup = this.dom.filter.querySelector('.fw-view-toggle');
if (viewToggleGroup) {
this.dom.filter.insertBefore(hideGroup, viewToggleGroup);
} else {
this.dom.filter.appendChild(hideGroup);
}
}
}
setViewMode(mode) {
this.viewMode = mode;
this.renderGrid();
}
handleFilter(key, value, type, filterDef) {
this.activeFilters[key] = { value, type, filterDef };
this.applyFilters();
}
handleSort(key, direction) {
this.activeSort = { key, direction };
this.sortData(); // Just re-sort and render, no need to re-filter
this.renderGrid();
}
applyFilters() {
this.filteredData = this.data.filter(item => {
// 0. Filter Completed if Hiding
if (this.selectable && this.hideCompleted) {
// We need the ID. Based on current logic, ID is item.name
if (this.completedItems.has(item.name)) return false;
}
for (const key in this.activeFilters) {
const f = this.activeFilters[key];
// Support multiple keys if defined in filterDef, fallback to loop key
const targetKey = f.filterDef ? f.filterDef.key : key;
if (f.type === 'text') {
if (!f.value) continue;
const q = f.value.toLowerCase();
if (key === 'generic_search') {
if (!Object.values(item).some(v => v && String(v).toLowerCase().includes(q))) return false;
} else {
// Support array of keys or single key
const keysToCheck = Array.isArray(targetKey) ? targetKey : [targetKey];
const match = keysToCheck.some(k => {
const val = item[k];
if (!val) return false;
if (Array.isArray(val)) {
return val.some(v =>
(typeof v === 'object' && v !== null)
? JSON.stringify(v).toLowerCase().includes(q)
: String(v).toLowerCase().includes(q)
);
}
return String(val).toLowerCase().includes(q);
});
if (!match) return false;
}
} else {
// For range/boolean, assume single key
const val = item[targetKey];
if (f.type === 'range') {
const min = parseFloat(f.value.min), max = parseFloat(f.value.max);
if (!isNaN(min) && (val === undefined || val max)) return false;
} else if (f.type === 'boolean') {
if (f.value === true) {
const expected = (f.filterDef && f.filterDef.expectedValue !== undefined)
? f.filterDef.expectedValue
: true;
if (val !== expected) return false;
}
} else if (f.type === 'toggle') {
const expected = f.value ?
(f.filterDef && f.filterDef.expectedValueOn !== undefined ? f.filterDef.expectedValueOn : true) :
(f.filterDef && f.filterDef.expectedValueOff !== undefined ? f.filterDef.expectedValueOff : false);
if (expected !== '*' && val !== expected) return false;
} else if (f.type === 'multi-select') {
if (f.value.length > 0) {
if (val === undefined || val === null) return false;
const itemValues = Array.isArray(val) ? val : [val];
const hasMatch = itemValues.some(v => f.value.includes(v));
if (!hasMatch) return false;
}
}
}
}
return true;
});
this.sortData(); // Apply sort after filtering
this.renderGrid();
}
sortData() {
const { key, direction } = this.activeSort;
if (!key) return; // No sort active
this.filteredData.sort((a, b) => {
let va = a[key];
let vb = b[key];
// Handle undefined/null (push to bottom usually)
if (va === undefined || va === null) va = '';
if (vb === undefined || vb === null) vb = '';
// Try to compare as numbers
const na = parseFloat(va);
const nb = parseFloat(vb);
// Check if they are effectively numbers (and not just starting with a number like "10th")
// Actually, if both parse to valid numbers, treat as numbers for sorting is usually desired.
// But for names like "2nd street" vs "10th street", numeric is better.
// For "Alfredo", parseFloat is NaN.
const aIsNum = !isNaN(na);
const bIsNum = !isNaN(nb);
if (aIsNum && bIsNum) {
return direction === 'asc' ? na - nb : nb - na;
}
// Fallback to string comparison
const sa = String(va).toLowerCase();
const sb = String(vb).toLowerCase();
if (sa sb) return direction === 'asc' ? 1 : -1;
return 0;
});
}
renderGrid() {
// 1. Update grid classes based on view mode
if (this.viewMode === 'list') {
this.dom.grid.classList.add('fw-list-view');
} else {
this.dom.grid.classList.remove('fw-list-view');
}
if (this.filteredData.length === 0) {
this.dom.grid.innerHTML = '
No results found
';
return;
}
// 2. Choose template and cache based on view mode
let templateName = (this.viewMode === 'list') ? 'itemList' : 'item';
// Safety check: if current view mode template is missing, try fallback
if (this.viewMode === 'list' && !this.templates.itemList && this.templates.item) {
this.viewMode = 'grid';
templateName = 'item';
this.dom.grid.classList.remove('fw-list-view');
}
const template = this.templates[templateName];
const activeCache = this.elementCache[this.viewMode];
if (!template) {
this.dom.grid.innerHTML = '
No template found for ' + this.viewMode + ' view
';
return;
}
// 3. Collect Nodes (Reuse from Cache or Create)
const nodesToRender = [];
this.filteredData.forEach(item => {
// Use a unique key for the item. Assuming 'name' describes identity.
// Fallback to index if name missing effectively disables valid caching for stability, so name is preferred.
const itemId = item.name || JSON.stringify(item);
let el = activeCache.get(itemId);
if (!el) {
// Create New Element
const html = this.renderTemplate(template, item);
const wrap = document.createElement('div');
wrap.innerHTML = html;
el = wrap.firstElementChild;
if (el) {
// === Handle Selectable Logic ===
if (this.selectable) {
if (this.completedItems.has(itemId)) {
el.classList.add('is-completed');
}
// Create Check Button
const checkBtn = document.createElement('div');
checkBtn.className = 'fw-check-toggle';
checkBtn.innerHTML = 'check';
checkBtn.title = this.completedItems.has(itemId) ? "Marcar como pendente" : "Marcar como concluído";
checkBtn.addEventListener('click', (e) => {
e.stopPropagation();
this.toggleItemCompletion(itemId, el, checkBtn);
});
// Append button
el.appendChild(checkBtn);
}
// === Handle Modal logic ===
if (this.templates.modal) {
const btn = el.querySelector('.fw-details-btn');
if (btn) {
btn.addEventListener('click', (e) => {
e.stopPropagation();
this.openModal(item);
});
el.style.cursor = 'default';
} else {
el.addEventListener('click', () => this.openModal(item));
el.style.cursor = 'pointer';
}
} else {
el.style.cursor = 'default';
}
// Update Cache
activeCache.set(itemId, el);
}
}
// else: Element exists, reuse it directly (preserves image load state)
if (el) nodesToRender.push(el);
});
// 5. Update DOM efficiently
if (this.dom.grid.replaceChildren) {
this.dom.grid.replaceChildren(...nodesToRender);
} else {
this.dom.grid.innerHTML = '';
nodesToRender.forEach(node => this.dom.grid.appendChild(node));
}
}
openModal(item) {
if (!this.templates.modal) return; // Do not open if no template
const itemId = item.name || JSON.stringify(item);
let cachedModalWrap = this.modalCache.get(itemId);
if (!cachedModalWrap) {
const html = this.renderTemplate(this.templates.modal, item);
cachedModalWrap = document.createElement('div');
cachedModalWrap.style.display = 'contents'; // Crucial so it doesn't affect flex properties
cachedModalWrap.innerHTML = html;
this.modalCache.set(itemId, cachedModalWrap);
}
// Use the cached node directly instead of triggering innerHTML parsing and reload
if (this.dom.modalContent.replaceChildren) {
this.dom.modalContent.replaceChildren(cachedModalWrap);
} else {
this.dom.modalContent.innerHTML = '';
this.dom.modalContent.appendChild(cachedModalWrap);
}
this.dom.modal.classList.add('active');
document.body.style.overflow = 'hidden';
}
renderTemplate(templateString, data) {
if (!templateString) return '';
let output = templateString;
// 1. Handle #if key
output = output.replace(/\{\{#if\s+(\w+)\}\}([\s\S]*?)\{\{\/if\}\}/g, (match, key, content) => {
const val = data[key];
// Check for truthy, not 'None', not empty array
const isTrue = val && val !== 'None' && (!Array.isArray(val) || val.length > 0);
return isTrue ? content : '';
});
// 2. Handle #each key
output = output.replace(/\{\{#each\s+(\w+)\}\}([\s\S]*?)\{\{\/each\}\}/g, (match, key, content) => {
const list = data[key];
if (!Array.isArray(list) || list.length === 0) return '';
return list.map(item => {
// Normalize item context: if primitive, map to { label: item, this: item }
let ctx = item;
if (typeof item !== 'object' || item === null) {
ctx = { this: item, label: item };
}
let itemHtml = content;
// Internal #if support (simple 1-level nesting)
itemHtml = itemHtml.replace(/\{\{#if\s+(\w+)\}\}([\s\S]*?)\{\{\/if\}\}/g, (m, k, c) => {
const v = ctx[k];
return (v) ? c : '';
});
// Internal values
itemHtml = itemHtml.replace(/\{\{\s*(\w+)\s*\?\s*'([^']*)'\s*:\s*'([^']*)'\s*\}\}/g, (m, k, t, f) => ctx[k] ? t : f);
// Handle "isSub" logic specifically for styling if used in ternary
itemHtml = itemHtml.replace(/\{\{\s*(\w+)\s*\}\}/g, (m, k) => ctx[k] !== undefined ? ctx[k] : '');
return itemHtml;
}).join('');
});
// 3. Conditional Values (Top Level)
output = output.replace(/\{\{\s*(\w+)\s*\?\s*'([^']*)'\s*:\s*'([^']*)'\s*\}\}/g, (match, key, tVal, fVal) => data[key] ? tVal : fVal);
output = output.replace(/\{\{\s*(\w+)\s*\?\s*"([^"]*)"\s*:\s*"([^"]*)"\s*\}\}/g, (match, key, tVal, fVal) => data[key] ? tVal : fVal);
// 4. Numeric Thresholds
output = output.replace(/\{\{\s*(\w+)\s*(>=|| {
const val = data[key];
const threshold = parseFloat(num);
let result = false;
if (val !== undefined) {
if (op === '>=') result = val >= threshold;
if (op === '') result = val > threshold;
if (op === ' data[key] !== undefined ? data[key] : '');
return output;
}
setupEvents() {
this.dom.closeBtn.addEventListener('click', () => { this.dom.modal.classList.remove('active'); document.body.style.overflow = ''; });
this.dom.modal.addEventListener('click', (e) => { if (e.target === this.dom.modal) this.dom.closeBtn.click(); });
}
toggleItemCompletion(id, cardEl, btnEl) {
if (this.completedItems.has(id)) {
this.completedItems.delete(id);
cardEl.classList.remove('is-completed');
if (btnEl) btnEl.title = "Marcar como concluído";
} else {
this.completedItems.add(id);
cardEl.classList.add('is-completed');
if (btnEl) btnEl.title = "Marcar como pendente";
}
this.saveCompletedItems();
// If we are currently hiding completed items, we should re-apply filters to remove this item immediately
if (this.hideCompleted) {
this.applyFilters();
}
}
saveCompletedItems() {
if (!this.storageKey) return;
localStorage.setItem(this.storageKey, JSON.stringify(Array.from(this.completedItems)));
}
}
if (window.initFlexibleWidget === undefined) {
window.initFlexibleWidget = function (cid, cfg) { new FlexibleWidget(cid, cfg); };
}
})();