Files
pinkudex/mockups/filter-options-sample.html
2026-05-26 22:46:00 +02:00

713 lines
26 KiB
HTML

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<title>Pinkudex — Filter Bar (Option 2 refined)</title>
<style>
:root {
--bg-0: #0e0a14;
--bg-1: #18121f;
--fg: #ece6f5;
--fg-dim: #b9b1c6;
--fg-muted: #7d7388;
--cyan: #4dd9e6;
--violet: #b87cf6;
--coral: #ff7a8a;
--mint: #79e6b2;
--amber: #fbbf24;
--glass-border: #2a2434;
--glass-border-strong: #3d3548;
--glass: rgba(40, 32, 56, 0.5);
--glass-strong: rgba(56, 46, 76, 0.7);
}
* { box-sizing: border-box; }
html, body {
margin: 0;
background: radial-gradient(1200px 600px at 70% -10%, rgba(184,124,246,0.08), transparent),
radial-gradient(800px 500px at 10% 110%, rgba(77,217,230,0.06), transparent),
var(--bg-0);
color: var(--fg);
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
font-size: 14px;
}
.page { max-width: 1600px; margin: 0 auto; padding: 32px 24px 64px; }
h1 { font-size: 28px; margin: 0 0 8px; font-weight: 600; letter-spacing: -0.01em; }
.lede { color: var(--fg-dim); margin: 0 0 32px; max-width: 820px; line-height: 1.5; }
h2 {
margin: 32px 0 4px;
font-size: 13px;
text-transform: uppercase;
letter-spacing: 0.1em;
color: var(--cyan);
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
}
.desc { color: var(--fg-dim); font-size: 13px; margin: 0 0 12px; max-width: 820px; line-height: 1.5; }
.stage {
background: var(--bg-1);
border: 1px solid var(--glass-border);
border-radius: 16px;
padding: 20px;
margin-bottom: 24px;
position: relative;
}
.stage-label {
font-size: 10px;
text-transform: uppercase;
letter-spacing: 0.1em;
color: var(--fg-muted);
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
margin-bottom: 14px;
}
/* bar */
.bar { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; margin-bottom: 12px; }
.chip {
display: inline-flex; align-items: center; gap: 6px;
padding: 6px 12px; border-radius: 9999px;
border: 1px solid var(--glass-border); background: var(--glass);
color: var(--fg-dim); font-size: 14px; cursor: pointer;
transition: all 150ms ease;
font-family: inherit;
}
.chip:hover { color: var(--fg); border-color: var(--glass-border-strong); }
.chip.active { background: rgba(77,217,230,0.15); border-color: rgba(77,217,230,0.4); color: var(--cyan); }
.chip .caret { font-size: 10px; opacity: 0.7; }
.chip .badge {
display: inline-flex; align-items: center; justify-content: center;
min-width: 16px; height: 16px; padding: 0 5px;
border-radius: 9999px; background: var(--cyan); color: #000;
font-size: 10px; font-weight: 700; margin-left: 2px;
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
}
.right-group { display: flex; align-items: center; gap: 8px; margin-left: auto; }
.search { position: relative; width: 224px; }
.search input {
width: 100%; background: var(--glass); border: 1px solid var(--glass-border);
border-radius: 8px; padding: 6px 28px 6px 30px; color: var(--fg);
font-size: 13px; outline: none;
}
.search input::placeholder { color: var(--fg-muted); }
.search .ico { position: absolute; left: 9px; top: 50%; transform: translateY(-50%); color: var(--fg-muted); width: 14px; height: 14px; }
.sort {
display: inline-flex; align-items: center; gap: 6px;
padding: 6px 12px; border-radius: 9999px;
border: 1px solid var(--glass-border); color: var(--fg-dim);
font-size: 13px; cursor: pointer; background: var(--glass);
font-family: inherit;
}
/* active criteria strip */
.crit-strip {
display: flex; align-items: center; gap: 6px; flex-wrap: wrap;
padding: 10px 12px; border: 1px dashed var(--glass-border-strong);
border-radius: 12px; margin-bottom: 12px;
background: rgba(77,217,230,0.04);
min-height: 48px;
}
.crit-strip .label {
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
font-size: 10px; text-transform: uppercase; letter-spacing: 0.1em;
color: var(--fg-muted); margin-right: 4px;
}
.crit-empty { color: var(--fg-muted); font-size: 12px; font-style: italic; }
.crit-pill {
display: inline-flex; align-items: center; gap: 6px;
padding: 4px 6px 4px 10px; border-radius: 9999px;
background: rgba(77,217,230,0.12); border: 1px solid rgba(77,217,230,0.4);
color: var(--cyan); font-size: 12px;
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
}
.crit-pill .x {
width: 16px; height: 16px; display: inline-flex; align-items: center; justify-content: center;
border-radius: 9999px; background: rgba(0,0,0,0.4); color: var(--cyan);
font-size: 10px; cursor: pointer;
}
.crit-pill .x:hover { background: rgba(255,122,138,0.3); color: #fff; }
.crit-pill .kind { color: var(--fg-muted); margin-right: 2px; }
.crit-pill .conn { color: var(--fg-muted); font-size: 10px; padding: 0 2px; }
.clear-all {
margin-left: auto;
color: var(--fg-muted); font-size: 11px;
background: none; border: none; cursor: pointer;
text-decoration: underline; padding: 0;
font-family: inherit;
}
.clear-all:hover { color: var(--coral); }
/* popover */
.popup-wrap { position: relative; display: inline-block; }
.popup {
position: absolute; top: calc(100% + 6px); left: 0;
background: var(--bg-0);
border: 1px solid var(--glass-border-strong);
border-radius: 14px;
padding: 12px;
box-shadow: 0 24px 60px rgba(0,0,0,0.7);
width: 540px;
z-index: 20;
display: none;
}
.popup.open { display: block; }
.tabs {
display: flex; gap: 2px; border-bottom: 1px solid var(--glass-border);
padding-bottom: 8px; margin-bottom: 10px; flex-wrap: wrap;
}
.tab {
padding: 5px 10px; border-radius: 6px;
font-size: 12px; color: var(--fg-muted); cursor: pointer;
user-select: none;
display: inline-flex; align-items: center; gap: 5px;
background: transparent; border: none;
font-family: inherit;
}
.tab:hover { background: var(--glass); color: var(--fg-dim); }
.tab.active { background: var(--glass-strong); color: var(--cyan); }
.tab .badge {
display: inline-flex; align-items: center; justify-content: center;
min-width: 14px; height: 14px; padding: 0 4px;
border-radius: 9999px; background: var(--cyan); color: #000;
font-size: 9px; font-weight: 700;
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
}
.popup-controls {
display: flex; align-items: center; gap: 8px; margin-bottom: 8px;
}
.popup-search { position: relative; flex: 1; }
.popup-search input {
width: 100%; background: var(--glass); border: 1px solid var(--glass-border);
border-radius: 8px; padding: 6px 10px 6px 28px; color: var(--fg);
font-size: 13px; outline: none;
font-family: inherit;
}
.popup-search .ico { position: absolute; left: 8px; top: 50%; transform: translateY(-50%); color: var(--fg-muted); width: 13px; height: 13px; }
.mode-toggle {
display: inline-flex;
border: 1px solid var(--glass-border);
border-radius: 8px;
overflow: hidden;
font-size: 11px;
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
user-select: none;
}
.mode-toggle .seg {
padding: 5px 10px; cursor: pointer;
color: var(--fg-muted); background: transparent;
border: none; font-family: inherit; font-size: 11px;
}
.mode-toggle .seg.on { background: var(--cyan); color: #000; font-weight: 700; }
.popup-list { max-height: 240px; overflow-y: auto; padding: 2px; }
.popup-list .row {
display: flex; align-items: center; gap: 10px;
padding: 6px 8px; border-radius: 6px; cursor: pointer;
font-size: 13px; color: var(--fg-dim);
user-select: none;
}
.popup-list .row:hover { background: var(--glass); color: var(--fg); }
.popup-list .row.checked { color: var(--cyan); }
.popup-list .row .name { flex: 1; }
.popup-list .row .count {
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
color: var(--fg-muted); font-size: 11px;
}
.check {
width: 14px; height: 14px; border-radius: 3px;
border: 1.5px solid var(--glass-border-strong);
display: inline-flex; align-items: center; justify-content: center;
font-size: 9px; color: var(--cyan);
flex-shrink: 0;
}
.check.on { background: rgba(77,217,230,0.18); border-color: var(--cyan); }
.popup-footer {
margin-top: 8px; padding-top: 8px;
border-top: 1px solid var(--glass-border);
display: flex; align-items: center; justify-content: space-between;
font-size: 11px; color: var(--fg-muted);
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
}
.popup-footer button {
background: none; border: none; color: var(--coral);
font-size: 11px; cursor: pointer; font-family: inherit;
text-decoration: underline;
}
/* missing popover */
.missing-popup {
position: absolute; top: calc(100% + 6px); left: 0;
background: var(--bg-0);
border: 1px solid var(--glass-border-strong);
border-radius: 12px;
padding: 8px;
box-shadow: 0 16px 40px rgba(0,0,0,0.55);
width: 220px;
z-index: 20;
display: none;
}
.missing-popup.open { display: block; }
.check-row {
display: flex; align-items: center; gap: 8px;
padding: 6px 6px; border-radius: 6px; cursor: pointer;
color: var(--fg-dim); font-size: 13px;
user-select: none;
}
.check-row:hover { background: var(--glass); color: var(--fg); }
.check-row .count {
margin-left: auto;
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
color: var(--fg-muted); font-size: 11px;
}
/* letter bar */
.letters { display: flex; gap: 4px; margin-top: 12px; }
.letter {
flex: 1; text-align: center;
border: 1px solid var(--glass-border);
border-radius: 8px; padding: 6px 0 4px;
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
font-size: 13px; color: var(--fg-muted); background: var(--glass);
}
.letter.on { background: var(--cyan); color: #000; border-color: transparent; font-weight: 600; }
.letter.dim { opacity: 0.35; border-color: transparent; }
.letter .num { display: block; font-size: 9px; margin-top: 2px; }
.note {
margin-top: 14px; font-size: 11px; color: var(--fg-muted);
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
}
code {
background: var(--glass); padding: 1px 5px; border-radius: 4px;
font-size: 12px; color: var(--cyan);
}
/* SQL preview */
.sql-preview {
margin-top: 14px;
padding: 10px 12px;
background: rgba(0,0,0,0.4);
border: 1px solid var(--glass-border);
border-radius: 8px;
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
font-size: 11px;
color: var(--fg-dim);
line-height: 1.5;
white-space: pre-wrap;
word-break: break-word;
}
.sql-preview .kw { color: var(--violet); }
.sql-preview .lit { color: var(--mint); }
.sql-preview .col { color: var(--cyan); }
</style>
</head>
<body>
<div class="page">
<h1>Filter Bar — Option 2 refined</h1>
<p class="lede">
Single <code>Filter by ▾</code> popover with tabs. Each tab supports multi-select and a per-type AND/OR toggle.
Active criteria render as removable pills above the letter bar. Single-entity pages (e.g. <code>/actress/aika</code>) stay
as-is — selecting an entity here adds an extra criterion on top of whatever you're viewing.
</p>
<div class="stage">
<div class="stage-label">Live mockup — click anything</div>
<div class="bar">
<button class="chip active">All</button>
<div class="popup-wrap" id="filterWrap">
<button class="chip" id="filterChip" onclick="togglePopup('filterPopup')">
⛓ Filter by <span id="filterBadge" class="badge" style="display:none">0</span>
<span class="caret"></span>
</button>
<div class="popup" id="filterPopup">
<div class="tabs" id="tabs"></div>
<div class="popup-controls">
<div class="popup-search">
<span class="ico">🔍</span>
<input id="popupSearch" placeholder="Filter list…" oninput="renderList()" />
</div>
<div class="mode-toggle" id="modeToggle">
<button class="seg on" onclick="setMode('OR')">OR</button>
<button class="seg" onclick="setMode('AND')">AND</button>
</div>
</div>
<div class="popup-list" id="popupList"></div>
<div class="popup-footer">
<span id="footerHint">tap row to toggle · OR = match any · AND = match all</span>
<button onclick="clearTab()">Clear this tab</button>
</div>
</div>
</div>
<div class="popup-wrap" id="missingWrap">
<button class="chip" id="missingChip" onclick="togglePopup('missingPopup')">
⊘ Missing <span id="missingBadge" class="badge" style="display:none">0</span>
<span class="caret"></span>
</button>
<div class="missing-popup" id="missingPopup">
<div class="check-row" onclick="toggleMissing('unwatched')">
<span class="check" id="m-unwatched">·</span>Unwatched <span class="count">12</span>
</div>
<div class="check-row" onclick="toggleMissing('uncollected')">
<span class="check" id="m-uncollected">·</span>No Collection <span class="count">8</span>
</div>
<div class="check-row" onclick="toggleMissing('untagged')">
<span class="check" id="m-untagged">·</span>No Tags <span class="count">17</span>
</div>
</div>
</div>
<div class="right-group">
<div class="search">
<span class="ico">🔍</span>
<input placeholder="Search Code, Title, Notes…" />
</div>
<button class="sort">⏱ Newest First ▾</button>
</div>
</div>
<!-- Active criteria strip -->
<div class="crit-strip" id="critStrip">
<span class="label">Active</span>
<span id="critList"></span>
<span id="critEmpty" class="crit-empty">no filters — showing all covers</span>
<button id="clearAll" class="clear-all" onclick="clearAll()" style="display:none">clear all</button>
</div>
<!-- Letter bar (untouched) -->
<div class="letters">
<div class="letter on">ALL<span class="num">26</span></div>
<div class="letter">A<span class="num">4</span></div>
<div class="letter">B<span class="num">3</span></div>
<div class="letter">C<span class="num">1</span></div>
<div class="letter dim">D<span class="num">·</span></div>
<div class="letter">E<span class="num">1</span></div>
<div class="letter">I<span class="num">3</span></div>
<div class="letter">K<span class="num">3</span></div>
<div class="letter">L<span class="num">1</span></div>
<div class="letter">M<span class="num">6</span></div>
<div class="letter">N<span class="num">1</span></div>
<div class="letter">P<span class="num">1</span></div>
<div class="letter">R<span class="num">2</span></div>
<div class="letter">S<span class="num">3</span></div>
<div class="letter">U<span class="num">1</span></div>
<div class="letter">Y<span class="num">1</span></div>
<div class="letter dim">Z<span class="num">·</span></div>
</div>
<!-- SQL preview -->
<div class="sql-preview" id="sqlPreview"></div>
</div>
<p class="note">URL example with current selection: <code id="urlExample">/?</code></p>
</div>
<script>
// Mock catalog data
const data = {
actresses: { icon: "👤", items: [
{ id: 1, name: "Aoi Kururugi", count: 3 },
{ id: 2, name: "Aika", count: 0 },
{ id: 3, name: "Eru Sato", count: 1 },
{ id: 4, name: "Ichigo Aoi", count: 2 },
{ id: 5, name: "Ichika Matsumoto", count: 5 },
{ id: 6, name: "Kana Kusakabe", count: 1 },
{ id: 7, name: "Lala Kudo", count: 2 },
{ id: 8, name: "Mitsuki Nagisa", count: 1 },
{ id: 9, name: "Reika Aiba", count: 2 },
{ id: 10, name: "Shuri Atomi", count: 4 },
]},
studios: { icon: "🏢", items: [
{ id: 1, name: "SOD", count: 12 },
{ id: 2, name: "S1", count: 8 },
{ id: 3, name: "Moodyz", count: 6 },
{ id: 4, name: "Madonna", count: 3 },
{ id: 5, name: "IdeaPocket", count: 4 },
]},
series: { icon: "🎬", items: [
{ id: 1, name: "First Time", count: 4 },
{ id: 2, name: "Schoolgirl Diaries", count: 6 },
{ id: 3, name: "Office Lady", count: 3 },
]},
genres: { icon: "#", items: [
{ id: 1, name: "Schoolgirl", count: 12 },
{ id: 2, name: "Drama", count: 5 },
{ id: 3, name: "Comedy", count: 2 },
{ id: 4, name: "Romance", count: 7 },
{ id: 5, name: "Solo", count: 3 },
]},
collections: { icon: "📁", items: [
{ id: 1, name: "Watchlist", count: 9 },
{ id: 2, name: "Best Of 2025", count: 14 },
]},
tags: { icon: "🏷", items: [
{ id: 1, name: "favorite", count: 11 },
{ id: 2, name: "vip", count: 3 },
{ id: 3, name: "rewatch", count: 5 },
{ id: 4, name: "saved", count: 8 },
]},
};
// State
const state = {
selected: { actresses: new Set(), studios: new Set(), series: new Set(), genres: new Set(), collections: new Set(), tags: new Set() },
mode: { actresses: "OR", studios: "OR", series: "OR", genres: "OR", collections: "OR", tags: "OR" },
activeTab: "actresses",
missing: new Set(),
};
function renderTabs() {
const el = document.getElementById("tabs");
el.innerHTML = "";
for (const key of Object.keys(data)) {
const t = data[key];
const selectedCount = state.selected[key].size;
const cap = key.charAt(0).toUpperCase() + key.slice(1);
const btn = document.createElement("button");
btn.className = "tab" + (state.activeTab === key ? " active" : "");
btn.onclick = () => { state.activeTab = key; document.getElementById("popupSearch").value = ""; renderTabs(); renderList(); renderModeToggle(); };
btn.innerHTML = `${t.icon} ${cap}` + (selectedCount > 0 ? ` <span class="badge">${selectedCount}</span>` : "");
el.appendChild(btn);
}
}
function renderList() {
const el = document.getElementById("popupList");
const q = (document.getElementById("popupSearch")?.value ?? "").toLowerCase();
const tab = data[state.activeTab];
const sel = state.selected[state.activeTab];
el.innerHTML = "";
for (const it of tab.items) {
if (q && !it.name.toLowerCase().includes(q)) continue;
const row = document.createElement("div");
const checked = sel.has(it.id);
row.className = "row" + (checked ? " checked" : "");
row.onclick = (ev) => {
ev.stopPropagation();
if (checked) sel.delete(it.id); else sel.add(it.id);
renderAll();
};
row.innerHTML = `
<span class="check ${checked ? "on" : ""}">${checked ? "✓" : ""}</span>
<span class="name">${it.name}</span>
<span class="count">${it.count}</span>
`;
el.appendChild(row);
}
}
function renderModeToggle() {
const segs = document.querySelectorAll("#modeToggle .seg");
const cur = state.mode[state.activeTab];
segs[0].classList.toggle("on", cur === "OR");
segs[1].classList.toggle("on", cur === "AND");
}
function setMode(mode) {
state.mode[state.activeTab] = mode;
renderModeToggle();
renderCriteria();
renderSql();
}
function clearTab() {
state.selected[state.activeTab].clear();
renderAll();
}
function clearAll() {
for (const k of Object.keys(state.selected)) state.selected[k].clear();
state.missing.clear();
renderAll();
}
function toggleMissing(key) {
if (state.missing.has(key)) state.missing.delete(key); else state.missing.add(key);
renderAll();
}
function renderMissing() {
for (const k of ["unwatched", "uncollected", "untagged"]) {
const el = document.getElementById("m-" + k);
const on = state.missing.has(k);
el.classList.toggle("on", on);
el.textContent = on ? "✓" : "·";
}
const total = state.missing.size;
const badge = document.getElementById("missingBadge");
badge.textContent = total;
badge.style.display = total > 0 ? "inline-flex" : "none";
document.getElementById("missingChip").classList.toggle("active", total > 0);
}
function renderFilterChip() {
let total = 0;
for (const k of Object.keys(state.selected)) total += state.selected[k].size;
const badge = document.getElementById("filterBadge");
badge.textContent = total;
badge.style.display = total > 0 ? "inline-flex" : "none";
document.getElementById("filterChip").classList.toggle("active", total > 0);
}
function renderCriteria() {
const list = document.getElementById("critList");
const empty = document.getElementById("critEmpty");
const clearAllBtn = document.getElementById("clearAll");
list.innerHTML = "";
let total = 0;
for (const key of Object.keys(state.selected)) {
const sel = state.selected[key];
if (sel.size === 0) continue;
const items = data[key].items.filter((i) => sel.has(i.id));
const mode = state.mode[key];
const conn = mode === "AND" ? "AND" : "OR";
items.forEach((it, i) => {
const pill = document.createElement("span");
pill.className = "crit-pill";
pill.innerHTML = `
<span class="kind">${data[key].icon}</span>
${it.name}
<span class="x" title="Remove">✕</span>
`;
pill.querySelector(".x").onclick = () => { sel.delete(it.id); renderAll(); };
list.appendChild(pill);
if (i < items.length - 1) {
const connector = document.createElement("span");
connector.className = "conn";
connector.textContent = conn;
list.appendChild(connector);
}
total++;
});
if (key !== Object.keys(state.selected).filter((k) => state.selected[k].size > 0).slice(-1)[0]) {
const cross = document.createElement("span");
cross.className = "conn";
cross.style.color = "var(--violet)";
cross.textContent = "AND";
list.appendChild(cross);
}
}
for (const m of state.missing) {
if (total > 0 || state.missing.size > 1) {
// already handled by section connector if applicable
}
const pill = document.createElement("span");
pill.className = "crit-pill";
pill.style.background = "rgba(255,122,138,0.12)";
pill.style.borderColor = "rgba(255,122,138,0.4)";
pill.style.color = "var(--coral)";
const labels = { unwatched: "Unwatched", uncollected: "No Collection", untagged: "No Tags" };
pill.innerHTML = `
<span class="kind">⊘</span>
${labels[m]}
<span class="x" title="Remove">✕</span>
`;
pill.querySelector(".x").onclick = () => { state.missing.delete(m); renderAll(); };
list.appendChild(pill);
total++;
}
empty.style.display = total === 0 ? "inline" : "none";
clearAllBtn.style.display = total === 0 ? "none" : "inline-block";
}
function renderSql() {
const parts = [];
for (const key of Object.keys(state.selected)) {
const sel = state.selected[key];
if (sel.size === 0) continue;
const ids = [...sel];
const items = data[key].items.filter((i) => sel.has(i.id));
const names = items.map((i) => `'${i.name}'`).join(", ");
const mode = state.mode[key];
const tableMap = {
actresses: "image_actresses ia",
studios: "i.studio_id",
series: "i.series_id",
genres: "image_genres ig",
collections: "collection_images ci",
tags: "image_tags it",
};
if (key === "studios" || key === "series") {
parts.push(`<span class="kw">AND</span> ${tableMap[key]} <span class="kw">IN</span> (<span class="lit">${names}</span>)`);
} else if (mode === "OR") {
parts.push(`<span class="kw">AND</span> <span class="col">${key}</span> <span class="kw">IN</span> (<span class="lit">${names}</span>)`);
} else {
parts.push(`<span class="kw">AND</span> covers w/ <span class="kw">ALL</span> of (<span class="lit">${names}</span>)`);
}
}
for (const m of state.missing) {
parts.push(`<span class="kw">AND</span> <span class="col">${m}</span>`);
}
const sql = parts.length === 0
? '<span class="kw">SELECT</span> * <span class="kw">FROM</span> <span class="col">images</span> <span class="kw">WHERE</span> deleted_at <span class="kw">IS NULL</span>'
: '<span class="kw">SELECT</span> * <span class="kw">FROM</span> <span class="col">images</span> <span class="kw">WHERE</span> deleted_at <span class="kw">IS NULL</span>\n ' + parts.join("\n ");
document.getElementById("sqlPreview").innerHTML = sql;
}
function renderUrl() {
const sp = new URLSearchParams();
for (const key of Object.keys(state.selected)) {
const sel = state.selected[key];
if (sel.size === 0) continue;
sp.set(key, [...sel].join(","));
if (state.mode[key] === "AND") sp.set(`${key}_mode`, "and");
}
for (const m of state.missing) sp.set(m, "1");
const s = sp.toString();
document.getElementById("urlExample").textContent = "/" + (s ? "?" + s : "");
}
function renderAll() {
renderTabs();
renderList();
renderModeToggle();
renderCriteria();
renderFilterChip();
renderMissing();
renderSql();
renderUrl();
}
function togglePopup(id) {
const p = document.getElementById(id);
const opening = !p.classList.contains("open");
document.querySelectorAll(".popup, .missing-popup").forEach((x) => x.classList.remove("open"));
if (opening) p.classList.add("open");
}
// Any click inside a popover stays inside — prevents the document listener
// from incorrectly closing it when an inner click rebuilds the DOM.
document.querySelectorAll(".popup, .missing-popup").forEach((p) => {
p.addEventListener("click", (e) => e.stopPropagation());
});
document.addEventListener("click", (e) => {
if (!e.target.closest(".popup-wrap")) {
document.querySelectorAll(".popup, .missing-popup").forEach((x) => x.classList.remove("open"));
}
});
// Pre-seed a demo selection so the user sees the chips on load.
state.selected.actresses.add(2); // Aika
state.selected.studios.add(1); // SOD
state.selected.genres.add(1);
state.selected.genres.add(2); // Schoolgirl OR Drama
state.selected.tags.add(1); // favorite
renderAll();
</script>
</body>
</html>