Wiki — KB Dashboard (top articles · stale flags)
A polished knowledge-base admin dashboard with animated count-up KPI cards for total articles, monthly views, average helpfulness, and stale flags, each with trend deltas. An interactive CSS and SVG chart plots views and searches over time with a 7d, 30d, and 90d range toggle. Below sit a sortable top-articles table, a needs-attention panel that flags stale, low-rated, and broken-link pages with resolve or dismiss actions, and a live recent-edits activity feed. Built with clean white docs styling, semantic landmarks, and a responsive drawer sidebar.
MCP
Code
:root {
--bg: #ffffff;
--bg-2: #f7f8fa;
--panel: #ffffff;
--ink: #1a1a1f;
--ink-2: #3a3a42;
--muted: #6b7280;
--line: rgba(20, 20, 30, 0.10);
--line-2: rgba(20, 20, 30, 0.18);
--link: #2563eb;
--link-hover: #1d4ed8;
--accent: #2563eb;
--note: #2563eb;
--tip: #16a34a;
--warn: #d97706;
--danger: #dc2626;
--code-bg: #f4f4f6;
--kbd-bg: #eceef2;
--r-sm: 6px;
--r-md: 10px;
--r-lg: 14px;
--shadow-sm: 0 1px 2px rgba(20, 20, 30, 0.05);
--shadow-md: 0 4px 16px rgba(20, 20, 30, 0.07);
--font-ui: "Inter", system-ui, -apple-system, "Segoe UI", sans-serif;
--font-serif: "Source Serif 4", Georgia, serif;
--font-mono: "JetBrains Mono", ui-monospace, "SF Mono", monospace;
--sidebar-w: 248px;
}
* { box-sizing: border-box; }
html { scroll-behavior: smooth; }
body {
margin: 0;
font-family: var(--font-ui);
color: var(--ink);
background: var(--bg-2);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
font-size: 14px;
line-height: 1.5;
}
a { color: var(--link); text-decoration: none; }
a:hover { color: var(--link-hover); }
.skip-link {
position: absolute;
left: -999px;
top: 8px;
z-index: 100;
background: var(--ink);
color: #fff;
padding: 8px 14px;
border-radius: var(--r-sm);
}
.skip-link:focus { left: 8px; }
:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
border-radius: var(--r-sm);
}
kbd {
font-family: var(--font-mono);
font-size: 11px;
background: var(--kbd-bg);
border: 1px solid var(--line-2);
border-bottom-width: 2px;
border-radius: 5px;
padding: 1px 6px;
color: var(--ink-2);
}
.shell { display: flex; min-height: 100vh; }
/* ---------- Sidebar ---------- */
.sidebar {
width: var(--sidebar-w);
flex-shrink: 0;
background: var(--panel);
border-right: 1px solid var(--line);
display: flex;
flex-direction: column;
position: sticky;
top: 0;
height: 100vh;
overflow-y: auto;
padding: 16px 12px;
}
.brand {
display: flex;
align-items: center;
gap: 9px;
padding: 6px 8px 16px;
}
.brand-mark {
width: 28px; height: 28px;
display: grid; place-items: center;
background: var(--accent);
color: #fff;
border-radius: 8px;
font-size: 16px;
}
.brand-name { font-weight: 800; font-size: 15px; letter-spacing: -0.01em; }
.brand-tag {
font-size: 10px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.06em;
color: var(--muted);
background: var(--bg-2);
padding: 2px 6px;
border-radius: 999px;
margin-left: auto;
}
.nav-group { margin-bottom: 14px; }
.nav-label {
font-size: 10.5px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.07em;
color: var(--muted);
margin: 8px 8px 6px;
}
.nav-item {
display: flex;
align-items: center;
gap: 10px;
padding: 7px 10px;
border-radius: var(--r-sm);
color: var(--ink-2);
font-weight: 500;
font-size: 13px;
transition: background 0.12s, color 0.12s;
}
.nav-item:hover { background: var(--bg-2); color: var(--ink); }
.nav-item.is-active {
background: color-mix(in srgb, var(--accent) 10%, transparent);
color: var(--link-hover);
font-weight: 600;
}
.ni-icon { width: 16px; text-align: center; opacity: 0.8; font-size: 13px; }
.nav-badge {
margin-left: auto;
background: var(--warn);
color: #fff;
font-size: 10.5px;
font-weight: 700;
border-radius: 999px;
padding: 1px 7px;
}
.nav-count {
margin-left: auto;
color: var(--muted);
font-size: 11.5px;
font-variant-numeric: tabular-nums;
}
.nav-foot { margin-top: auto; padding-top: 12px; border-top: 1px solid var(--line); }
.nav-user { display: flex; align-items: center; gap: 9px; padding: 6px 8px; }
.avatar {
width: 30px; height: 30px;
border-radius: 50%;
background: linear-gradient(135deg, #2563eb, #7c3aed);
color: #fff;
display: grid; place-items: center;
font-size: 11px; font-weight: 700;
}
.nav-user-meta { display: flex; flex-direction: column; line-height: 1.25; }
.nu-name { font-weight: 600; font-size: 12.5px; }
.nu-role { font-size: 11px; color: var(--muted); }
.sidebar-scrim {
display: none;
position: fixed; inset: 0;
background: rgba(10, 10, 20, 0.4);
z-index: 40;
}
/* ---------- Main ---------- */
.main { flex: 1; min-width: 0; display: flex; flex-direction: column; }
.topbar {
display: flex;
align-items: center;
gap: 14px;
padding: 10px 22px;
background: rgba(255, 255, 255, 0.86);
backdrop-filter: blur(8px);
border-bottom: 1px solid var(--line);
position: sticky;
top: 0;
z-index: 30;
}
.icon-btn {
border: 1px solid var(--line);
background: var(--panel);
border-radius: var(--r-sm);
width: 34px; height: 34px;
font-size: 16px;
cursor: pointer;
color: var(--ink-2);
}
.icon-btn:hover { background: var(--bg-2); }
.menu-btn { display: none; }
.crumbs { display: flex; align-items: center; gap: 8px; color: var(--muted); font-size: 13px; font-weight: 500; }
.crumb-sep { opacity: 0.5; }
.crumb-current { color: var(--ink); font-weight: 600; }
.topbar-search {
margin-left: auto;
display: flex;
align-items: center;
gap: 8px;
background: var(--bg-2);
border: 1px solid var(--line);
border-radius: var(--r-md);
padding: 0 10px;
width: 280px;
max-width: 32vw;
transition: border-color 0.12s, box-shadow 0.12s;
}
.topbar-search:focus-within {
border-color: var(--accent);
box-shadow: 0 0 0 3px color-mix(in srgb, var(--accent) 16%, transparent);
}
.ts-icon { color: var(--muted); font-size: 14px; }
.topbar-search input {
border: 0; background: none; outline: none;
font: inherit; color: var(--ink);
padding: 8px 0; flex: 1; min-width: 0;
}
.topbar-actions { display: flex; gap: 8px; }
.btn {
font: inherit;
font-weight: 600;
font-size: 13px;
border-radius: var(--r-sm);
padding: 8px 13px;
cursor: pointer;
border: 1px solid var(--line-2);
background: var(--panel);
color: var(--ink-2);
transition: background 0.12s, border-color 0.12s, transform 0.06s;
white-space: nowrap;
}
.btn:hover { background: var(--bg-2); }
.btn:active { transform: translateY(1px); }
.btn-ghost { border-color: var(--line); }
.btn-primary { background: var(--accent); border-color: var(--accent); color: #fff; }
.btn-primary:hover { background: var(--link-hover); }
.content { padding: 26px 28px 60px; max-width: 1240px; width: 100%; margin: 0 auto; }
.page-head {
display: flex;
align-items: flex-end;
justify-content: space-between;
gap: 16px;
margin-bottom: 22px;
flex-wrap: wrap;
}
.page-head h1 { margin: 0 0 4px; font-size: 23px; font-weight: 800; letter-spacing: -0.02em; }
.page-sub { margin: 0; color: var(--muted); font-size: 13px; }
.page-sub time { color: var(--ink-2); font-weight: 500; }
/* segmented control */
.seg {
display: inline-flex;
background: var(--bg-2);
border: 1px solid var(--line);
border-radius: var(--r-md);
padding: 3px;
}
.seg-btn {
font: inherit;
font-weight: 600;
font-size: 12.5px;
color: var(--muted);
background: none;
border: 0;
border-radius: var(--r-sm);
padding: 6px 14px;
cursor: pointer;
transition: background 0.14s, color 0.14s, box-shadow 0.14s;
}
.seg-btn:hover { color: var(--ink); }
.seg-btn.is-active {
background: var(--panel);
color: var(--ink);
box-shadow: var(--shadow-sm);
}
/* ---------- KPI cards ---------- */
.kpis {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 14px;
margin-bottom: 18px;
}
.kpi {
background: var(--panel);
border: 1px solid var(--line);
border-radius: var(--r-lg);
padding: 16px 16px 14px;
box-shadow: var(--shadow-sm);
}
.kpi-head { display: flex; align-items: center; justify-content: space-between; }
.kpi-label { font-size: 12.5px; color: var(--muted); font-weight: 600; }
.kpi-dot {
width: 26px; height: 26px;
display: grid; place-items: center;
background: var(--bg-2);
border-radius: 8px;
font-size: 13px;
color: var(--accent);
}
.kpi-value {
font-size: 28px;
font-weight: 800;
letter-spacing: -0.02em;
margin: 10px 0 6px;
font-variant-numeric: tabular-nums;
}
.kpi-delta { margin: 0; font-size: 12px; font-weight: 700; display: flex; gap: 6px; align-items: baseline; }
.kpi-delta span { font-weight: 500; color: var(--muted); }
.kpi-delta.is-up { color: var(--tip); }
.kpi-delta.is-down { color: var(--danger); }
.kpi-warn { border-color: color-mix(in srgb, var(--warn) 35%, var(--line)); }
.kpi-warn .kpi-dot { color: var(--warn); background: color-mix(in srgb, var(--warn) 12%, var(--bg-2)); }
.kpi-warn .kpi-delta.is-down { color: var(--tip); }
/* ---------- Panels ---------- */
.panel {
background: var(--panel);
border: 1px solid var(--line);
border-radius: var(--r-lg);
box-shadow: var(--shadow-sm);
margin-bottom: 18px;
overflow: hidden;
}
.panel-head {
display: flex;
align-items: flex-start;
justify-content: space-between;
gap: 12px;
padding: 16px 18px 12px;
}
.panel-title { margin: 0 0 2px; font-size: 15px; font-weight: 700; letter-spacing: -0.01em; }
.panel-sub { margin: 0; color: var(--muted); font-size: 12.5px; }
.panel-link { font-size: 12.5px; font-weight: 600; }
/* ---------- Chart ---------- */
.legend { display: flex; gap: 14px; font-size: 12px; color: var(--ink-2); font-weight: 600; }
.leg { display: inline-flex; align-items: center; gap: 6px; }
.leg i { width: 14px; height: 3px; border-radius: 2px; display: inline-block; }
.leg-views i { background: var(--accent); }
.leg-search i { background: var(--warn); }
.chart-wrap { padding: 4px 18px 16px; position: relative; }
.chart { width: 100%; height: 240px; display: block; overflow: visible; }
.grid line { stroke: var(--line); stroke-width: 1; }
.grid text { fill: var(--muted); font-size: 10px; font-family: var(--font-mono); }
.area-views { fill: url(#viewsGrad); opacity: 0.16; }
.line { fill: none; stroke-width: 2.5; stroke-linejoin: round; stroke-linecap: round; }
.line-views { stroke: var(--accent); }
.line-search { stroke: var(--warn); stroke-dasharray: 0; }
.dot {
cursor: pointer;
transition: r 0.1s;
}
.dot-views { fill: var(--accent); }
.dot-search { fill: var(--warn); }
.dot:hover { r: 5.5; }
.chart-x {
display: flex;
justify-content: space-between;
font-family: var(--font-mono);
font-size: 10.5px;
color: var(--muted);
padding: 4px 2px 0;
}
.chart-tooltip {
position: absolute;
z-index: 5;
background: var(--ink);
color: #fff;
font-size: 11.5px;
border-radius: var(--r-sm);
padding: 7px 10px;
pointer-events: none;
transform: translate(-50%, -118%);
white-space: nowrap;
box-shadow: var(--shadow-md);
line-height: 1.5;
}
.chart-tooltip b { font-weight: 700; }
.chart-tooltip .tt-date { color: #c5cad6; display: block; margin-bottom: 2px; font-size: 10.5px; }
.chart-tooltip .tt-views { color: #93b4ff; }
.chart-tooltip .tt-search { color: #fbbf67; }
/* ---------- Grid 2 ---------- */
.grid-2 { display: grid; grid-template-columns: 1.3fr 1fr; gap: 18px; }
.grid-2 .panel { margin-bottom: 0; }
/* ---------- Tables ---------- */
.table-scroll { overflow-x: auto; }
.data-table { width: 100%; border-collapse: collapse; font-size: 13px; }
.data-table th, .data-table td {
padding: 10px 14px;
text-align: left;
border-top: 1px solid var(--line);
}
.data-table thead th {
border-top: 0;
border-bottom: 1px solid var(--line);
background: var(--bg-2);
font-weight: 600;
font-size: 11.5px;
text-transform: uppercase;
letter-spacing: 0.04em;
color: var(--muted);
position: sticky;
top: 0;
}
.data-table th.sortable { padding: 0; }
.data-table th.sortable button {
font: inherit;
text-transform: inherit;
letter-spacing: inherit;
color: inherit;
background: none;
border: 0;
cursor: pointer;
padding: 10px 14px;
width: 100%;
text-align: left;
display: flex;
align-items: center;
gap: 5px;
}
.data-table th.sortable button:hover { color: var(--ink); }
.data-table th.sortable button::after { content: "↕"; opacity: 0.35; font-size: 11px; }
.data-table th.is-sorted-asc button::after { content: "↑"; opacity: 1; color: var(--accent); }
.data-table th.is-sorted-desc button::after { content: "↓"; opacity: 1; color: var(--accent); }
.data-table tbody tr { transition: background 0.1s; }
.data-table tbody tr:hover { background: var(--bg-2); }
.art-title { font-weight: 600; color: var(--ink); }
.art-space { font-size: 11px; color: var(--muted); display: block; margin-top: 1px; }
.num { font-variant-numeric: tabular-nums; text-align: right; }
.data-table th[data-type="num"] button, .data-table th[data-type="date"] button { justify-content: flex-end; }
.help-bar {
display: inline-flex;
align-items: center;
gap: 7px;
font-variant-numeric: tabular-nums;
}
.help-track {
width: 46px; height: 6px;
background: var(--bg-2);
border-radius: 999px;
overflow: hidden;
}
.help-fill { height: 100%; border-radius: 999px; }
.help-good { background: var(--tip); }
.help-mid { background: var(--warn); }
.help-low { background: var(--danger); }
.cell-stale { color: var(--warn); font-weight: 600; }
/* ---------- Attention ---------- */
.attention-list { list-style: none; margin: 0; padding: 6px 12px 14px; }
.attention-item {
display: flex;
gap: 11px;
padding: 11px;
border: 1px solid var(--line);
border-radius: var(--r-md);
margin-bottom: 8px;
transition: opacity 0.25s, transform 0.25s, border-color 0.12s;
background: var(--panel);
}
.attention-item:hover { border-color: var(--line-2); }
.attention-item.is-removing { opacity: 0; transform: translateX(14px); }
.att-icon {
width: 30px; height: 30px;
flex-shrink: 0;
border-radius: 8px;
display: grid; place-items: center;
font-size: 15px;
}
.att-stale { background: color-mix(in srgb, var(--warn) 14%, #fff); color: var(--warn); }
.att-rating { background: color-mix(in srgb, var(--danger) 12%, #fff); color: var(--danger); }
.att-link { background: color-mix(in srgb, var(--note) 12%, #fff); color: var(--note); }
.att-body { flex: 1; min-width: 0; }
.att-top { display: flex; align-items: center; gap: 8px; flex-wrap: wrap; }
.att-name { font-weight: 600; font-size: 13px; }
.att-tag {
font-size: 10px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.04em;
padding: 1px 6px;
border-radius: 999px;
}
.tag-stale { background: color-mix(in srgb, var(--warn) 15%, #fff); color: var(--warn); }
.tag-rating { background: color-mix(in srgb, var(--danger) 12%, #fff); color: var(--danger); }
.tag-link { background: color-mix(in srgb, var(--note) 12%, #fff); color: var(--note); }
.att-detail { margin: 3px 0 0; font-size: 12px; color: var(--muted); }
.att-actions { display: flex; gap: 6px; align-items: flex-start; }
.att-btn {
font: inherit;
font-size: 11.5px;
font-weight: 600;
border: 1px solid var(--line-2);
background: var(--panel);
color: var(--ink-2);
border-radius: var(--r-sm);
padding: 4px 9px;
cursor: pointer;
}
.att-btn:hover { background: var(--bg-2); }
.att-btn.is-resolve { border-color: var(--tip); color: var(--tip); }
.att-btn.is-resolve:hover { background: color-mix(in srgb, var(--tip) 10%, #fff); }
.attention-empty {
padding: 26px 16px;
text-align: center;
color: var(--tip);
font-weight: 600;
font-size: 13px;
}
/* ---------- Activity feed ---------- */
.feed { list-style: none; margin: 0; padding: 4px 18px 16px; }
.feed-item {
display: flex;
gap: 12px;
padding: 11px 0;
border-top: 1px solid var(--line);
}
.feed-item:first-child { border-top: 0; }
.feed-avatar {
width: 28px; height: 28px;
flex-shrink: 0;
border-radius: 50%;
display: grid; place-items: center;
font-size: 10.5px; font-weight: 700;
color: #fff;
}
.feed-body { flex: 1; min-width: 0; }
.feed-text { margin: 0; font-size: 13px; color: var(--ink-2); }
.feed-text b { color: var(--ink); font-weight: 600; }
.feed-text a { font-weight: 600; }
.feed-meta { margin: 2px 0 0; font-size: 11.5px; color: var(--muted); display: flex; gap: 8px; align-items: center; }
.feed-action {
font-size: 10px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: 0.04em;
padding: 1px 6px;
border-radius: 999px;
background: var(--bg-2);
color: var(--ink-2);
}
.fa-edit { background: color-mix(in srgb, var(--note) 12%, #fff); color: var(--note); }
.fa-publish { background: color-mix(in srgb, var(--tip) 12%, #fff); color: var(--tip); }
.fa-archive { background: color-mix(in srgb, var(--muted) 16%, #fff); color: var(--muted); }
.fa-revert { background: color-mix(in srgb, var(--warn) 14%, #fff); color: var(--warn); }
/* ---------- Toast ---------- */
.toast-host {
position: fixed;
bottom: 22px;
right: 22px;
z-index: 80;
display: flex;
flex-direction: column;
gap: 10px;
}
.toast {
background: var(--ink);
color: #fff;
padding: 11px 15px;
border-radius: var(--r-md);
font-size: 13px;
font-weight: 500;
box-shadow: var(--shadow-md);
display: flex;
align-items: center;
gap: 9px;
animation: toast-in 0.25s cubic-bezier(0.2, 0.9, 0.3, 1);
max-width: 320px;
}
.toast.is-out { animation: toast-out 0.25s forwards; }
.toast-ico { color: var(--tip); font-weight: 700; }
@keyframes toast-in { from { opacity: 0; transform: translateY(12px); } to { opacity: 1; transform: none; } }
@keyframes toast-out { to { opacity: 0; transform: translateY(12px); } }
/* ---------- Responsive ---------- */
@media (max-width: 1080px) {
.kpis { grid-template-columns: repeat(2, 1fr); }
.grid-2 { grid-template-columns: 1fr; }
.grid-2 .panel { margin-bottom: 0; }
}
@media (max-width: 820px) {
.sidebar {
position: fixed;
left: 0; top: 0;
z-index: 50;
transform: translateX(-100%);
transition: transform 0.22s ease;
box-shadow: var(--shadow-md);
}
.sidebar.is-open { transform: none; }
.sidebar-scrim.is-open { display: block; }
.menu-btn { display: grid; place-items: center; }
.content { padding: 20px 18px 50px; }
.topbar-search { max-width: none; width: auto; flex: 1; }
}
@media (max-width: 520px) {
body { font-size: 13.5px; }
.kpis { grid-template-columns: 1fr 1fr; gap: 10px; }
.kpi-value { font-size: 23px; }
.topbar { padding: 9px 14px; gap: 10px; }
.crumbs { display: none; }
.topbar-actions .btn-ghost { display: none; }
.btn-primary { padding: 8px 11px; }
.page-head h1 { font-size: 20px; }
.content { padding: 16px 12px 46px; }
.att-actions { flex-direction: column; }
.chart { height: 200px; }
}
@media (max-width: 380px) {
.kpis { grid-template-columns: 1fr; }
}
@media (prefers-reduced-motion: reduce) {
* { animation-duration: 0.001ms !important; transition-duration: 0.001ms !important; }
html { scroll-behavior: auto; }
}(function () {
"use strict";
/* ------------------------------------------------------------------ */
/* Helpers */
/* ------------------------------------------------------------------ */
const $ = (sel, root = document) => root.querySelector(sel);
const $$ = (sel, root = document) => Array.from(root.querySelectorAll(sel));
const SVGNS = "http://www.w3.org/2000/svg";
function el(tag, attrs) {
const node = document.createElementNS(SVGNS, tag);
for (const k in attrs) node.setAttribute(k, attrs[k]);
return node;
}
function fmtCompact(n) {
if (n >= 1000) return (n / 1000).toFixed(n >= 10000 ? 1 : 1).replace(/\.0$/, "") + "k";
return String(Math.round(n));
}
function fmtInt(n) {
return Math.round(n).toLocaleString("en-US");
}
/* ------------------------------------------------------------------ */
/* Toast */
/* ------------------------------------------------------------------ */
const toastHost = $("#toastHost");
function toast(msg, icon = "✓") {
const t = document.createElement("div");
t.className = "toast";
t.innerHTML = '<span class="toast-ico"></span><span class="toast-msg"></span>';
t.querySelector(".toast-ico").textContent = icon;
t.querySelector(".toast-msg").textContent = msg;
toastHost.appendChild(t);
setTimeout(() => {
t.classList.add("is-out");
t.addEventListener("animationend", () => t.remove(), { once: true });
}, 2600);
}
/* ------------------------------------------------------------------ */
/* Sidebar drawer */
/* ------------------------------------------------------------------ */
const sidebar = $("#sidebar");
const scrim = $("#scrim");
const menuBtn = $("#menuBtn");
function openNav() {
sidebar.classList.add("is-open");
scrim.classList.add("is-open");
scrim.hidden = false;
menuBtn.setAttribute("aria-expanded", "true");
}
function closeNav() {
sidebar.classList.remove("is-open");
scrim.classList.remove("is-open");
scrim.hidden = true;
menuBtn.setAttribute("aria-expanded", "false");
}
menuBtn.addEventListener("click", () =>
sidebar.classList.contains("is-open") ? closeNav() : openNav()
);
scrim.addEventListener("click", closeNav);
$$(".sidebar .nav-item").forEach((a) =>
a.addEventListener("click", () => {
if (window.innerWidth <= 820) closeNav();
})
);
/* ------------------------------------------------------------------ */
/* Count-up KPIs */
/* ------------------------------------------------------------------ */
function countUp(node) {
const target = parseFloat(node.dataset.count);
const decimals = parseInt(node.dataset.decimals || "0", 10);
const suffix = node.dataset.suffix || "";
const compact = node.dataset.format === "compact";
const dur = 1100;
const start = performance.now();
function frame(now) {
const p = Math.min(1, (now - start) / dur);
const eased = 1 - Math.pow(1 - p, 3);
const val = target * eased;
let text;
if (compact) text = fmtCompact(val);
else if (decimals > 0) text = val.toFixed(decimals);
else text = fmtInt(val);
node.textContent = text + suffix;
if (p < 1) requestAnimationFrame(frame);
}
requestAnimationFrame(frame);
}
const kpiNodes = $$(".kpi-value");
if ("IntersectionObserver" in window) {
const io = new IntersectionObserver(
(entries, obs) => {
entries.forEach((e) => {
if (e.isIntersecting) {
countUp(e.target);
obs.unobserve(e.target);
}
});
},
{ threshold: 0.4 }
);
kpiNodes.forEach((n) => io.observe(n));
} else {
kpiNodes.forEach(countUp);
}
/* ------------------------------------------------------------------ */
/* Chart data + rendering */
/* ------------------------------------------------------------------ */
// Deterministic pseudo-random series so demo looks stable per range.
function series(range) {
const pts = range === 7 ? 7 : range === 30 ? 15 : 18;
const out = [];
let seed = range * 9301 + 49297;
const rnd = () => {
seed = (seed * 9301 + 49297) % 233280;
return seed / 233280;
};
const baseViews = range === 7 ? 1500 : range === 30 ? 1450 : 1300;
const now = Date.now();
const stepDays = range / (pts - 1);
for (let i = 0; i < pts; i++) {
const wave = Math.sin(i / 2.1) * 240 + Math.sin(i / 5) * 160;
const views = Math.max(420, Math.round(baseViews + wave + (rnd() - 0.4) * 420 + i * 6));
const searches = Math.max(120, Math.round(views * (0.32 + rnd() * 0.08)));
const d = new Date(now - (range - i * stepDays) * 86400000);
out.push({
date: d,
label: d.toLocaleDateString("en-US", { month: "short", day: "numeric" }),
views,
searches,
});
}
return out;
}
const chart = $("#chart");
const gridG = $("#gridLines");
const areaViews = $("#areaViews");
const lineViews = $("#lineViews");
const lineSearch = $("#lineSearch");
const pointsG = $("#points");
const chartX = $("#chartX");
const tip = $("#chartTip");
const chartWrap = $(".chart-wrap");
const W = 800, H = 260, padL = 8, padR = 8, padT = 14, padB = 18;
// gradient def for area fill
(function injectGrad() {
const defs = el("defs", {});
const grad = el("linearGradient", { id: "viewsGrad", x1: "0", y1: "0", x2: "0", y2: "1" });
grad.appendChild(el("stop", { offset: "0", "stop-color": "#2563eb" }));
grad.appendChild(el("stop", { offset: "1", "stop-color": "#2563eb", "stop-opacity": "0" }));
defs.appendChild(grad);
chart.insertBefore(defs, chart.firstChild);
})();
function renderChart(range) {
const data = series(range);
const maxV = Math.max(...data.map((d) => d.views)) * 1.12;
const n = data.length;
const innerW = W - padL - padR;
const innerH = H - padT - padB;
const x = (i) => padL + (innerW * i) / (n - 1);
const y = (v) => padT + innerH - (v / maxV) * innerH;
// grid + y labels
gridG.textContent = "";
const rows = 4;
for (let r = 0; r <= rows; r++) {
const gv = (maxV / rows) * r;
const gy = y(gv);
gridG.appendChild(el("line", { x1: padL, y1: gy, x2: W - padR, y2: gy }));
const tx = document.createElementNS(SVGNS, "text");
tx.setAttribute("x", padL);
tx.setAttribute("y", gy - 3);
tx.textContent = fmtCompact(gv);
gridG.appendChild(tx);
}
const linePath = (key) =>
data.map((d, i) => `${i === 0 ? "M" : "L"}${x(i).toFixed(1)},${y(d[key]).toFixed(1)}`).join(" ");
const vp = linePath("views");
lineViews.setAttribute("d", vp);
lineSearch.setAttribute("d", linePath("searches"));
areaViews.setAttribute(
"d",
`${vp} L${x(n - 1).toFixed(1)},${(padT + innerH).toFixed(1)} L${x(0).toFixed(1)},${(padT + innerH).toFixed(1)} Z`
);
// animate stroke
[lineViews, lineSearch].forEach((p) => {
const len = p.getTotalLength();
p.style.transition = "none";
p.style.strokeDasharray = len;
p.style.strokeDashoffset = len;
// force reflow
void p.getBoundingClientRect();
p.style.transition = "stroke-dashoffset 0.7s ease";
p.style.strokeDashoffset = "0";
});
// points
pointsG.textContent = "";
data.forEach((d, i) => {
["searches", "views"].forEach((key) => {
const c = el("circle", {
cx: x(i),
cy: y(d[key]),
r: n > 12 ? 3 : 3.6,
class: "dot " + (key === "views" ? "dot-views" : "dot-search"),
});
c.addEventListener("mouseenter", () => showTip(d, x(i), Math.min(y(d.views), y(d.searches))));
c.addEventListener("mouseleave", hideTip);
pointsG.appendChild(c);
});
});
// x axis labels (show a subset)
chartX.textContent = "";
const labelEvery = Math.ceil(n / 6);
data.forEach((d, i) => {
if (i % labelEvery === 0 || i === n - 1) {
const s = document.createElement("span");
s.textContent = d.label;
chartX.appendChild(s);
}
});
$("#rangeLabel").textContent = "last " + range + " days";
}
function showTip(d, px, py) {
const rect = chart.getBoundingClientRect();
const sx = (px / W) * rect.width;
const sy = (py / H) * rect.height;
tip.hidden = false;
tip.innerHTML =
'<span class="tt-date">' +
d.date.toLocaleDateString("en-US", { weekday: "short", month: "short", day: "numeric" }) +
"</span>" +
'<span class="tt-views">● <b>' + fmtInt(d.views) + "</b> views</span><br>" +
'<span class="tt-search">● <b>' + fmtInt(d.searches) + "</b> searches</span>";
tip.style.left = sx + chartWrap.querySelector(".chart").offsetLeft + "px";
tip.style.top = sy + chart.offsetTop + "px";
}
function hideTip() {
tip.hidden = true;
}
// range toggle
$$(".seg-btn").forEach((btn) => {
btn.addEventListener("click", () => {
$$(".seg-btn").forEach((b) => {
b.classList.remove("is-active");
b.removeAttribute("aria-pressed");
});
btn.classList.add("is-active");
btn.setAttribute("aria-pressed", "true");
renderChart(parseInt(btn.dataset.range, 10));
});
});
renderChart(30);
window.addEventListener("resize", () => hideTip());
/* ------------------------------------------------------------------ */
/* Top articles table (sortable) */
/* ------------------------------------------------------------------ */
const articles = [
{ title: "Connecting Aurora DB over TLS", space: "Aurora DB", views: 8420, helpful: 94, updated: "2026-06-02" },
{ title: "Project Nimbus: deploy from CLI", space: "Project Nimbus", views: 6315, helpful: 91, updated: "2026-05-28" },
{ title: "Verdant API rate limits explained", space: "Verdant API", views: 5890, helpful: 88, updated: "2026-06-04" },
{ title: "Resetting your workspace password", space: "Onboarding", views: 5120, helpful: 79, updated: "2026-03-11" },
{ title: "Migrating from v3 to v4 schema", space: "Aurora DB", views: 4730, helpful: 72, updated: "2026-01-19" },
{ title: "Webhooks & event subscriptions", space: "Verdant API", views: 4210, helpful: 90, updated: "2026-05-30" },
{ title: "Backups, restores, and PITR", space: "Aurora DB", views: 3980, helpful: 86, updated: "2026-04-22" },
{ title: "Single sign-on with SAML", space: "Onboarding", views: 3540, helpful: 61, updated: "2025-11-14" },
];
const topBody = $("#topBody");
let sortKey = "title";
let sortDir = "desc"; // matches default header
function helpClass(h) {
return h >= 85 ? "help-good" : h >= 70 ? "help-mid" : "help-low";
}
function staleDays(dateStr) {
return Math.floor((Date.now() - new Date(dateStr).getTime()) / 86400000);
}
function relUpdated(dateStr) {
const days = staleDays(dateStr);
if (days > 120) return { text: days + "d ago", stale: true };
if (days > 30) return { text: Math.round(days / 30) + "mo ago", stale: false };
return { text: days + "d ago", stale: false };
}
function renderTable() {
const sorted = articles.slice().sort((a, b) => {
let av = a[sortKey], bv = b[sortKey];
if (sortKey === "updated") {
av = new Date(av).getTime();
bv = new Date(bv).getTime();
}
if (typeof av === "string") return sortDir === "asc" ? av.localeCompare(bv) : bv.localeCompare(av);
return sortDir === "asc" ? av - bv : bv - av;
});
topBody.innerHTML = sorted
.map((a) => {
const u = relUpdated(a.updated);
return (
"<tr>" +
'<td><span class="art-title">' + a.title + '</span><span class="art-space">' + a.space + "</span></td>" +
'<td class="num">' + fmtInt(a.views) + "</td>" +
'<td class="num"><span class="help-bar"><span class="help-track"><span class="help-fill ' +
helpClass(a.helpful) + '" style="width:' + a.helpful + '%"></span></span>' + a.helpful + "%</span></td>" +
'<td class="num' + (u.stale ? " cell-stale" : "") + '">' + (u.stale ? "⚑ " : "") + u.text + "</td>" +
"</tr>"
);
})
.join("");
}
$$("#topTable th.sortable").forEach((th) => {
th.querySelector("button").addEventListener("click", () => {
const key = th.dataset.key;
if (sortKey === key) {
sortDir = sortDir === "asc" ? "desc" : "asc";
} else {
sortKey = key;
sortDir = th.dataset.type === "text" ? "asc" : "desc";
}
$$("#topTable th").forEach((h) => h.classList.remove("is-sorted-asc", "is-sorted-desc"));
th.classList.add(sortDir === "asc" ? "is-sorted-asc" : "is-sorted-desc");
renderTable();
});
});
renderTable();
/* ------------------------------------------------------------------ */
/* Needs attention */
/* ------------------------------------------------------------------ */
const attentionData = [
{
id: "a1", type: "stale", icon: "⚑", iconCls: "att-stale", tagCls: "tag-stale", tag: "Stale",
name: "Migrating from v3 to v4 schema",
detail: "Last edited 140 days ago — references a deprecated migration tool.",
},
{
id: "a2", type: "rating", icon: "♥", iconCls: "att-rating", tagCls: "tag-rating", tag: "Low rated",
name: "Single sign-on with SAML",
detail: "Helpfulness dropped to 61% over 38 votes. 9 readers left feedback.",
},
{
id: "a3", type: "link", icon: "⛓", iconCls: "att-link", tagCls: "tag-link", tag: "Broken link",
name: "Resetting your workspace password",
detail: "2 outbound links return 404 (status.aurora.dev, /legacy/reset).",
},
{
id: "a4", type: "stale", icon: "⚑", iconCls: "att-stale", tagCls: "tag-stale", tag: "Stale",
name: "Verdant API authentication tokens",
detail: "No edits since the v4 token format shipped 96 days ago.",
},
];
const attentionList = $("#attentionList");
const attentionEmpty = $("#attentionEmpty");
const attentionCount = $("#attentionCount");
const navAttentionCount = $("#navAttentionCount");
let openFlags = attentionData.length;
function renderAttention() {
attentionList.innerHTML = attentionData
.map(
(a) =>
'<li class="attention-item" data-id="' + a.id + '">' +
'<span class="att-icon ' + a.iconCls + '">' + a.icon + "</span>" +
'<div class="att-body">' +
'<div class="att-top"><span class="att-name">' + a.name + "</span>" +
'<span class="att-tag ' + a.tagCls + '">' + a.tag + "</span></div>" +
'<p class="att-detail">' + a.detail + "</p></div>" +
'<div class="att-actions">' +
'<button class="att-btn is-resolve" data-act="resolve" type="button">Resolve</button>' +
'<button class="att-btn" data-act="dismiss" type="button">Dismiss</button>' +
"</div></li>"
)
.join("");
}
function updateCounts() {
attentionCount.textContent = openFlags;
navAttentionCount.textContent = openFlags;
navAttentionCount.style.display = openFlags === 0 ? "none" : "";
attentionEmpty.hidden = openFlags !== 0;
}
attentionList.addEventListener("click", (e) => {
const btn = e.target.closest(".att-btn");
if (!btn) return;
const item = btn.closest(".attention-item");
const id = item.dataset.id;
const act = btn.dataset.act;
item.classList.add("is-removing");
item.addEventListener(
"transitionend",
() => {
const idx = attentionData.findIndex((a) => a.id === id);
if (idx > -1) attentionData.splice(idx, 1);
openFlags = attentionData.length;
item.remove();
updateCounts();
},
{ once: true }
);
toast(
act === "resolve" ? "Flag resolved and re-indexed" : "Flag dismissed",
act === "resolve" ? "✓" : "↪"
);
});
renderAttention();
updateCounts();
/* ------------------------------------------------------------------ */
/* Activity feed */
/* ------------------------------------------------------------------ */
const feedData = [
{ who: "Dao Lin", init: "DL", color: "#2563eb", action: "edit", actCls: "fa-edit", verb: "edited", art: "Connecting Aurora DB over TLS", when: "4 min ago" },
{ who: "Mara Reyes", init: "MR", color: "#7c3aed", action: "publish", actCls: "fa-publish", verb: "published", art: "Verdant API rate limits explained", when: "26 min ago" },
{ who: "Ines Okafor", init: "IO", color: "#db2777", action: "revert", actCls: "fa-revert", verb: "reverted", art: "Webhooks & event subscriptions", when: "1 hr ago" },
{ who: "Tomas Vega", init: "TV", color: "#0891b2", action: "edit", actCls: "fa-edit", verb: "edited", art: "Backups, restores, and PITR", when: "3 hr ago" },
{ who: "Mara Reyes", init: "MR", color: "#7c3aed", action: "archive", actCls: "fa-archive", verb: "archived", art: "Legacy v2 setup guide", when: "Yesterday" },
{ who: "Dao Lin", init: "DL", color: "#2563eb", action: "edit", actCls: "fa-edit", verb: "edited", art: "Project Nimbus: deploy from CLI", when: "Yesterday" },
];
$("#feed").innerHTML = feedData
.map(
(f) =>
'<li class="feed-item">' +
'<span class="feed-avatar" style="background:' + f.color + '">' + f.init + "</span>" +
'<div class="feed-body">' +
'<p class="feed-text"><b>' + f.who + "</b> " + f.verb + ' <a href="#main">' + f.art + "</a></p>" +
'<p class="feed-meta"><span class="feed-action ' + f.actCls + '">' + f.action + "</span><span>" + f.when + "</span></p>" +
"</div></li>"
)
.join("");
/* ------------------------------------------------------------------ */
/* Search + misc */
/* ------------------------------------------------------------------ */
const kbSearch = $("#kbSearch");
document.addEventListener("keydown", (e) => {
if (e.key === "/" && document.activeElement !== kbSearch && !/^(INPUT|TEXTAREA)$/.test(document.activeElement.tagName)) {
e.preventDefault();
kbSearch.focus();
}
if (e.key === "Escape") {
kbSearch.blur();
if (sidebar.classList.contains("is-open")) closeNav();
}
});
kbSearch.addEventListener("keydown", (e) => {
if (e.key === "Enter" && kbSearch.value.trim()) {
toast('Searching for "' + kbSearch.value.trim() + '" across 713 articles', "⌕");
}
});
$("#refreshBtn").addEventListener("click", () => {
const btn = $("#refreshBtn");
btn.disabled = true;
btn.textContent = "↻ Syncing…";
setTimeout(() => {
btn.disabled = false;
btn.textContent = "↻ Sync";
toast("Knowledge base re-synced — 713 articles indexed");
}, 900);
});
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Aurora KB — Admin Dashboard</title>
<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=Source+Serif+4:opsz,[email protected],400;8..60,500;8..60,600&family=JetBrains+Mono:wght@400;500;600&display=swap" rel="stylesheet" />
<link rel="stylesheet" href="style.css" />
</head>
<body>
<a class="skip-link" href="#main">Skip to content</a>
<div class="shell">
<!-- Sidebar -->
<nav class="sidebar" id="sidebar" aria-label="Knowledge base navigation">
<div class="brand">
<span class="brand-mark" aria-hidden="true">◭</span>
<span class="brand-name">Aurora KB</span>
<span class="brand-tag">admin</span>
</div>
<div class="nav-group">
<p class="nav-label">Overview</p>
<a class="nav-item is-active" href="#main"><span class="ni-icon" aria-hidden="true">▤</span> Dashboard</a>
<a class="nav-item" href="#top-articles"><span class="ni-icon" aria-hidden="true">★</span> Top articles</a>
<a class="nav-item" href="#attention"><span class="ni-icon" aria-hidden="true">⚑</span> Needs attention <span class="nav-badge" id="navAttentionCount">4</span></a>
<a class="nav-item" href="#activity"><span class="ni-icon" aria-hidden="true">↻</span> Activity</a>
</div>
<div class="nav-group">
<p class="nav-label">Spaces</p>
<a class="nav-item" href="#main"><span class="ni-icon" aria-hidden="true">◔</span> Aurora DB <span class="nav-count">312</span></a>
<a class="nav-item" href="#main"><span class="ni-icon" aria-hidden="true">◔</span> Project Nimbus <span class="nav-count">188</span></a>
<a class="nav-item" href="#main"><span class="ni-icon" aria-hidden="true">◔</span> Verdant API <span class="nav-count">141</span></a>
<a class="nav-item" href="#main"><span class="ni-icon" aria-hidden="true">◔</span> Onboarding <span class="nav-count">72</span></a>
</div>
<div class="nav-group">
<p class="nav-label">Manage</p>
<a class="nav-item" href="#main"><span class="ni-icon" aria-hidden="true">⚙</span> Settings</a>
<a class="nav-item" href="#main"><span class="ni-icon" aria-hidden="true">⌗</span> Taxonomy</a>
<a class="nav-item" href="#main"><span class="ni-icon" aria-hidden="true">⎙</span> Export</a>
</div>
<div class="nav-foot">
<div class="nav-user">
<span class="avatar" aria-hidden="true">MR</span>
<span class="nav-user-meta">
<span class="nu-name">Mara Reyes</span>
<span class="nu-role">KB Lead</span>
</span>
</div>
</div>
</nav>
<div class="sidebar-scrim" id="scrim" hidden></div>
<!-- Main -->
<main class="main" id="main">
<header class="topbar">
<button class="icon-btn menu-btn" id="menuBtn" aria-label="Open navigation" aria-controls="sidebar" aria-expanded="false">☰</button>
<div class="crumbs" aria-label="Breadcrumb">
<span>Knowledge base</span>
<span class="crumb-sep" aria-hidden="true">/</span>
<span class="crumb-current">Dashboard</span>
</div>
<div class="topbar-search">
<span class="ts-icon" aria-hidden="true">⌕</span>
<input type="search" id="kbSearch" placeholder="Search 713 articles…" aria-label="Search articles" />
<kbd>/</kbd>
</div>
<div class="topbar-actions">
<button class="btn btn-ghost" id="refreshBtn">↻ Sync</button>
<button class="btn btn-primary">+ New article</button>
</div>
</header>
<div class="content">
<div class="page-head">
<div>
<h1>Knowledge base health</h1>
<p class="page-sub">713 published articles across 4 spaces · last synced <time>2 min ago</time></p>
</div>
<div class="seg" role="group" aria-label="Date range">
<button class="seg-btn" data-range="7" type="button">7d</button>
<button class="seg-btn is-active" data-range="30" type="button" aria-pressed="true">30d</button>
<button class="seg-btn" data-range="90" type="button">90d</button>
</div>
</div>
<!-- KPI cards -->
<section class="kpis" aria-label="Key metrics">
<article class="kpi">
<header class="kpi-head"><span class="kpi-label">Total articles</span><span class="kpi-dot" aria-hidden="true">▤</span></header>
<p class="kpi-value" data-count="713" data-decimals="0">0</p>
<p class="kpi-delta is-up">▲ 18 <span>new this period</span></p>
</article>
<article class="kpi">
<header class="kpi-head"><span class="kpi-label">Monthly views</span><span class="kpi-dot" aria-hidden="true">◔</span></header>
<p class="kpi-value" data-count="48230" data-decimals="0" data-format="compact">0</p>
<p class="kpi-delta is-up">▲ 12.4% <span>vs prev period</span></p>
</article>
<article class="kpi">
<header class="kpi-head"><span class="kpi-label">Avg helpfulness</span><span class="kpi-dot" aria-hidden="true">♥</span></header>
<p class="kpi-value" data-count="87.6" data-decimals="1" data-suffix="%">0</p>
<p class="kpi-delta is-up">▲ 1.9 pts <span>vs prev period</span></p>
</article>
<article class="kpi kpi-warn">
<header class="kpi-head"><span class="kpi-label">Stale articles</span><span class="kpi-dot" aria-hidden="true">⚑</span></header>
<p class="kpi-value" data-count="34" data-decimals="0">0</p>
<p class="kpi-delta is-down">▼ 6 <span>resolved this period</span></p>
</article>
</section>
<!-- Chart -->
<section class="panel chart-panel" aria-labelledby="chartTitle">
<header class="panel-head">
<div>
<h2 id="chartTitle" class="panel-title">Traffic over time</h2>
<p class="panel-sub">Page views and on-site searches · <span id="rangeLabel">last 30 days</span></p>
</div>
<div class="legend">
<span class="leg leg-views"><i aria-hidden="true"></i> Views</span>
<span class="leg leg-search"><i aria-hidden="true"></i> Searches</span>
</div>
</header>
<div class="chart-wrap">
<svg id="chart" class="chart" viewBox="0 0 800 260" preserveAspectRatio="none" role="img" aria-label="Line chart of views and searches over the selected range">
<g class="grid" id="gridLines"></g>
<path id="areaViews" class="area area-views" d="" />
<path id="lineViews" class="line line-views" d="" />
<path id="lineSearch" class="line line-search" d="" />
<g id="points"></g>
</svg>
<div class="chart-x" id="chartX" aria-hidden="true"></div>
<div class="chart-tooltip" id="chartTip" hidden></div>
</div>
</section>
<div class="grid-2">
<!-- Top articles -->
<section class="panel" id="top-articles" aria-labelledby="topTitle">
<header class="panel-head">
<div>
<h2 id="topTitle" class="panel-title">Top articles</h2>
<p class="panel-sub">Click a column to sort</p>
</div>
<a class="panel-link" href="#main">View all →</a>
</header>
<div class="table-scroll">
<table class="data-table" id="topTable">
<thead>
<tr>
<th class="sortable is-sorted-desc" data-key="title" data-type="text" scope="col"><button type="button">Article</button></th>
<th class="sortable" data-key="views" data-type="num" scope="col"><button type="button">Views</button></th>
<th class="sortable" data-key="helpful" data-type="num" scope="col"><button type="button">Helpful</button></th>
<th class="sortable" data-key="updated" data-type="date" scope="col"><button type="button">Updated</button></th>
</tr>
</thead>
<tbody id="topBody"><!-- rendered by JS --></tbody>
</table>
</div>
</section>
<!-- Needs attention -->
<section class="panel" id="attention" aria-labelledby="attTitle">
<header class="panel-head">
<div>
<h2 id="attTitle" class="panel-title">Needs attention</h2>
<p class="panel-sub"><span id="attentionCount">4</span> open flags</p>
</div>
</header>
<ul class="attention-list" id="attentionList"><!-- rendered by JS --></ul>
<p class="attention-empty" id="attentionEmpty" hidden>✓ All flags resolved. Nice work.</p>
</section>
</div>
<!-- Activity -->
<section class="panel" id="activity" aria-labelledby="actTitle">
<header class="panel-head">
<div>
<h2 id="actTitle" class="panel-title">Recent edits</h2>
<p class="panel-sub">Live activity feed across all spaces</p>
</div>
<a class="panel-link" href="#main">Audit log →</a>
</header>
<ol class="feed" id="feed"><!-- rendered by JS --></ol>
</section>
</div>
</main>
</div>
<div class="toast-host" id="toastHost" aria-live="polite" aria-atomic="true"></div>
<script src="script.js"></script>
</body>
</html>KB Dashboard (top articles · stale flags)
A knowledge-base admin dashboard for the fictional Aurora KB, built in the clean white docs aesthetic: a persistent left sidebar that lists spaces (Aurora DB, Project Nimbus, Verdant API, Onboarding), a sticky top bar with article search, and a centered content column. Four KPI stat cards animate a count-up on first view — total articles, monthly views, average helpfulness, and stale articles — each carrying a colored trend delta against the previous period.
The centerpiece is a hand-rolled SVG line chart of page views and on-site searches with a 7d / 30d / 90d segmented range toggle that re-renders the series, animates the stroke draw-in, and shows a tooltip on point hover. Underneath, a sortable Top articles table ranks pages by views, helpfulness (rendered as a mini progress bar), and last-updated recency, with stale rows flagged inline. A Needs attention panel surfaces stale, low-rated, and broken-link articles; each can be resolved or dismissed, which animates the item out, decrements the open-flag badge, and fires a toast.
A live Recent edits activity feed rounds out the view with edit, publish, revert, and archive events. Everything is vanilla JS — no frameworks, no build step. Keyboard support includes / to focus search and Esc to dismiss, the sidebar collapses to a drawer under 820px, and all controls keep visible focus rings for WCAG AA accessibility.
Illustrative UI only — fictional articles, products, and data.