Files
PartnerExpo-Core/public/search_demo.html
2026-02-01 15:24:02 +01:00

999 lines
27 KiB
HTML

<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>Mock Search UI (Masonry)</title>
<style>
.pexpo-core-root {
--accent1: #950000;
--accent2: #2c3489;
--bg: #f6f7fb;
--panel: #ffffff;
--panel2: #f2f4fb;
--stroke: rgba(16, 24, 40, .10);
--stroke2: rgba(16, 24, 40, .14);
--text: rgba(16, 24, 40, .92);
--muted: rgba(16, 24, 40, .62);
--shadow: 0 18px 45px rgba(16,24,40,.12);
--r: 14px;
--cardMin: 240;
--gap: 12px;
--filterW: 170px;
--filterWOpen: 360px;
--topbarH: 64px;
/* Basic font reset for the component only */
font-family: ui-sans-serif, system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, "Apple Color Emoji","Segoe UI Emoji";
color: var(--text);
line-height: 1.35;
font-size: 14px;
}
/* Scoped box-sizing reset */
.pexpo-core-root * { box-sizing: border-box; }
.pexpo-core-shell {
width: min(1100px, 96vw);
height: min(720px, 92vh);
background: linear-gradient(180deg, rgba(44,52,137,.05), transparent 42%), var(--panel);
border: 1px solid var(--stroke);
border-radius: calc(var(--r) + 2px);
box-shadow: var(--shadow);
overflow: hidden;
display:flex;
flex-direction:column;
position: relative;
isolation: isolate;
}
/* --- Top row --- */
.pexpo-core-topbar {
display:flex;
align-items:flex-start;
justify-content:space-between;
gap: 12px;
padding: 14px;
border-bottom: 1px solid var(--stroke);
background: linear-gradient(180deg, rgba(16,24,40,.02), rgba(16,24,40,.00));
position: relative;
z-index: 3;
}
.pexpo-core-topHeight {
height: 45px;
}
/* Filter button */
.pexpo-core-filterWrap {
width: var(--filterW);
transition: width 220ms ease;
}
.pexpo-core-filterWrap.pexpo-core-open { width: var(--filterWOpen); }
.pexpo-core-filterBtn {
width: 100%;
display:flex;
align-items:center;
justify-content:space-between;
gap: 10px;
padding: 10px 10px;
border-radius: 12px;
border: 1px solid var(--stroke2);
background: var(--panel2);
color: var(--text);
cursor:pointer;
user-select:none;
box-shadow: 0 8px 22px rgba(16,24,40,.10);
}
.pexpo-core-filterBtn .pexpo-core-left {
display:flex;
align-items:center;
gap: 10px;
min-width: 0;
}
.pexpo-core-pill {
font-size: 12px;
padding: 3px 8px;
border: 1px solid rgba(44,52,137,.25);
border-radius: 999px;
color: rgba(44,52,137,.85);
background: rgba(44,52,137,.06);
white-space:nowrap;
}
.pexpo-core-filterBtn .pexpo-core-label {
font-weight: 650;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
/* Sort */
.pexpo-core-sortWrap {
display:flex;
justify-content: space-between;
align-items:center;
gap: 10px;
padding: 10px 10px;
border-radius: 12px;
border: 1px solid var(--stroke2);
background: var(--panel2);
color: var(--text);
box-shadow: 0 8px 22px rgba(16,24,40,.10);
flex: 0 0 auto;
}
.pexpo-core-sortLabel {
font-weight: 650;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.pexpo-core-sortSelect {
border: 1px solid rgba(16,24,40,.16);
background: rgba(255,255,255,.95);
color: var(--text);
border-radius: 10px;
padding: 5px 10px;
outline: none;
font: inherit;
height: 35px;
}
.pexpo-core-sortSelect:focus {
border-color: rgba(44,52,137,.45);
box-shadow: 0 0 0 3px rgba(44,52,137,.14);
}
/* Search */
.pexpo-core-searchWrap {
flex: 1 1 auto;
display:flex;
justify-content:flex-end;
align-items:flex-start;
}
.pexpo-core-search {
width: min(520px, 100%);
display:flex;
align-items:center;
gap: 10px;
padding: 10px 12px;
border-radius: 14px;
border: 1px solid var(--stroke2);
background: rgba(255,255,255,.9);
box-shadow: 0 8px 22px rgba(16,24,40,.10);
}
.pexpo-core-search input {
width: 100%;
border: none;
outline: none;
background: transparent;
color: var(--text);
font-size: 14px;
}
.pexpo-core-kbd {
color: rgba(16,24,40,.62);
border: 1px solid rgba(16,24,40,.16);
border-bottom-color: rgba(16,24,40,.22);
background: rgba(255,255,255,.85);
border-radius: 8px;
padding: 2px 8px;
font-size: 12px;
white-space: nowrap;
}
/* --- Main body --- */
.pexpo-core-body {
flex: 1 1 auto;
padding: 14px;
overflow:auto;
position: relative;
z-index: 1;
}
.pexpo-core-metaRow {
display:flex;
align-items:center;
justify-content:space-between;
gap: 10px;
margin-bottom: 12px;
color: var(--muted);
font-size: 12px;
}
/* Masonry */
.pexpo-core-masonry {
display:flex;
gap: var(--gap);
align-items:flex-start;
}
.pexpo-core-mCol {
flex: 1 1 0;
min-width: 0;
display:flex;
flex-direction:column;
gap: var(--gap);
}
.pexpo-core-card {
border-radius: 14px;
border: 1px solid var(--stroke2);
background:
linear-gradient(180deg, rgba(143, 152, 255, 0.05), rgba(255,255,255,.92));
padding: 12px;
box-shadow: 0 12px 26px rgba(16,24,40,.10);
display:flex;
flex-direction:column;
gap: 8px;
}
.pexpo-core-imageWrap {
width: 100%;
height: auto;
border-radius: 10px;
overflow: hidden;
}
.pexpo-core-imageWrap img {
width: 100%;
height: auto;
display: block;
}
.pexpo-core-cardTop {
display:flex;
align-items:flex-start;
justify-content:space-between;
gap: 10px;
}
.pexpo-core-title {
font-weight: 750;
letter-spacing: .15px;
color: var(--text);
&:hover {
color: initial;
}
}
.pexpo-core-tags {
display: flex;
flex-wrap: wrap;
gap: 6px;
row-gap: 6px;
}
.pexpo-core-tag {
font-size: 12px;
padding: 3px 8px;
border-radius: 999px;
border: 1px solid #333333ce;
color: var(--text);
background: #99999930;
white-space:nowrap;
}
.pexpo-core-desc {
color: rgba(16,24,40,.74);
font-size: 13px;
}
.pexpo-core-foot {
margin-top:auto;
display:flex;
align-items:center;
justify-content:space-between;
color: rgba(16,24,40,.60);
font-size: 12px;
border-top: 1px dashed rgba(16,24,40,.16);
padding-top: 8px;
}
.pexpo-core-dot {
width: 7px; height: 7px;
border-radius: 999px;
background: rgba(44,52,137,.55);
display:inline-block;
margin-right: 8px;
}
.pexpo-core-status {
display:flex;
align-items:center;
gap: 0;
}
/* Filter drawer */
.pexpo-core-filterDrawerBackdrop {
position:absolute;
inset: var(--topbarH) 0 0 0;
background: rgba(16,24,40,.20);
backdrop-filter: blur(2px);
opacity: 0;
pointer-events: none;
transition: opacity 180ms ease;
z-index: 2;
}
.pexpo-core-filterDrawerBackdrop.pexpo-core-show {
opacity: 1;
pointer-events: auto;
}
.pexpo-core-filterDrawer {
position:absolute;
top: var(--topbarH);
bottom: 0;
left: 14px;
width: var(--filterW);
border-radius: 14px;
border: 1px solid var(--stroke2);
background: rgba(255,255,255,.92);
overflow:hidden;
box-shadow: 0 18px 45px rgba(16,24,40,.18);
opacity: 0;
transform: translateY(-8px);
pointer-events: none;
transition: opacity 200ms ease, transform 200ms ease, width 220ms ease;
z-index: 3;
display:flex;
flex-direction:column;
min-height: 0;
margin: 12.5px 0 12.5px 0;
}
.pexpo-core-filterDrawer.pexpo-core-open {
opacity: 1;
transform: translateY(0);
pointer-events: auto;
width: var(--filterWOpen);
}
.pexpo-core-filterPanelHeader {
display:flex;
align-items:center;
justify-content:space-between;
padding: 10px 10px;
border-bottom: 1px solid var(--stroke);
color: rgba(16,24,40,.60);
background: linear-gradient(180deg, rgba(44,52,137,.05), rgba(255,255,255,.0));
flex: 0 0 auto;
}
.pexpo-core-filterPanelHeader b { color: var(--text); }
.pexpo-core-jsonBox {
padding: 10px;
flex: 1 1 auto;
overflow:auto;
min-height: 0;
}
.pexpo-core-textarea {
width: 100%;
height: 210px;
resize: vertical;
min-height: 180px;
max-height: 60vh;
border-radius: 10px;
border: 1px solid rgba(44,52,137,.20);
background: #fbfbfe;
color: rgba(16,24,40,.92);
padding: 10px;
font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;
font-size: 12px;
line-height: 1.35;
outline: none;
}
.pexpo-core-textarea:focus {
border-color: rgba(44,52,137,.45);
box-shadow: 0 0 0 3px rgba(44,52,137,.14);
}
.pexpo-core-hint {
margin-top: 8px;
color: rgba(16,24,40,.60);
font-size: 12px;
}
.pexpo-core-error {
margin-top: 8px;
color: #950000;
font-size: 12px;
display:none;
}
.pexpo-core-error.pexpo-core-show { display:block; }
/* Drawer open padding */
.pexpo-core-shell.pexpo-core-drawerOpen .pexpo-core-body {
/* padding-left: calc(14px + var(--filterWOpen) + 14px); */
transition: padding-left 220ms ease;
}
.pexpo-core-shell:not(.pexpo-core-drawerOpen) .pexpo-core-body {
transition: padding-left 220ms ease;
}
/* Icons */
.pexpo-core-icons-search {
box-sizing: border-box;
position: relative;
display: block;
transform: scale(var(--ggs, 1));
width: 16px;
height: 16px;
border: 2px solid;
border-radius: 100%;
margin-left: -4px;
margin-top: -4px;
}
.pexpo-core-icons-search:after {
content: "";
display: block;
box-sizing: border-box;
position: absolute;
border-radius: 3px;
width: 3px;
height: 8px;
background: currentColor;
transform: rotate(-45deg);
top: 10px;
left: 12px;
}
.pexpo-core-icons-down {
box-sizing: border-box;
position: relative;
display: block;
transform: scale(var(--ggs, 1));
width: 22px;
height: 22px;
border: 2px solid transparent;
border-radius: 100px;
}
.pexpo-core-icons-down:after {
content: "";
display: block;
box-sizing: border-box;
position: absolute;
width: 10px;
height: 10px;
border-bottom: 2px solid;
border-right: 2px solid;
transform: rotate(45deg);
transition: 1s transform ease;
left: 4px;
top: 2px;
}
.pexpo-core-filterWrap.pexpo-core-open .pexpo-core-icons-down {
transform: rotate(-135deg) translate(-1px,-1px);
}
.pexpo-core-icons-options {
box-sizing: border-box;
position: relative;
display: block;
transform: scale(var(--ggs, 1));
width: 10px;
height: 2px;
box-shadow:
-3px 4px 0 0,
3px -4px 0 0;
margin: 0 10px 0 10px;
}
.pexpo-core-icons-options::after,
.pexpo-core-icons-options::before {
content: "";
display: block;
box-sizing: border-box;
position: absolute;
width: 8px;
height: 8px;
border: 2px solid;
border-radius: 100%;
}
.pexpo-core-icons-options::before {
top: -7px;
left: -4px;
}
.pexpo-core-icons-options::after {
bottom: -7px;
right: -4px;
}
@media (max-width: 720px) {
.pexpo-core-topbar { flex-direction:column; }
.pexpo-core-searchWrap { justify-content:stretch; }
.pexpo-core-search { width: 100%; }
.pexpo-core-filterWrap { width: 100%; }
.pexpo-core-filterWrap.pexpo-core-open { width: 100%; }
.pexpo-core-sortWrap { width: 100%; }
.pexpo-core-sortSelect { width: 110px; }
.pexpo-core-searchWrap { width: 100%; }
.pexpo-core-filterDrawer {
left: 14px;
right: 14px;
width: auto;
}
.pexpo-core-filterDrawer.pexpo-core-open { width: auto; }
.pexpo-core-shell.pexpo-core-drawerOpen .pexpo-core-body { padding-left: 14px; }
}
</style>
</head>
<div class="pexpo-core-root pexpo-core-shell" id="shell">
<div class="pexpo-core-topbar" id="topbar">
<div class="pexpo-core-filterWrap" id="filterWrap">
<div class="pexpo-core-filterBtn pexpo-core-topHeight" id="filterBtn" role="button" aria-expanded="false" tabindex="0">
<div class="pexpo-core-left">
<span class="pexpo-core-icons-options" aria-hidden="true"></span>
<span class="pexpo-core-label" id="filterLabel">Filterek</span>
</div>
<span class="pexpo-core-icons-down" aria-hidden="true"></span>
</div>
</div>
<div class="pexpo-core-sortWrap pexpo-core-topHeight">
<label class="pexpo-core-sortLabel" for="sortSelect">Rendezés</label>
<select id="sortSelect" class="pexpo-core-sortSelect">
</select>
</div>
<div class="pexpo-core-searchWrap pexpo-core-topHeight">
<div class="pexpo-core-search" role="search">
<span class="pexpo-core-icons-search" aria-hidden="true"></span>
<form id="qForm" action="javascript:void(0)">
<input id="q" placeholder="Keresés..." autocomplete="off" />
</form>
</div>
</div>
</div>
<div class="pexpo-core-filterDrawerBackdrop" id="drawerBackdrop" aria-hidden="true"></div>
<div class="pexpo-core-filterDrawer" id="filterDrawer" aria-hidden="true">
<div class="pexpo-core-filterPanelHeader">
<span><b>Filter JSON</b> (edit + apply)</span>
<span class="pexpo-core-pill" id="applyHint">Ctrl + Enter</span>
</div>
<div class="pexpo-core-jsonBox">
<textarea id="filterJson" class="pexpo-core-textarea" spellcheck="false">
{
"category": "all",
"sort": "relevance",
"status": ["active", "archived"],
"minScore": 0,
"tags": ["ui", "mock"],
"maxResults": 17
}
</textarea>
<div class="pexpo-core-error" id="jsonError">Invalid JSON. Fix the syntax and try again.</div>
</div>
</div>
<div class="pexpo-core-body">
<div class="pexpo-core-metaRow">
<div id="metaLeft"><b id="count">0</b> találat</div>
<div id="metaRight">Elrendezés: <span id="layoutMeta"></span></div>
</div>
<div class="pexpo-core-masonry" id="masonry" aria-live="polite"></div>
<div id="measure" style="position:absolute; left:-9999px; top:-9999px; width:300px; visibility:hidden;"></div>
</div>
</div>
<script>
document.addEventListener('DOMContentLoaded', () => {
// ---------- DOM Elements ----------
const shell = document.getElementById("shell");
const topbar = document.getElementById("topbar");
const filterWrap = document.getElementById("filterWrap");
const filterBtn = document.getElementById("filterBtn");
const filterDrawer = document.getElementById("filterDrawer");
const drawerBackdrop = document.getElementById("drawerBackdrop");
const filterJson = document.getElementById("filterJson");
const jsonError = document.getElementById("jsonError");
const filterLabel = document.getElementById("filterLabel");
const qInput = document.getElementById("q");
const masonry = document.getElementById("masonry");
const measure = document.getElementById("measure");
const count = document.getElementById("count");
const layoutMeta = document.getElementById("layoutMeta");
const sortSelect = document.getElementById("sortSelect");
// ---------- Mock Data Generator (Restored & Adapted) ----------
function makeResults(n, q = "") {
const words = [
"apple","river","mountain","cloud","stone","forest","breeze","candle","mirror","shadow",
"sunrise","sunset","ocean","island","valley","thunder","lightning","rain","snow","frost",
"ember","flame","smoke","ash","spark","comet","star","planet","orbit","galaxy",
"nebula","vacuum","gravity","rocket","satellite","signal","beacon","radar","sonar","echo",
"whisper","melody","rhythm","harmony","tempo","chord","lyric","verse","chorus","bridge",
"canvas","pigment","brush","palette","sketch","mural","statue","marble","bronze","copper",
"silver","golden","velvet","cotton","linen","silk","denim","leather","paper","ink",
"quill","notebook","journal","diary","letter","envelope","stamp","postcard","parcel","package",
"crate","barrel","bottle","jar","kettle","teapot","mug","cup","saucer","plate",
"bowl","spoon","fork","knife","napkin","table","chair","couch","pillow","blanket",
"curtain","window","door","hallway","kitchen","pantry","cellar","attic","ladder","hammer",
"nail","screw","wrench","pliers","drill","saw","chisel","ruler","compass","protractor",
"calculator","keyboard","mouse","screen","monitor","printer","scanner","speaker","microphone","camera",
"tripod","lens","shutter","focus","zoom","pixel","vector","shader","texture","model",
"render","frame","layer","filter","gradient","border","margin","padding","align","justify",
"center","left","right","top","bottom","header","footer","sidebar","menu","button",
"toggle","slider","checkbox","radio","select","option","input","output","submit","cancel",
"confirm","alert","notice","banner","badge","icon","symbol","logo","brand","slogan",
"motto","phrase","idiom","proverb","story","novel","poem","essay","article","report",
"summary","outline","draft","revision","edition","volume","chapter","page","paragraph","sentence",
"word","letterform","alphabet","grammar","syntax","semantics","context","nuance","tone","mood",
"theme","motif","plot","scene","actor","director","writer","editor","critic","reader",
"viewer","listener","audience","crowd","group","team","crew","squad","unit","league",
"match","game","round","score","point","level","stage","arena","field","court",
"track","trail","path","route","journey","voyage","trip","tour","adventure","quest",
"mission","challenge","victory","legend"
];
const out = [];
for (let i = 1; i <= n; i++) {
const score = Math.round(Math.random() * 100);
const extra = " ".repeat(Math.floor(Math.random() * 3)).replace(/ /g, "—");
// Generate a title containing query words if query exists (to simulate search hits)
let title = "";
if(q && Math.random() > 0.3) {
title = `${q} ${words[Math.floor(Math.random() * words.length)]}`;
} else {
title = Math.random() < 0.3
? `The ${words[Math.floor(Math.random() * words.length)]} of ${words[Math.floor(Math.random() * words.length)]}`
: `${words[Math.floor(Math.random() * words.length)]} ${words[Math.floor(Math.random() * words.length)]}`;
}
// Format for New Prod Schema
out.push({
id: i,
title: title.charAt(0).toUpperCase() + title.slice(1),
url: "#",
// Adapting tags to be an array for the new renderer
tag: (() => {
const numberOfTags = Math.floor(Math.random() * 5) + 1; // 1-5 tags
// Determine random selection
return words.slice().sort(() => 0.5 - Math.random()).slice(0, numberOfTags);
})(),
excerpt:
"Lorem ipsum dolor sit amet consectetur adipiscing elit. Consectetur adipiscing elit quisque faucibus ex sapien vitae." +
(score % 3 === 0 ? " Ex sapien vitae pellentesque sem placerat in id." : "") +
(score % 5 === 0 ? " Placerat in id cursus mi pretium tellus duis. Pretium tellus duis convallis tempus leo eu aenean." : "") +
(extra ? "" : ""),
score, // Internal use for sorting
image: `https://placehold.co/600x400?text=${encodeURIComponent(title)}`,
date: new Date(Date.now() - Math.random() * 1000 * 60 * 60 * 24 * 30)
.toISOString()
.slice(0, 10),
relevance: 0 // Computed locally
});
}
return out;
}
// ---------- State ----------
let filters = {
sort: "relevance",
status: ["active", "archived"],
tags: ["ui", "mock"],
maxResults: 20
};
let activeSortKey = "relevance";
let results = [];
// ---------- Sort Definitions ----------
const SORTS = {
relevance: {
label: "Relevance",
compare: (a, b) => (b.relevance - a.relevance) || (b.score - a.score)
},
score_desc: {
label: "Score (high → low)",
compare: (a, b) => (b.score - a.score)
},
date_desc: {
label: "Date (new → old)",
compare: (a, b) => new Date(b.date) - new Date(a.date)
},
title_asc: {
label: "Title (A → Z)",
compare: (a, b) => (a.title || "").localeCompare(b.title || "")
}
};
// ---------- Helpers ----------
function escapeHtml(str) {
return String(str).replace(/[&<>"']/g, s => ({
"&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#039;"
}[s]));
}
// NOTE: This uses the new Production HTML structure and Classes
function cardEl(r) {
const div = document.createElement("div");
div.className = "pexpo-core-card";
div.innerHTML = `
<div class="pexpo-core-imageWrap">
<img src="${escapeHtml(r.image || '')}"
alt="${escapeHtml(r.title)}"
style="width:100%; border-radius:10px; object-fit:cover;" />
</div>
<div class="pexpo-core-cardTop">
<a href="${escapeHtml(r.url)}" class="pexpo-core-title">${escapeHtml(r.title)}</a>
</div>
<div class="pexpo-core-desc">${escapeHtml(r.excerpt)}</div>
<div class="pexpo-core-foot">
<div class="pexpo-core-tags">
${
Array.isArray(r.tag)
? r.tag.map(element => `<span class="pexpo-core-tag">${escapeHtml(element)}</span>`).join('')
: ''
}
</div>
<div>${escapeHtml(r.date)}</div>
</div>
`;
return div;
}
// ---------- Logic: Data Fetch & Relevance ----------
// Local client-side relevance scorer (Restored from old version)
function computeRelevance(r, q) {
const query = (q || "").trim().toLowerCase();
if (!query) return r.score;
const terms = query.split(/\s+/).filter(Boolean);
if (!terms.length) return r.score;
const title = (r.title || "").toLowerCase();
// Check title, excerpt, and tags
const hay = `${r.title} ${r.excerpt} ${r.tag.join(' ')}`.toLowerCase();
let hits = 0;
let titleHits = 0;
for (const t of terms) {
if (hay.includes(t)) hits++;
if (title.includes(t)) titleHits++;
}
// Weight formula
return (hits * 30) + (titleHits * 25) + (r.score * 0.35);
}
function recomputeRelevance(q) {
for (const r of results) r.relevance = computeRelevance(r, q);
}
// Mock Fetcher
async function fetchData(query = "") {
// 1. Simulate Loading
masonry.style.opacity = "0.5";
// Simulate network delay (200ms) for realism
await new Promise(resolve => setTimeout(resolve, 200));
try {
// 2. Generate Data locally instead of fetching from WP
const count = Number(filters.maxResults) || 20;
results = makeResults(Math.min(count, 100), query);
// 3. Compute relevance locally
recomputeRelevance(query);
// 4. Trigger Layout
requestLayout();
} catch (error) {
console.error("Mock generation failed:", error);
masonry.innerHTML = `<div style="padding:20px; color:red;">Hiba történt a keresés során.</div>`;
} finally {
masonry.style.opacity = "1";
}
}
// ---------- Layout & Masonry Logic ----------
const minCard = () => Number(getComputedStyle(document.documentElement).getPropertyValue("--cardMin")) || 240;
const gap = () => Number(getComputedStyle(document.documentElement).getPropertyValue("--gap")) || 12;
let layoutQueued = false;
function requestLayout() {
if (layoutQueued) return;
layoutQueued = true;
requestAnimationFrame(() => {
layoutQueued = false;
applyMasonry();
});
}
function getColumnCount() {
const w = masonry.clientWidth || masonry.getBoundingClientRect().width || 1;
const mc = minCard();
const g = gap();
return Math.max(1, Math.floor((w + g) / (mc + g)));
}
function measureCardHeights(cols, list) {
const w = masonry.clientWidth || 1;
const g = gap();
const colW = Math.floor((w - (cols - 1) * g) / cols);
measure.style.width = colW + "px";
measure.innerHTML = "";
const measured = list.map((r, idx) => {
const el = cardEl(r);
measure.appendChild(el);
const h = el.offsetHeight;
measure.removeChild(el);
return { r, idx, h };
});
return { measured, colW };
}
function packRankAware(measured, cols, K = 6) {
const colHeights = new Array(cols).fill(0);
const placements = [];
const queue = measured.slice();
while (queue.length) {
let bestCol = 0;
for (let c = 1; c < cols; c++) {
if (colHeights[c] < colHeights[bestCol]) bestCol = c;
}
const lookN = Math.min(K, queue.length);
let pick = 0;
for (let i = 1; i < lookN; i++) {
if (queue[i].h > queue[pick].h) pick = i;
}
const picked = queue.splice(pick, 1)[0];
placements.push({ item: picked, col: bestCol });
colHeights[bestCol] += picked.h + gap();
}
return placements;
}
function applyMasonry() {
const n = results.length || 0;
count.textContent = n;
masonry.innerHTML = "";
const cols = getColumnCount();
// Create Columns
const colEls = [];
for (let c = 0; c < cols; c++) {
const col = document.createElement("div");
col.className = "pexpo-core-mCol";
masonry.appendChild(col);
colEls.push(col);
}
// 1. Sort Data
const sorter = SORTS[activeSortKey] || SORTS.relevance;
const sorted = results.slice().sort(sorter.compare);
// 2. Measure & Pack
const { measured } = measureCardHeights(cols, sorted);
const placements = packRankAware(measured, cols, 6);
// 3. Render
for (const p of placements) {
colEls[p.col].appendChild(cardEl(p.item.r));
}
layoutMeta.textContent = `${cols} cols • ${SORTS[activeSortKey]?.label || "Relevance"}`;
}
// ---------- UI Events ----------
// Sort Dropdown
if (sortSelect) {
sortSelect.innerHTML = Object.entries(SORTS)
.map(([k, def]) => `<option value="${escapeHtml(k)}">${escapeHtml(def.label)}</option>`)
.join("");
sortSelect.value = activeSortKey;
sortSelect.addEventListener("change", () => {
activeSortKey = sortSelect.value;
requestLayout(); // Just relayout, no need to re-fetch
});
}
// Search
qInput.addEventListener("keydown", (e) => {
if (e.key === "Enter") {
fetchData(qInput.value.trim());
}
});
// Filter Drawer Logic
function syncTopbarHeight() {
const h = topbar.offsetHeight;
document.documentElement.style.setProperty("--topbarH", h + "px");
}
function setOpen(open) {
filterWrap.classList.toggle("pexpo-core-open", open);
filterBtn.setAttribute("aria-expanded", String(open));
filterDrawer.classList.toggle("pexpo-core-open", open);
filterDrawer.setAttribute("aria-hidden", String(!open));
drawerBackdrop.classList.toggle("pexpo-core-show", open);
shell.classList.toggle("pexpo-core-drawerOpen", open);
syncTopbarHeight();
if (open) filterJson.focus();
}
filterBtn.addEventListener("click", () => setOpen(!filterDrawer.classList.contains("pexpo-core-open")));
drawerBackdrop.addEventListener("click", () => setOpen(false));
document.addEventListener("keydown", (e) => {
if (e.key === "Escape" && filterDrawer.classList.contains("pexpo-core-open")) setOpen(false);
});
// Apply JSON Filter
function applyJson() {
try {
const parsed = JSON.parse(filterJson.value);
jsonError.classList.remove("pexpo-core-show");
filters = parsed;
filterLabel.textContent = (filters.category || "Custom");
// Trigger new search (generation) with new filters
fetchData(qInput.value.trim());
} catch (err) {
jsonError.classList.add("pexpo-core-show");
}
}
filterJson.addEventListener("keydown", (e) => {
if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) {
e.preventDefault();
applyJson();
}
});
// Resize Observer
const ro = new ResizeObserver(() => {
syncTopbarHeight();
requestLayout();
});
ro.observe(shell);
ro.observe(masonry);
// Initial Load
syncTopbarHeight();
fetchData("");
});
</script>
</body>
</html>