att na interface
Deploy / Deploy (push) Successful in 2m42s Details

This commit is contained in:
JotaChina 2025-12-03 18:24:56 -03:00
parent edc6497a06
commit 1cbe632b31
6 changed files with 17635 additions and 147 deletions

File diff suppressed because it is too large Load Diff

3455
_data/guitarra__baixo.yml Normal file

File diff suppressed because it is too large Load Diff

3086
_data/samples-manifest.json Normal file

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -16,92 +16,78 @@ permalink: /samples/
{% include sidebar.html %} {% include sidebar.html %}
</div> </div>
<details class="box mb-5 p-0 collapse-card" open <div class="has-text-centered mb-5">
style="border: 1px solid #cfe8fc; overflow: hidden; background-color: #fff; box-shadow: 0 2px 5px rgba(0,0,0,0.05); height: fit-content !important; min-height: unset;"> <h1 class="title is-3 has-text-grey-dark">🎤 Biblioteca de Samples</h1>
<p class="subtitle is-6 has-text-grey">Navegue pelas pastas para ouvir e filtrar projetos.</p>
<div style="width: 60px; height: 4px; background-color: #3273dc; margin: 1rem auto; border-radius: 2px;"></div>
</div>
<summary class="p-4 is-flex is-justify-content-space-between is-align-items-center" <div class="box p-0 mb-6" style="border: 1px solid #cfe8fc; overflow: hidden; background-color: #fff; min-height: 400px; display: flex; flex-direction: column; box-shadow: 0 4px 10px rgba(0,0,0,0.05);">
style="cursor: pointer; background-color: #f0f8ff; transition: background-color 0.2s; user-select: none;">
<div class="is-flex is-align-items-center"> <div class="p-3 has-background-white-ter" style="border-bottom: 1px solid #cfe8fc; display: flex; align-items: center;">
<span class="icon has-text-info mr-2"><i class="fa-solid fa-guitar"></i></span> <button id="btn-home" class="button is-small is-info is-light mr-3" title="Voltar ao início">
<span class="has-text-grey-dark has-text-weight-bold">Todos os Samples Disponíveis</span> <i class="fa-solid fa-house"></i>
<span id="filter-counter" class="tag is-info is-light ml-3 is-hidden">0 selecionados</span> </button>
</div> <nav class="breadcrumb is-small mb-0" aria-label="breadcrumbs">
<span class="icon has-text-grey-light chevron-icon"><i class="fa-solid fa-chevron-down"></i></span> <ul id="breadcrumb-list">
</summary> <li class="is-active"><a href="#">Raiz</a></li>
</ul>
<div class="p-4" style="background-color: #fff; border-top: 1px solid #cfe8fc;"> </nav>
<div class="tags is-centered are-medium mb-0">
{% assign all_sample_string = "" %}
{% for p in site.data.all %}
{% for track in p.tracks %}
{% if track.sample %}
{% for inst in track.sample %}
{% if inst.sample_name and inst.sample_name != "" %}
{% unless all_sample_string contains inst.sample_name %}
{% assign all_sample_string = all_sample_string | append: inst.sample_name | append: "|||" %}
{% endunless %}
{% endif %}
{% endfor %}
{% elsif track.sample_name and track.sample_name != "" %}
{% unless all_sample_string contains track.sample_name %}
{% assign all_sample_string = all_sample_string | append: track.sample_name | append: "|||" %}
{% endunless %}
{% endif %}
{% endfor %}
{% endfor %}
{% assign unique_sample = all_sample_string | split: "|||" | sort %}
{% for item in unique_sample %}
{% if item != "" %}
<a href="#" class="tag is-white filter-item clickable-tag"
data-value="{{ item }}"
style="border: 1px solid #deeaf6; color: #5b7da3; margin-bottom: 0.5rem; transition: all 0.2s;">
{{ item }}
</a>
{% endif %}
{% endfor %}
</div>
<div class="has-text-centered mt-3">
<p class="help has-text-grey">Clique para filtrar.</p>
</div>
</div> </div>
</details>
<div id="browser-view" class="p-4" style="flex: 1;">
<p class="has-text-centered has-text-grey-light mt-6">Carregando biblioteca...</p>
</div>
<div id="preview-bar" class="p-3 has-background-info-light is-hidden" style="border-top: 1px solid #cfe8fc; display: flex; align-items: center; justify-content: space-between;">
<div style="display: flex; align-items: center; gap: 10px; overflow: hidden;">
<span class="icon has-text-info"><i class="fa-solid fa-music"></i></span>
<div>
<strong id="preview-filename" style="display:block; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; max-width: 200px; line-height: 1;">Nome do Arquivo</strong>
<span class="is-size-7 has-text-grey">Filtrando projetos abaixo...</span>
</div>
</div>
<audio id="browser-audio-player" controls style="height: 30px; max-width: 300px;"></audio>
</div>
</div>
<div class="columns is-mobile is-vcentered mb-5"> <div class="columns is-mobile is-vcentered mb-5">
<div class="column is-auto"> <div class="column is-auto">
<h2 class="title is-4 has-text-grey-dark"> <h2 class="title is-4 has-text-grey-dark">
<span class="icon has-text-info mr-2"><i class="fa-solid fa-filter"></i></span> <span class="icon has-text-info mr-2"><i class="fa-solid fa-filter"></i></span>
Filtro: <code id="filter-display-name" style="color: #d63384;">(todos)</code> Filtro: <code id="filter-display-name" style="color: #d63384;">(nenhum selecionado)</code>
</h2> </h2>
</div> </div>
<div class="column is-narrow"> <div class="column is-narrow">
<button id="clearFilterButton" class="button is-small is-danger is-light"> <button id="clearFilterButton" class="button is-small is-danger is-light">
<span class="icon is-small"><i class="fa-solid fa-xmark"></i></span> <span class="icon is-small"><i class="fa-solid fa-xmark"></i></span>
<span>Limpar Tudo</span> <span>Limpar Filtro</span>
</button> </button>
</div> </div>
</div> </div>
<div id="project-list" class="columns is-multiline"> <div id="project-list" class="columns is-multiline">
{% for projeto in site.data.all %} {% for projeto in site.data.all %}
{% assign project_samples_list = "" %}
{% for track in projeto.tracks %}
{% if track.sample %}
{% for inst in track.sample %}
{% if inst.sample_name %}
{% assign project_samples_list = project_samples_list | append: inst.sample_name | append: "," %}
{% endif %}
{% endfor %}
{% elsif track.sample_name %}
{% assign project_samples_list = project_samples_list | append: track.sample_name | append: "," %}
{% endif %}
{% endfor %}
{% assign project_insts = "" %} {% if project_samples_list != "" %}
{% for track in projeto.tracks %}
{% if track.sample %} <div class="column is-12-mobile is-6-tablet is-4-desktop is-3-widescreen project-item"
{% for inst in track.sample %} data-samples="{{ project_samples_list }}">
{% assign project_insts = project_insts | append: inst.sample_name | append: "," %}
{% endfor %} <div class="card project-card" style="height: 100%; background-color: #f0f8ff; border: 1px solid #cfe8fc; border-radius: 12px; display: flex; flex-direction: column; position: relative;">
{% elsif track.sample_name %}
{% assign project_insts = project_insts | append: track.sample_name | append: "," %}
{% endif %}
{% endfor %}
<div class="column is-12-mobile is-6-tablet is-4-desktop is-3-widescreen project-item"
data-sample="{{ project_insts }}">
<div class="card project-card" style="height: 100%; background-color: #f0f8ff; border: 1px solid #cfe8fc; border-radius: 12px; display: flex; flex-direction: column;">
{% assign file_url = projeto.file | downcase | replace: ' ', '-' | replace: 'ç', 'c' | replace: 'ã', 'a' | replace: 'á', 'a' | replace: 'â', 'a' | replace: 'é', 'e' | replace: 'ê', 'e' | replace: 'í', 'i' | replace: 'ó', 'o' | replace: 'ô', 'o' | replace: 'õ', 'o' | replace: 'ú', 'u' %} {% assign file_url = projeto.file | downcase | replace: ' ', '-' | replace: 'ç', 'c' | replace: 'ã', 'a' | replace: 'á', 'a' | replace: 'â', 'a' | replace: 'é', 'e' | replace: 'ê', 'e' | replace: 'í', 'i' | replace: 'ó', 'o' | replace: 'ô', 'o' | replace: 'õ', 'o' | replace: 'ú', 'u' %}
{% assign page_url = '../mmp_pages/' | append: file_url | append: '.html' %} {% assign page_url = '../mmp_pages/' | append: file_url | append: '.html' %}
@ -110,7 +96,7 @@ permalink: /samples/
<div class="card-content has-text-centered p-4" style="flex: 1; display: flex; flex-direction: column;"> <div class="card-content has-text-centered p-4" style="flex: 1; display: flex; flex-direction: column;">
<div style="width: 50px; height: 50px; background-color: #fff; border-radius: 50%; display: flex; align-items: center; justify-content: center; margin: 0 auto 0.5rem auto; box-shadow: 0 2px 5px rgba(0,0,0,0.05);"> <div style="width: 50px; height: 50px; background-color: #fff; border-radius: 50%; display: flex; align-items: center; justify-content: center; margin: 0 auto 0.5rem auto; box-shadow: 0 2px 5px rgba(0,0,0,0.05);">
<span class="icon" style="color: #3273dc;"><i class="fa-solid fa-music fa-lg"></i></span> <span class="icon" style="color: #3273dc;"><i class="fa-solid fa-microphone-lines fa-lg"></i></span>
</div> </div>
<p class="title is-6 mb-2" style="color: #205081; word-break: break-word; font-weight: 700; line-height: 1.2;"> <p class="title is-6 mb-2" style="color: #205081; word-break: break-word; font-weight: 700; line-height: 1.2;">
@ -127,21 +113,20 @@ permalink: /samples/
<div class="mb-3" style="height: 24px;"></div> <div class="mb-3" style="height: 24px;"></div>
{% endif %} {% endif %}
<div style="flex: 1;"></div> <div style="flex: 1;"></div>
{% assign unique_proj_insts = project_insts | split: "," | uniq %} <div class="tags is-centered is-gapless mb-0 mt-2" style="gap: 4px; justify-content: center;">
{% if unique_proj_insts.size > 0 %} {% assign unique_list = project_samples_list | split: "," | uniq %}
<div class="tags is-centered is-gapless mb-0 mt-2" style="gap: 4px; justify-content: center;"> {% for item in unique_list %}
{% for item in unique_proj_insts %}
{% if item != "" %} {% if item != "" %}
<span class="tag is-white filter-item clickable-tag" data-value="{{ item }}" {% assign item_display = item | split: '/' | last | split: '\\' | last %}
<span class="tag is-white sample-tag-item clickable-tag" data-value="{{ item }}"
style="font-size: 0.65rem; border: 1px solid #deeaf6; color: #5b7da3; padding: 0 6px; height: 1.5em; text-decoration: none; cursor: pointer;"> style="font-size: 0.65rem; border: 1px solid #deeaf6; color: #5b7da3; padding: 0 6px; height: 1.5em; text-decoration: none; cursor: pointer;">
{{ item | truncate: 15 }} {{ item_display | truncate: 15 }}
</span> </span>
{% endif %} {% endif %}
{% endfor %} {% endfor %}
</div> </div>
{% endif %}
</div> </div>
</a> </a>
@ -154,10 +139,11 @@ permalink: /samples/
</div> </div>
</div> </div>
{% endif %}
{% endfor %} {% endfor %}
</div> </div>
</div>
</div>
</div> </div>
</main> </main>
@ -179,94 +165,244 @@ permalink: /samples/
</div> </div>
<style> <style>
.project-card:hover { transform: translateY(-5px); box-shadow: 0 8px 20px rgba(50, 115, 220, 0.15); border-color: #3273dc !important; background-color: #fff !important; } .browser-grid { display: grid; grid-template-columns: repeat(auto-fill, minmax(140px, 1fr)); gap: 15px; }
.clickable-tag:hover { background-color: #e3effd !important; color: #3273dc !important; border-color: #3273dc !important; transform: scale(1.05); } .browser-item { display: flex; flex-direction: column; align-items: center; text-align: center; padding: 15px; border: 1px solid transparent; border-radius: 8px; cursor: pointer; transition: all 0.2s; }
.tag.is-active-filter { background-color: #3273dc !important; color: #fff !important; border-color: #3273dc !important; font-weight: bold; box-shadow: 0 2px 5px rgba(50, 115, 220, 0.3); } .browser-item:hover { background-color: #f0f8ff; border-color: #cfe8fc; transform: translateY(-2px); }
.collapse-card summary { list-style: none; transition: background 0.2s; } .browser-icon { font-size: 2.5rem; margin-bottom: 10px; color: #888; }
.collapse-card summary::-webkit-details-marker { display: none; } .browser-item.is-folder .browser-icon { color: #fce96a; text-shadow: 0 2px 2px rgba(0,0,0,0.1); }
.collapse-card[open] summary .chevron-icon { transform: rotate(180deg); } .browser-item.is-file .browser-icon { color: #3273dc; }
.chevron-icon { transition: transform 0.3s ease; } .browser-item.is-unsupported .browser-icon { color: #ccc; }
.browser-name { font-size: 0.85rem; word-break: break-word; line-height: 1.3; color: #4a4a4a; }
.project-card:hover { transform: translateY(-5px); box-shadow: 0 8px 20px rgba(50, 115, 220, 0.15); border-color: #3273dc !important; background-color: #fff !important; }
.clickable-tag:hover { background-color: #e3effd !important; color: #3273dc !important; border-color: #3273dc !important; transform: scale(1.05); }
.tag.is-active-filter { background-color: #3273dc !important; color: #fff !important; border-color: #3273dc !important; font-weight: bold; box-shadow: 0 2px 5px rgba(50, 115, 220, 0.3); }
</style> </style>
<script> <script>
document.addEventListener('DOMContentLoaded', function () { document.addEventListener('DOMContentLoaded', () => {
// === DADOS E CONFIGURAÇÃO ===
const manifestData = {{ site.data['samples-manifest'] | jsonify }};
const BASE_ASSETS_URL = "{{ '/src/samples/' | relative_url }}";
// === ELEMENTOS DO DOM ===
const browserView = document.getElementById('browser-view');
const breadcrumbList = document.getElementById('breadcrumb-list');
const btnHome = document.getElementById('btn-home');
const previewBar = document.getElementById('preview-bar');
const audioPlayer = document.getElementById('browser-audio-player');
const previewLabel = document.getElementById('preview-filename');
const projects = document.querySelectorAll('.project-item'); const projects = document.querySelectorAll('.project-item');
const filterDisplayName = document.getElementById('filter-display-name'); const filterDisplayName = document.getElementById('filter-display-name');
const filterCounter = document.getElementById('filter-counter'); const clearFilterButton = document.getElementById('clearFilterButton');
let activeFilters = [];
function applyFilters() { let currentPathStack = [];
if (activeFilters.length > 0) { let currentFolderObj = manifestData;
filterDisplayName.textContent = activeFilters.join(" + ");
filterCounter.textContent = activeFilters.length + " selecionados";
filterCounter.classList.remove('is-hidden');
} else {
filterDisplayName.textContent = "(todos)";
filterCounter.classList.add('is-hidden');
}
// Atualiza visual das tags // === FUNÇÕES DE UTILIDADE ===
document.querySelectorAll('.filter-item').forEach(tag => {
const val = tag.getAttribute('data-value'); // Remove extensão do arquivo (ex: "kick.wav" -> "kick")
if (activeFilters.includes(val)) { function removeExtension(filename) {
tag.classList.add('is-active-filter'); return filename.replace(/\.[^/.]+$/, "");
} else { }
tag.classList.remove('is-active-filter');
}
});
projects.forEach(project => { // === LÓGICA DO NAVEGADOR DE ARQUIVOS ===
const projectInstStr = project.getAttribute('data-sample');
// Cria array de automações deste projeto
const projectInsts = projectInstStr.split(',').map(s => s.trim());
if (activeFilters.length === 0) { function getFolderByPath(pathArray) {
project.style.display = 'block'; let folder = manifestData;
} else { for (const dir of pathArray) {
// Lógica OR: Se tiver pelo menos uma das automações filtradas, exibe if (folder[dir]) folder = folder[dir];
const hasMatch = activeFilters.some(filter => projectInsts.includes(filter));
project.style.display = hasMatch ? 'block' : 'none';
} }
}); return folder;
const newUrl = new URL(window.location.href);
if (activeFilters.length > 0) {
newUrl.searchParams.set('sample', activeFilters.join(','));
} else {
newUrl.searchParams.delete('sample');
}
window.history.replaceState({}, '', newUrl);
} }
function toggleFilter(val) { function isAudioFile(filename) {
const index = activeFilters.indexOf(val); const ext = filename.split('.').pop().toLowerCase();
if (index > -1) activeFilters.splice(index, 1); return ['wav', 'ogg', 'mp3', 'flac'].includes(ext);
else activeFilters.push(val);
applyFilters();
} }
// Inicializa function renderBreadcrumbs() {
const urlParams = new URLSearchParams(window.location.search); breadcrumbList.innerHTML = '';
const instParam = urlParams.get('sample'); const liRoot = document.createElement('li');
if (instParam) { if (currentPathStack.length === 0) liRoot.classList.add('is-active');
activeFilters = instParam.split(',').map(s => s.trim()).filter(s => s !== ""); liRoot.innerHTML = `<a href="#">Raiz</a>`;
applyFilters(); liRoot.onclick = (e) => { e.preventDefault(); navigateTo([]); };
breadcrumbList.appendChild(liRoot);
let accumulatedPath = [];
currentPathStack.forEach((folderName, index) => {
accumulatedPath.push(folderName);
const isLast = index === currentPathStack.length - 1;
const li = document.createElement('li');
if (isLast) li.classList.add('is-active');
const pathForClick = [...accumulatedPath];
li.innerHTML = `<a href="#">${folderName}</a>`;
if (!isLast) li.onclick = (e) => { e.preventDefault(); navigateTo(pathForClick); };
breadcrumbList.appendChild(li);
});
} }
// Eventos function renderBrowser() {
document.querySelectorAll('.filter-item').forEach(tag => { browserView.innerHTML = '';
const grid = document.createElement('div');
grid.className = 'browser-grid';
const folders = [];
const files = [];
Object.keys(currentFolderObj).forEach(key => {
if (key === '_isFile') return;
const item = currentFolderObj[key];
if (item._isFile) files.push(key);
else folders.push(key);
});
folders.sort();
files.sort();
folders.forEach(folderName => {
const el = document.createElement('div');
el.className = 'browser-item is-folder';
el.innerHTML = `<i class="fa-solid fa-folder browser-icon"></i><span class="browser-name">${folderName}</span>`;
el.onclick = () => navigateTo([...currentPathStack, folderName]);
grid.appendChild(el);
});
files.forEach(fileName => {
const isAudio = isAudioFile(fileName);
const el = document.createElement('div');
el.className = `browser-item ${isAudio ? 'is-file' : 'is-unsupported'}`;
let iconClass = isAudio ? 'fa-file-audio' : (fileName.endsWith('.ds') ? 'fa-sliders' : 'fa-file');
el.innerHTML = `<i class="fa-solid ${iconClass} browser-icon"></i><span class="browser-name">${fileName}</span>`;
if (isAudio) {
el.onclick = () => {
playFile(fileName);
filterBySample(fileName);
};
} else {
el.onclick = () => alert('Este arquivo não é um áudio reproduzível (preset/binário).');
}
grid.appendChild(el);
});
if (folders.length === 0 && files.length === 0) {
browserView.innerHTML = `<div class="has-text-centered has-text-grey p-5">Esta pasta está vazia.</div>`;
} else {
browserView.appendChild(grid);
}
}
function navigateTo(newPathArray) {
currentPathStack = newPathArray;
currentFolderObj = getFolderByPath(currentPathStack);
renderBreadcrumbs();
renderBrowser();
}
function playFile(fileName) {
const fullPath = BASE_ASSETS_URL + currentPathStack.join('/') + '/' + fileName;
previewBar.classList.remove('is-hidden');
previewLabel.textContent = fileName;
audioPlayer.src = fullPath;
audioPlayer.play().catch(e => console.log('Erro ao tocar:', e));
}
// === LÓGICA DE FILTRAGEM (ROBUSTA) ===
function filterBySample(sampleName) {
if (!sampleName) {
filterDisplayName.textContent = "(todos)";
projects.forEach(p => p.style.display = 'block');
document.querySelectorAll('.sample-tag-item').forEach(tag => tag.classList.remove('is-active-filter'));
return;
}
filterDisplayName.textContent = sampleName;
// Preparação do Alvo (Target)
const targetClean = sampleName.trim().toLowerCase();
const targetBase = removeExtension(targetClean); // Ex: "kick" (sem .wav)
console.log(`🔍 Buscando por: "${sampleName}" (Base: "${targetBase}")`);
let foundCount = 0;
projects.forEach(project => {
const projectSamplesStr = project.getAttribute('data-samples');
if (!projectSamplesStr) {
project.style.display = 'none';
return;
}
const projectSamples = projectSamplesStr.split(',');
// Verifica se ALGUM sample do projeto bate com o alvo (com ou sem extensão)
const hasMatch = projectSamples.some(s => {
const samplePathClean = s.trim().toLowerCase();
const sampleFileName = samplePathClean.split(/[/\\]/).pop(); // Pega nome do arquivo
const sampleBaseName = removeExtension(sampleFileName); // Remove extensão
// 1. Tenta match exato de nome (ex: kick.wav == kick.wav)
if (sampleFileName === targetClean) return true;
// 2. Tenta match de nome base (ex: kick.wav == kick.ogg)
if (sampleBaseName === targetBase && sampleBaseName !== "") return true;
return false;
});
if (hasMatch) {
project.style.display = 'block';
foundCount++;
// Destaca a tag correta dentro do card
const tags = project.querySelectorAll('.sample-tag-item');
tags.forEach(tag => {
const tagValClean = tag.dataset.value.trim().toLowerCase().split(/[/\\]/).pop();
const tagBase = removeExtension(tagValClean);
if(tagBase === targetBase) tag.classList.add('is-active-filter');
else tag.classList.remove('is-active-filter');
});
} else {
project.style.display = 'none';
}
});
console.log(`✅ Encontrados ${foundCount} projetos.`);
// Rola até os resultados se encontrou algo
if(foundCount > 0) {
document.getElementById('project-list').scrollIntoView({ behavior: 'smooth' });
}
}
// Inicialização
btnHome.onclick = () => navigateTo([]);
renderBreadcrumbs();
renderBrowser();
// Eventos para as tags nos cards
document.querySelectorAll('.sample-tag-item').forEach(tag => {
tag.addEventListener('click', function(e) { tag.addEventListener('click', function(e) {
e.preventDefault(); e.preventDefault();
e.stopPropagation(); // Evita clique no card se for dentro dele const val = this.dataset.value.split(/[/\\]/).pop();
const val = this.getAttribute('data-value'); filterBySample(val);
toggleFilter(val);
}); });
}); });
document.querySelector('#clearFilterButton').addEventListener('click', function () { if(clearFilterButton) {
activeFilters = []; clearFilterButton.addEventListener('click', () => filterBySample(null));
applyFilters(); }
});
// Leitura URL
const urlParams = new URLSearchParams(window.location.search);
const sampleParam = urlParams.get('sample');
if (sampleParam) filterBySample(sampleParam);
// Modal // Modal
const modal = document.getElementById('preview-modal'); const modal = document.getElementById('preview-modal');
@ -276,6 +412,7 @@ permalink: /samples/
const closeButtons = document.querySelectorAll('.modal-background, .modal-card-head .delete, #close-modal-btn'); const closeButtons = document.querySelectorAll('.modal-background, .modal-card-head .delete, #close-modal-btn');
function openModal(url, title, btnText, btnLink) { function openModal(url, title, btnText, btnLink) {
if(!modal) return;
modalTitle.textContent = title; modalTitle.textContent = title;
iframe.src = url; iframe.src = url;
fullEditBtn.textContent = btnText; fullEditBtn.textContent = btnText;
@ -284,6 +421,7 @@ permalink: /samples/
document.documentElement.classList.add('is-clipped'); document.documentElement.classList.add('is-clipped');
} }
function closeModal() { function closeModal() {
if(!modal) return;
modal.classList.remove('is-active'); modal.classList.remove('is-active');
document.documentElement.classList.remove('is-clipped'); document.documentElement.classList.remove('is-clipped');
iframe.src = ""; iframe.src = "";
@ -302,5 +440,5 @@ permalink: /samples/
} catch(e){} }); } catch(e){} });
closeButtons.forEach(el => el.addEventListener('click', closeModal)); closeButtons.forEach(el => el.addEventListener('click', closeModal));
document.addEventListener('keydown', (e) => { if (e.key === "Escape") closeModal(); }); document.addEventListener('keydown', (e) => { if (e.key === "Escape") closeModal(); });
}); });
</script> </script>