Initial commit

This commit is contained in:
admin
2026-05-26 22:46:00 +02:00
commit 7e2c2ff89c
256 changed files with 51523 additions and 0 deletions
+509
View File
@@ -0,0 +1,509 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Pinkudex — Pagination behavior options (functional)</title>
<style>
:root {
--bg-0: #0b0d10;
--bg-1: #14171c;
--bg-2: #1c2027;
--fg: #e6e8ec;
--fg-dim: #a4abb6;
--fg-muted: #6b7380;
--cyan: #22d3ee;
--mint: #34d399;
--amber: #fbbf24;
--coral: #f87171;
--violet: #a78bfa;
--glass: rgba(255,255,255,0.04);
--glass-border: rgba(255,255,255,0.10);
--glass-border-strong: rgba(255,255,255,0.18);
}
* { box-sizing: border-box; }
html, body { margin: 0; padding: 0; }
body {
background: var(--bg-0);
color: var(--fg);
font: 14px/1.5 ui-sans-serif, system-ui, -apple-system, "Segoe UI";
min-height: 100vh;
}
.page { max-width: 1100px; margin: 0 auto; padding: 24px; }
header { display: flex; justify-content: space-between; align-items: flex-start; margin-bottom: 20px; gap: 24px; }
h1 { font-weight: 500; font-size: 20px; margin: 0 0 4px; }
.sub { color: var(--fg-muted); font-size: 13px; }
.modes {
display: flex; gap: 4px; padding: 4px;
background: var(--bg-1); border: 1px solid var(--glass-border);
border-radius: 10px;
}
.modes button {
background: transparent; border: 0; color: var(--fg-dim);
font-family: inherit; font-size: 12px; font-weight: 500;
padding: 8px 14px; border-radius: 7px; cursor: pointer;
transition: background 120ms, color 120ms;
}
.modes button:hover { color: var(--fg); }
.modes button.active {
background: rgba(34,211,238,0.15);
color: var(--cyan);
box-shadow: inset 0 0 0 1px rgba(34,211,238,0.4);
}
/* Hud panel */
.hud {
display: grid; grid-template-columns: repeat(4, 1fr); gap: 12px;
background: var(--bg-1); border: 1px solid var(--glass-border);
border-radius: 12px; padding: 14px 16px; margin-bottom: 14px;
font-family: ui-monospace, "SF Mono", monospace; font-size: 12px;
}
.hud .item { display: flex; flex-direction: column; gap: 4px; }
.hud .label { color: var(--fg-muted); font-size: 10px; text-transform: uppercase; letter-spacing: 0.08em; }
.hud .value { color: var(--fg); font-weight: 500; }
.hud .value .accent { color: var(--cyan); }
.description {
background: var(--bg-1); border: 1px solid var(--glass-border);
border-radius: 12px; padding: 14px 16px; margin-bottom: 14px;
font-size: 13px; color: var(--fg-dim); line-height: 1.55;
}
.description b { color: var(--fg); }
/* Event log */
.log {
background: rgba(0,0,0,0.35); border: 1px solid var(--glass-border);
border-radius: 10px; padding: 10px 12px; margin-bottom: 14px;
font-family: ui-monospace, "SF Mono", monospace; font-size: 11.5px;
height: 110px; overflow-y: auto;
}
.log .line { padding: 1px 0; color: var(--fg-dim); }
.log .line .t { color: var(--fg-muted); margin-right: 8px; }
.log .line .k { font-weight: 600; margin-right: 8px; }
.log .line .k.url { color: var(--coral); }
.log .line .k.scroll { color: var(--mint); }
.log .line .k.fetch { color: var(--amber); }
.log .line .k.click { color: var(--cyan); }
.log .line .k.remount { color: var(--violet); }
.log .line .k.note { color: var(--fg-muted); }
/* Grid viewport — emulates a virtualized scroll area */
.viewport {
background: var(--bg-1); border: 1px solid var(--glass-border);
border-radius: 14px; height: 540px; overflow-y: auto;
scroll-behavior: smooth;
position: relative;
}
.viewport-inner { padding: 16px; }
.grid {
display: grid; grid-template-columns: repeat(5, 1fr);
gap: 8px;
}
.card {
aspect-ratio: 0.7; border-radius: 8px;
background: linear-gradient(135deg, var(--bg-2), #232831);
border: 1px solid var(--glass-border);
display: flex; flex-direction: column; justify-content: flex-end;
padding: 6px 8px;
font-family: ui-monospace, monospace; font-size: 10px;
color: var(--fg-muted);
position: relative;
overflow: hidden;
animation: fadeIn 200ms ease-out both;
}
.card::before {
content: ""; position: absolute; inset: 0;
background: radial-gradient(circle at 30% 30%, rgba(34,211,238,0.06), transparent 60%);
}
.card.flash { animation: flashCard 600ms ease-out; }
@keyframes flashCard {
0% { box-shadow: inset 0 0 0 2px var(--cyan); }
100% { box-shadow: inset 0 0 0 0 transparent; }
}
@keyframes fadeIn {
from { opacity: 0; transform: translateY(4px); }
to { opacity: 1; transform: none; }
}
.page-marker {
grid-column: 1 / -1;
margin: 18px 0 8px;
padding: 6px 10px;
border-radius: 6px;
background: rgba(34,211,238,0.06);
border-left: 3px solid var(--cyan);
font-family: ui-monospace, monospace; font-size: 11px;
color: var(--cyan); font-weight: 600;
text-transform: uppercase; letter-spacing: 0.08em;
}
.page-marker.current { background: rgba(34,211,238,0.14); }
/* Pagination bar */
.bar {
display: flex; justify-content: center; align-items: center; gap: 10px;
margin-top: 16px; padding: 12px;
background: var(--bg-1); border: 1px solid var(--glass-border);
border-radius: 12px;
}
.bar button {
font-family: inherit; font-size: 13px; padding: 7px 14px;
border-radius: 8px; border: 1px solid var(--glass-border-strong);
background: var(--glass); color: var(--fg); cursor: pointer;
display: inline-flex; align-items: center; gap: 6px;
transition: background 120ms;
}
.bar button:hover:not(:disabled) { background: rgba(255,255,255,0.07); }
.bar button:disabled { opacity: 0.35; cursor: not-allowed; }
.bar .label {
font-family: ui-monospace, monospace; font-size: 13px;
color: var(--fg-dim); padding: 0 14px; min-width: 200px; text-align: center;
}
.bar .label .num { color: var(--cyan); font-weight: 600; }
.bar .jump {
margin-left: 8px; display: inline-flex; align-items: center; gap: 6px;
}
.bar .jump input {
width: 56px; padding: 6px 8px; border-radius: 6px;
border: 1px solid var(--glass-border-strong);
background: var(--bg-0); color: var(--fg);
font-family: ui-monospace, monospace; font-size: 12px; text-align: center;
outline: none;
}
.bar .jump input:focus { border-color: var(--cyan); }
.load-more {
width: 100%; margin-top: 10px;
padding: 10px; border-radius: 8px;
background: rgba(52,211,153,0.08); color: var(--mint);
border: 1px dashed rgba(52,211,153,0.4); cursor: pointer;
font-family: inherit; font-size: 12px;
text-transform: uppercase; letter-spacing: 0.08em; font-weight: 600;
transition: background 120ms;
}
.load-more:hover { background: rgba(52,211,153,0.14); }
.load-more:disabled { opacity: 0.3; cursor: not-allowed; }
.toast {
position: fixed; bottom: 24px; left: 50%; transform: translateX(-50%);
background: var(--bg-2); border: 1px solid var(--glass-border-strong);
border-radius: 10px; padding: 8px 14px;
font-family: ui-monospace, monospace; font-size: 12px; color: var(--fg);
box-shadow: 0 10px 40px rgba(0,0,0,0.5);
opacity: 0; pointer-events: none;
transition: opacity 200ms, transform 200ms;
z-index: 100;
}
.toast.show { opacity: 1; transform: translateX(-50%) translateY(-4px); }
.toast .tag {
display: inline-block; padding: 1px 6px; border-radius: 4px;
font-size: 10px; text-transform: uppercase; letter-spacing: 0.05em;
margin-right: 8px; font-weight: 700;
}
.toast.url .tag { background: rgba(248,113,113,0.2); color: var(--coral); }
.toast.scroll .tag { background: rgba(52,211,153,0.2); color: var(--mint); }
.toast.fetch .tag { background: rgba(251,191,36,0.2); color: var(--amber); }
.remount-flash {
position: absolute; inset: 0; pointer-events: none;
background: rgba(248,113,113,0.08);
opacity: 0; transition: opacity 120ms;
}
.remount-flash.on { opacity: 1; }
</style>
</head>
<body>
<div class="page">
<header>
<div>
<h1>Pagination behavior — functional comparison</h1>
<div class="sub">Click <b>Prev / Next / Jump</b> with each mode active and watch the HUD + event log.</div>
</div>
<div class="modes" role="tablist">
<button id="mode-1" class="active">1 · Always URL</button>
<button id="mode-2">2 · Scroll + prefetch</button>
<button id="mode-3">3 · Two affordances</button>
</div>
</header>
<div class="hud">
<div class="item">
<span class="label">URL</span>
<span class="value" id="hud-url">/?page=1</span>
</div>
<div class="item">
<span class="label">Logical page</span>
<span class="value"><span class="accent" id="hud-page">1</span> / <span id="hud-total">12</span></span>
</div>
<div class="item">
<span class="label">Loaded buffer</span>
<span class="value">page <span id="hud-buf-lo">1</span> <span id="hud-buf-hi">1</span></span>
</div>
<div class="item">
<span class="label">DOM cards</span>
<span class="value" id="hud-card-count">25</span>
</div>
</div>
<div class="description" id="mode-desc"></div>
<div class="log" id="log"></div>
<div class="viewport" id="viewport">
<div class="remount-flash" id="remount-flash"></div>
<div class="viewport-inner" id="viewport-inner"></div>
</div>
<div class="bar">
<button id="prev-btn">← Prev</button>
<span class="label">Page <span class="num" id="bar-page">1</span> of <span id="bar-total">12</span></span>
<button id="next-btn">Next →</button>
<form class="jump" id="jump-form" onsubmit="return false;">
<span style="color: var(--fg-muted); font-size: 11px; text-transform: uppercase; letter-spacing: 0.08em;">Jump</span>
<input id="jump-input" type="text" inputmode="numeric" placeholder="1-12">
<button type="submit"></button>
</form>
</div>
<button class="load-more" id="load-more" style="display: none;">↓ Load more (extend buffer)</button>
</div>
<div class="toast" id="toast"></div>
<script>
// ---------- model ----------
const TOTAL_PAGES = 12;
const PAGE_SIZE = 25; // cards per page (5×5)
const FETCH_LATENCY_MS = 280;
let mode = 1; // 1 | 2 | 3
let currentPage = 1; // logical page
let initialPage = 1; // page that's anchored as the "SSR" first page
let bufLo = 1; // first loaded page
let bufHi = 1; // last loaded page
let inflight = false;
// ---------- helpers ----------
const $ = (id) => document.getElementById(id);
const viewportEl = $("viewport");
const innerEl = $("viewport-inner");
const logEl = $("log");
function fmtTime() {
const d = new Date();
return `${String(d.getHours()).padStart(2,"0")}:${String(d.getMinutes()).padStart(2,"0")}:${String(d.getSeconds()).padStart(2,"0")}.${String(d.getMilliseconds()).padStart(3,"0")}`;
}
function log(kind, msg) {
const line = document.createElement("div");
line.className = "line";
line.innerHTML = `<span class="t">${fmtTime()}</span><span class="k ${kind}">${kind.toUpperCase()}</span>${msg}`;
logEl.appendChild(line);
logEl.scrollTop = logEl.scrollHeight;
while (logEl.children.length > 50) logEl.removeChild(logEl.firstChild);
}
function toast(kind, msg) {
const el = $("toast");
el.className = `toast ${kind}`;
el.innerHTML = `<span class="tag">${kind}</span>${msg}`;
requestAnimationFrame(() => el.classList.add("show"));
clearTimeout(toast._t);
toast._t = setTimeout(() => el.classList.remove("show"), 1400);
}
function buildCard(pageNum, idx) {
const el = document.createElement("div");
el.className = "card";
el.dataset.page = String(pageNum);
el.innerHTML = `<div>p${pageNum}·#${idx+1}</div>`;
return el;
}
function renderBuffer({ animate = false } = {}) {
innerEl.innerHTML = "";
const grid = document.createElement("div");
grid.className = "grid";
for (let p = bufLo; p <= bufHi; p++) {
const marker = document.createElement("div");
marker.className = "page-marker" + (p === currentPage ? " current" : "");
marker.textContent = `Page ${p}` + (p === currentPage ? " · current" : "");
marker.dataset.pageMarker = String(p);
grid.appendChild(marker);
for (let i = 0; i < PAGE_SIZE; i++) {
const c = buildCard(p, i);
if (!animate) c.style.animation = "none";
grid.appendChild(c);
}
}
innerEl.appendChild(grid);
updateHud();
}
function updateHud() {
$("hud-url").textContent = currentPage === 1 ? "/" : `/?page=${currentPage}`;
$("hud-page").textContent = String(currentPage);
$("hud-total").textContent = String(TOTAL_PAGES);
$("hud-buf-lo").textContent = String(bufLo);
$("hud-buf-hi").textContent = String(bufHi);
$("hud-card-count").textContent = String((bufHi - bufLo + 1) * PAGE_SIZE);
$("bar-page").textContent = String(currentPage);
$("bar-total").textContent = String(TOTAL_PAGES);
$("prev-btn").disabled = currentPage <= 1;
$("next-btn").disabled = currentPage >= TOTAL_PAGES;
const lm = $("load-more");
lm.disabled = bufHi >= TOTAL_PAGES;
lm.textContent = bufHi >= TOTAL_PAGES
? "(end of library)"
: `↓ Load page ${bufHi + 1} (extend buffer)`;
}
function flashRemount() {
const f = $("remount-flash");
f.classList.add("on");
setTimeout(() => f.classList.remove("on"), 200);
}
function flashPage(p) {
[...innerEl.querySelectorAll(`.card[data-page="${p}"]`)].forEach((el) => {
el.classList.remove("flash");
void el.offsetWidth;
el.classList.add("flash");
});
}
function scrollToPageMarker(p) {
const m = innerEl.querySelector(`[data-page-marker="${p}"]`);
if (!m) return false;
const target = m.offsetTop - 12;
viewportEl.scrollTo({ top: target, behavior: "smooth" });
return true;
}
// ---------- the three modes ----------
// Mode 1: always URL nav. Buffer is reset to {target}.
function mode1Nav(target) {
log("click", `Prev/Next clicked, target=${target}`);
log("url", `router.push(?page=${target})`);
log("remount", `LibraryGrid remounts (key includes effectivePage)`);
flashRemount();
initialPage = target;
bufLo = target; bufHi = target;
currentPage = target;
renderBuffer({ animate: true });
viewportEl.scrollTop = 0;
toast("url", `Navigated to page ${target} (remount)`);
}
// Mode 2: scroll if in buffer; else fetch+append; backward beyond initial → URL.
async function mode2Nav(target) {
log("click", `Prev/Next clicked, target=${target}`);
if (target < initialPage) {
log("note", `target < initialPage (${target} < ${initialPage}) — fall back to URL`);
log("url", `router.push(?page=${target})`);
log("remount", `LibraryGrid remounts`);
flashRemount();
initialPage = target;
bufLo = target; bufHi = target;
currentPage = target;
renderBuffer({ animate: true });
viewportEl.scrollTop = 0;
toast("url", `Backward across buffer → URL nav (page ${target})`);
return;
}
if (target > bufHi) {
log("note", `target > bufHi (${target} > ${bufHi}) — prefetching`);
if (inflight) return;
inflight = true;
for (let p = bufHi + 1; p <= target; p++) {
log("fetch", `fetch page ${p}`);
await new Promise((r) => setTimeout(r, FETCH_LATENCY_MS));
bufHi = p;
renderBuffer({ animate: false });
log("note", `appended page ${p}, buffer now ${bufLo}${bufHi}`);
}
inflight = false;
}
currentPage = target;
log("scroll", `scrollToIndex(rowOf(page ${target}))`);
renderBuffer({ animate: false });
requestAnimationFrame(() => scrollToPageMarker(target));
flashPage(target);
toast("scroll", `Scrolled to page ${target} (in-buffer)`);
}
// Mode 3: Prev/Next is always URL. Load-more button does the buffer growth.
function mode3Nav(target) {
// Same as mode 1 for Prev/Next.
mode1Nav(target);
}
async function mode3LoadMore() {
if (bufHi >= TOTAL_PAGES) return;
log("click", "Load-more clicked");
log("fetch", `fetch page ${bufHi + 1}`);
inflight = true;
await new Promise((r) => setTimeout(r, FETCH_LATENCY_MS));
bufHi += 1;
inflight = false;
renderBuffer({ animate: false });
log("note", `appended page ${bufHi}, buffer now ${bufLo}${bufHi}`);
toast("fetch", `Buffer extended to page ${bufHi}`);
}
function navTo(target) {
target = Math.max(1, Math.min(TOTAL_PAGES, target));
if (target === currentPage && target >= bufLo && target <= bufHi) return;
if (mode === 1) mode1Nav(target);
else if (mode === 2) mode2Nav(target);
else mode3Nav(target);
}
// ---------- mode switching ----------
const MODE_DESC = {
1: `<b>Mode 1 — Always URL.</b> Every Prev/Next click pushes a new URL. The grid
remounts (red flash). Buffer always reduces to a single page. Predictable but you
lose the smooth in-buffer jump.`,
2: `<b>Mode 2 — Scroll + prefetch.</b> If the target is already in the buffer it
scrolls (green flash). Past <code>bufHi</code> it fetches missing pages then scrolls.
Backward past <code>initialPage</code> still falls back to URL nav (residual seam).
Try Next a few times then Prev.`,
3: `<b>Mode 3 — Two affordances.</b> Prev/Next behaves like Mode 1 (always URL).
A separate <span style="color: var(--mint)">Load more</span> button below extends
the buffer in place. Each affordance has exactly one behavior.`,
};
function setMode(n) {
mode = n;
document.querySelectorAll(".modes button").forEach((b) => b.classList.remove("active"));
$(`mode-${n}`).classList.add("active");
$("mode-desc").innerHTML = MODE_DESC[n];
$("load-more").style.display = n === 3 ? "block" : "none";
// Reset state on mode change so each demo starts clean.
currentPage = 1; initialPage = 1; bufLo = 1; bufHi = 1;
renderBuffer({ animate: true });
viewportEl.scrollTop = 0;
log("note", `Mode switched to ${n}`);
}
// ---------- wire up ----------
$("mode-1").addEventListener("click", () => setMode(1));
$("mode-2").addEventListener("click", () => setMode(2));
$("mode-3").addEventListener("click", () => setMode(3));
$("prev-btn").addEventListener("click", () => navTo(currentPage - 1));
$("next-btn").addEventListener("click", () => navTo(currentPage + 1));
$("jump-form").addEventListener("submit", (e) => {
e.preventDefault();
const v = Number($("jump-input").value);
if (Number.isFinite(v) && v >= 1 && v <= TOTAL_PAGES) navTo(v);
$("jump-input").value = "";
});
$("load-more").addEventListener("click", mode3LoadMore);
setMode(1);
</script>
</body>
</html>