208 lines
9.9 KiB
HTML
208 lines
9.9 KiB
HTML
<!doctype html>
|
||
<html lang="en">
|
||
<head>
|
||
<meta charset="utf-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||
<title>Public history — Palette</title>
|
||
<link rel="stylesheet" href="../themes/tokens.css">
|
||
<style>
|
||
* { box-sizing: border-box; margin: 0; padding: 0; }
|
||
body {
|
||
font-family: var(--font-body);
|
||
background: var(--bg);
|
||
color: var(--fg);
|
||
line-height: 1.45;
|
||
-webkit-font-smoothing: antialiased;
|
||
min-height: 100vh;
|
||
font-size: 14px;
|
||
}
|
||
.topbar {
|
||
display: flex; align-items: center; gap: 20px;
|
||
padding: 0 20px; height: 52px;
|
||
background: var(--surface); border-bottom: 1px solid var(--border);
|
||
}
|
||
.logo { font-weight: 700; font-size: 16px; letter-spacing: -0.02em; text-decoration: none; color: var(--fg); }
|
||
.logo em { font-style: normal; color: var(--muted-fg); font-weight: 400; }
|
||
.topbar nav { display: flex; gap: 4px; }
|
||
.topbar nav a { color: var(--muted-fg); text-decoration: none; padding: 6px 12px; border-radius: 8px; font-size: 13.5px; }
|
||
.topbar nav a:hover { background: var(--surface-2); color: var(--fg); }
|
||
.topbar nav a.on { background: var(--accent); color: var(--bg); }
|
||
.topbar .spacer { flex: 1; }
|
||
|
||
.deck { max-width: 860px; margin: 0 auto; padding: 20px; display: flex; flex-direction: column; gap: 16px; }
|
||
.head-row { display: flex; align-items: baseline; gap: 14px; }
|
||
h1 { font-size: 20px; font-weight: 600; letter-spacing: -0.01em; }
|
||
.count { font-size: 12.5px; color: var(--muted-fg); }
|
||
.head-row .spacer { flex: 1; }
|
||
.search {
|
||
display: flex; align-items: center; gap: 8px; background: var(--surface);
|
||
border: 1px solid var(--border); border-radius: 10px; padding: 8px 14px; width: 260px;
|
||
}
|
||
.search input { border: none; outline: none; background: transparent; color: var(--fg); font: inherit; font-size: 13px; width: 100%; }
|
||
.search input::placeholder { color: var(--muted); }
|
||
|
||
.float {
|
||
background: var(--surface); border: 1px solid var(--border); border-radius: 14px;
|
||
box-shadow: 0 2px 6px rgba(20,14,32,.25), 0 12px 32px rgba(20,14,32,.3);
|
||
overflow: hidden;
|
||
}
|
||
table { width: 100%; border-collapse: collapse; font-size: 13px; table-layout: fixed; }
|
||
th:nth-child(1), td:nth-child(1) { width: 130px; }
|
||
th:nth-child(2), td:nth-child(2) { width: auto; }
|
||
th:nth-child(3), td:nth-child(3) { width: 80px; }
|
||
th:nth-child(4), td:nth-child(4) { width: 80px; }
|
||
th:nth-child(5), td:nth-child(5) { width: 64px; }
|
||
th:nth-child(6), td:nth-child(6) { width: 90px; }
|
||
th {
|
||
text-align: left; font-size: 11px; text-transform: uppercase; letter-spacing: .08em;
|
||
color: var(--muted-fg); padding: 10px 16px; border-bottom: 1px solid var(--border); font-weight: 600;
|
||
}
|
||
td { padding: 10px 16px; border-bottom: 1px solid var(--border); }
|
||
tr:last-child td { border-bottom: none; }
|
||
tr.row:hover td { background: var(--surface-2); }
|
||
td a.slug { font-family: var(--font-mono); font-size: 12.5px; color: var(--fg); text-decoration: none; }
|
||
td a.slug:hover { color: var(--accent); }
|
||
.title-cell { color: var(--muted-fg); max-width: 240px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||
.badge { font-size: 11px; border: 1px solid var(--border); color: var(--muted-fg); border-radius: 999px; padding: 1px 8px; margin-right: 4px; white-space: nowrap; }
|
||
.badge.lock { color: var(--accent); border-color: var(--accent); }
|
||
.dim { color: var(--muted-fg); white-space: nowrap; }
|
||
.pager { display: flex; align-items: center; justify-content: space-between; padding: 12px 16px; font-size: 12.5px; color: var(--muted-fg); }
|
||
.pager .pg { display: flex; gap: 6px; }
|
||
.pager button { border: 1px solid var(--border); background: var(--surface-2); color: var(--muted-fg); border-radius: 7px; padding: 4px 11px; font: inherit; font-size: 12.5px; cursor: pointer; }
|
||
.pager button:hover:not(:disabled) { color: var(--fg); border-color: var(--muted); }
|
||
.pager button.on { background: var(--accent); color: var(--bg); border-color: var(--accent); }
|
||
.pager button:disabled { opacity: .4; cursor: default; }
|
||
.empty { text-align: center; padding: 40px 16px; color: var(--muted-fg); font-size: 13px; }
|
||
</style>
|
||
</head>
|
||
<body data-preset="midnight">
|
||
<div class="topbar">
|
||
<a class="logo" href="/history/">Palette <em>/ v0</em></a>
|
||
<nav><a href="/new/">new</a><a class="on" href="/history/">history</a><a href="https://git.archfox.org/poslop/palette">git</a></nav>
|
||
<div class="spacer"></div>
|
||
</div>
|
||
<div class="deck">
|
||
<div class="head-row">
|
||
<h1>Public pastes</h1>
|
||
<span class="count" id="count"></span>
|
||
<div class="spacer"></div>
|
||
<div class="search"><input id="filter" placeholder="filter by language or slug…"></div>
|
||
</div>
|
||
<div class="float">
|
||
<table>
|
||
<thead><tr><th>Paste</th><th>Description</th><th>Language</th><th>Size</th><th>Views</th><th>Created</th></tr></thead>
|
||
<tbody id="rows"></tbody>
|
||
</table>
|
||
<div class="empty" id="empty" style="display:none">No pastes match that filter.</div>
|
||
<div class="pager">
|
||
<span id="showing"></span>
|
||
<div class="pg" id="pg"></div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
<script>
|
||
// Demo dataset: 7 real entries + generated filler to simulate 2,431 total
|
||
const real = [
|
||
{slug:"k3sYt2", href:"/paste/index.html", desc:"traefik redirect middleware snippet", lang:"go", size:"412 B", views:"12", created:"2m ago", lock:false},
|
||
{slug:"b7QwRn", href:"/paste/index.html", desc:"nginx notes for staging", lang:"txt", size:"1.2 KB", views:"3", created:"14m ago",lock:false},
|
||
{slug:"xT8vZc", href:"/paste/index.html", desc:"csv parse helper", lang:"py", size:"890 B", views:"41", created:"3h ago", lock:false},
|
||
{slug:"9mQkLp", href:"/unlock/index.html", desc:"uptime kuma compose", lang:"yaml", size:"2.4 KB", views:"—", created:"5h ago", lock:true},
|
||
{slug:"can/9fL2m", href:"/can-demo/index.html",desc:"incident screenshots + logs (4 items)",lang:"can", size:"3.8 MB", views:"7", created:"1h ago", lock:false},
|
||
{slug:"vR4nWq", href:"/paste/index.html", desc:"sql index check", lang:"sql", size:"220 B", views:"2", created:"6h ago", lock:false},
|
||
{slug:"pL2xCa", href:"/unlock/index.html", desc:"todo: rewrite auth flow", lang:"md", size:"560 B", views:"—", created:"1d ago", lock:true},
|
||
];
|
||
const langs = ["go","py","txt","js","sh","yaml","md","sql","rs","c"];
|
||
const TOTAL = 2431, PER_PAGE = 7;
|
||
const PAGES = Math.ceil(TOTAL / PER_PAGE); // 348
|
||
// deterministic pseudo-random filler so pages look stable between renders
|
||
function filler(i) {
|
||
const lang = langs[(i * 7 + 3) % langs.length];
|
||
const locked = i % 11 === 0;
|
||
const slug = locked ? String(100000 + (i * 3719) % 899999).slice(0,6) : genSlugDemo(i);
|
||
return {
|
||
slug,
|
||
href: locked ? "/unlock/index.html" : "/paste/index.html",
|
||
desc: ["scratch notes","config diff","error log","snippet dump","test output","benchmark results","query plan","draft changelog"][i % 8] + " #" + (i+1),
|
||
lang, locked,
|
||
size: (0.2 + (i % 40) / 10).toFixed(1) + " KB",
|
||
views: locked ? "—" : String((i * 13) % 97),
|
||
created: (i % 24) + "h ago",
|
||
};
|
||
}
|
||
function genSlugDemo(i) {
|
||
const a = "23456789abcdefghjkmnpqrstuvwxyz";
|
||
let s = "";
|
||
let x = i * 2654435761 % 4294967296;
|
||
for (let j = 0; j < 6; j++) { s += a[x % a.length]; x = Math.floor(x / a.length) + 7; }
|
||
return s;
|
||
}
|
||
const data = [...real];
|
||
for (let i = real.length; i < TOTAL; i++) data.push(filler(i));
|
||
|
||
let page = 1;
|
||
const rowsEl = document.getElementById('rows');
|
||
const pgEl = document.getElementById('pg');
|
||
const showingEl = document.getElementById('showing');
|
||
const countEl = document.getElementById('count');
|
||
const filterEl = document.getElementById('filter');
|
||
|
||
function filtered() {
|
||
const q = filterEl.value.trim().toLowerCase();
|
||
if (!q) return data;
|
||
return data.filter(d => d.slug.toLowerCase().includes(q) || d.lang.includes(q) || d.desc.toLowerCase().includes(q));
|
||
}
|
||
|
||
function render() {
|
||
const all = filtered();
|
||
const totalPages = Math.max(1, Math.ceil(all.length / PER_PAGE));
|
||
if (page > totalPages) page = totalPages;
|
||
const start = (page - 1) * PER_PAGE;
|
||
const slice = all.slice(start, start + PER_PAGE);
|
||
|
||
if (slice.length === 0) {
|
||
rowsEl.innerHTML = '';
|
||
document.getElementById('empty').style.display = 'block';
|
||
} else {
|
||
document.getElementById('empty').style.display = 'none';
|
||
rowsEl.innerHTML = slice.map(d =>
|
||
`<tr class="row"><td><a class="slug" href="${d.href}">${d.slug}</a>${d.lock ? '<span class="badge lock">🔒</span>' : ''}</td>` +
|
||
`<td class="title-cell">${d.desc}</td>` +
|
||
`<td><span class="badge">${d.lang}</span></td>` +
|
||
`<td class="dim">${d.size}</td><td class="dim">${d.views}</td><td class="dim">${d.created}</td></tr>`
|
||
).join('');
|
||
}
|
||
|
||
countEl.textContent = all.length.toLocaleString() + " total";
|
||
const lo = all.length === 0 ? 0 : start + 1;
|
||
const hi = Math.min(start + PER_PAGE, all.length);
|
||
showingEl.textContent = `Showing ${lo}–${hi} of ${all.length.toLocaleString()} · page ${page} of ${totalPages}`;
|
||
|
||
// pager: 1, prev-1, [cur], next+1, last, plus ellipses
|
||
const btns = [];
|
||
const add = (label, target, opts={}) => btns.push(`<button ${opts.on?'class="on"':''} ${opts.dis?'disabled':''} data-p="${target}">${label}</button>`);
|
||
add('‹', page-1, {dis: page===1});
|
||
const window = new Set([1, 2, page-1, page, page+1, totalPages]);
|
||
let last = 0;
|
||
for (let i = 1; i <= totalPages; i++) {
|
||
if (window.has(i)) {
|
||
if (last && i - last > 1) btns.push('<span class="dim">…</span>');
|
||
add(String(i), i, {on: i===page});
|
||
last = i;
|
||
}
|
||
}
|
||
add('›', page+1, {dis: page===totalPages});
|
||
pgEl.innerHTML = btns.join('');
|
||
}
|
||
|
||
pgEl.addEventListener('click', e => {
|
||
const b = e.target.closest('button[data-p]');
|
||
if (!b || b.disabled) return;
|
||
page = parseInt(b.dataset.p);
|
||
render();
|
||
});
|
||
filterEl.addEventListener('input', () => { page = 1; render(); });
|
||
render();
|
||
</script>
|
||
</body>
|
||
</html>
|