Wiki — Revision History / Diff View
A MediaWiki-style revision history and diff viewer built with plain HTML, CSS, and vanilla JavaScript. Each revision row shows a timestamp, editor avatar, edit summary, byte delta, and tags like minor, revert, or bot. Radio selectors pick an older and newer revision, and the comparison panel renders a live line-by-line diff with word-level highlighting. Toggle between inline and side-by-side layouts, restore any version with a toast confirmation, and filter out minor or bot edits.
MCP
コード
: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;
--add-bg: #e7f7ec;
--add-line: #b7e6c4;
--add-ink: #14532d;
--add-mark: #abf0bf;
--del-bg: #fdeaea;
--del-line: #f3bdbd;
--del-ink: #7f1d1d;
--del-mark: #f7c4c4;
--r-sm: 6px;
--r-md: 10px;
--r-lg: 14px;
--ui: "Inter", system-ui, -apple-system, "Segoe UI", sans-serif;
--serif: "Source Serif 4", Georgia, "Times New Roman", serif;
--mono: "JetBrains Mono", ui-monospace, "SFMono-Regular", Menlo, monospace;
--sidebar-w: 232px;
--topbar-h: 56px;
}
* { box-sizing: border-box; }
html { scroll-behavior: smooth; }
body {
margin: 0;
font-family: var(--ui);
background: var(--bg);
color: var(--ink);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
line-height: 1.5;
}
a { color: var(--link); text-decoration: none; }
a:hover { color: var(--link-hover); text-decoration: underline; }
:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
border-radius: 3px;
}
kbd {
font-family: var(--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);
line-height: 1.4;
}
.skip-link {
position: absolute;
left: 8px;
top: -48px;
z-index: 60;
background: var(--ink);
color: #fff;
padding: 8px 14px;
border-radius: var(--r-sm);
transition: top .15s;
}
.skip-link:focus { top: 8px; text-decoration: none; }
/* ---------- Topbar ---------- */
.topbar {
position: sticky;
top: 0;
z-index: 40;
height: var(--topbar-h);
display: flex;
align-items: center;
gap: 14px;
padding: 0 16px;
background: var(--panel);
border-bottom: 1px solid var(--line);
}
.nav-toggle {
display: none;
flex-direction: column;
justify-content: center;
gap: 4px;
width: 38px;
height: 38px;
background: transparent;
border: 1px solid var(--line);
border-radius: var(--r-sm);
cursor: pointer;
}
.nav-toggle span {
display: block;
height: 2px;
width: 18px;
margin: 0 auto;
background: var(--ink);
border-radius: 2px;
}
.brand {
display: flex;
align-items: center;
gap: 9px;
font-weight: 800;
font-size: 16px;
color: var(--ink);
}
.brand:hover { text-decoration: none; }
.brand-mark {
display: grid;
place-items: center;
width: 30px;
height: 30px;
border-radius: 8px;
background: linear-gradient(145deg, #2563eb, #4f46e5);
color: #fff;
font-weight: 800;
font-size: 16px;
}
.brand-dim { color: var(--muted); font-weight: 700; }
.topbar-search {
position: relative;
flex: 1;
max-width: 440px;
display: flex;
align-items: center;
}
.topbar-search .search-ic {
position: absolute;
left: 11px;
width: 16px;
height: 16px;
color: var(--muted);
pointer-events: none;
}
.topbar-search input {
width: 100%;
font: inherit;
font-size: 13.5px;
padding: 8px 38px 8px 34px;
border: 1px solid var(--line-2);
border-radius: var(--r-md);
background: var(--bg-2);
color: var(--ink);
}
.topbar-search input::placeholder { color: var(--muted); }
.topbar-search input:focus {
background: var(--panel);
border-color: var(--accent);
outline: none;
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.15);
}
.topbar-search kbd { position: absolute; right: 10px; }
.topbar-actions {
display: flex;
align-items: center;
gap: 14px;
margin-left: auto;
}
.tb-link {
font-size: 13px;
font-weight: 600;
color: var(--ink-2);
}
.avatar {
display: inline-grid;
place-items: center;
width: 30px;
height: 30px;
border-radius: 50%;
background: var(--c, var(--muted));
color: #fff;
font-size: 11px;
font-weight: 700;
flex: none;
letter-spacing: .2px;
}
/* ---------- Shell ---------- */
.shell {
display: grid;
grid-template-columns: var(--sidebar-w) minmax(0, 1fr);
align-items: start;
}
.scrim {
position: fixed;
inset: var(--topbar-h) 0 0 0;
background: rgba(10, 12, 20, 0.4);
z-index: 29;
}
.sidebar {
position: sticky;
top: var(--topbar-h);
align-self: start;
height: calc(100vh - var(--topbar-h));
overflow-y: auto;
border-right: 1px solid var(--line);
background: var(--bg-2);
padding: 18px 14px 40px;
}
.side-h {
margin: 18px 8px 6px;
font-size: 11px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: .07em;
color: var(--muted);
}
.side-h:first-child { margin-top: 0; }
.side-nav ul { list-style: none; margin: 0 0 4px; padding: 0; }
.side-link {
display: block;
padding: 6px 10px;
border-radius: var(--r-sm);
font-size: 13.5px;
font-weight: 500;
color: var(--ink-2);
}
.side-link:hover { background: rgba(37, 99, 235, 0.08); text-decoration: none; color: var(--ink); }
.side-link.is-active {
background: rgba(37, 99, 235, 0.12);
color: var(--link-hover);
font-weight: 600;
}
.side-toc .side-link { color: var(--muted); font-size: 13px; }
/* ---------- Content ---------- */
.content {
min-width: 0;
padding: 26px clamp(18px, 4vw, 48px) 64px;
max-width: 920px;
}
.crumbs {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 7px;
font-size: 12.5px;
color: var(--muted);
margin-bottom: 16px;
}
.crumbs a { color: var(--muted); font-weight: 500; }
.crumbs a:hover { color: var(--link); }
.crumbs [aria-current] { color: var(--ink-2); font-weight: 600; }
.page-head { border-bottom: 1px solid var(--line); padding-bottom: 18px; margin-bottom: 22px; }
.page-kicker {
margin: 0 0 2px;
font-size: 12px;
font-weight: 600;
text-transform: uppercase;
letter-spacing: .08em;
color: var(--accent);
}
.page-head h1 {
margin: 0 0 10px;
font-family: var(--serif);
font-size: clamp(28px, 4vw, 38px);
font-weight: 600;
line-height: 1.15;
letter-spacing: -0.01em;
}
.page-sub {
margin: 0 0 14px;
font-family: var(--serif);
font-size: 16.5px;
line-height: 1.65;
color: var(--ink-2);
max-width: 62ch;
}
.page-sub em { color: var(--ink); }
.meta-strip {
display: flex;
flex-wrap: wrap;
gap: 8px 18px;
list-style: none;
margin: 0;
padding: 0;
font-size: 13px;
color: var(--muted);
}
.meta-strip strong { color: var(--ink); font-weight: 700; }
.panel {
background: var(--panel);
border: 1px solid var(--line);
border-radius: var(--r-lg);
}
/* ---------- Filter bar ---------- */
.filter-bar {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
gap: 14px;
padding: 12px 16px;
margin-bottom: 18px;
background: var(--bg-2);
}
.filter-group { display: flex; flex-wrap: wrap; gap: 16px; }
.check {
display: inline-flex;
align-items: center;
gap: 7px;
font-size: 13.5px;
font-weight: 500;
color: var(--ink-2);
cursor: pointer;
user-select: none;
}
.check input { width: 15px; height: 15px; accent-color: var(--accent); cursor: pointer; }
/* ---------- Buttons ---------- */
.btn {
font: inherit;
font-size: 13.5px;
font-weight: 600;
padding: 8px 15px;
border-radius: var(--r-md);
border: 1px solid transparent;
cursor: pointer;
transition: background .14s, border-color .14s, color .14s, box-shadow .14s, transform .06s;
}
.btn:active { transform: translateY(1px); }
.btn:disabled { opacity: .5; cursor: not-allowed; }
.btn-primary { background: var(--accent); color: #fff; }
.btn-primary:not(:disabled):hover { background: var(--link-hover); }
.btn-ghost {
background: var(--panel);
color: var(--ink-2);
border-color: var(--line-2);
}
.btn-ghost:not(:disabled):hover { background: var(--bg-2); border-color: var(--accent); color: var(--accent); }
/* ---------- Selected pair legend ---------- */
.hist-legend {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px;
font-size: 12.5px;
color: var(--muted);
margin-bottom: 10px;
}
.leg-pill {
font-weight: 600;
font-size: 12px;
padding: 3px 9px;
border-radius: 999px;
border: 1px solid var(--line-2);
color: var(--ink-2);
background: var(--bg-2);
}
.leg-pill.set.leg-old { background: var(--del-bg); border-color: var(--del-line); color: var(--del-ink); }
.leg-pill.set.leg-new { background: var(--add-bg); border-color: var(--add-line); color: var(--add-ink); }
.leg-arrow { color: var(--muted); }
/* ---------- History list ---------- */
.hist {
list-style: none;
margin: 0;
padding: 0;
border: 1px solid var(--line);
border-radius: var(--r-lg);
overflow: hidden;
background: var(--panel);
}
.hist-row {
display: grid;
grid-template-columns: auto auto 1fr;
align-items: start;
gap: 12px;
padding: 12px 16px;
border-bottom: 1px solid var(--line);
transition: background .12s;
}
.hist-row:last-child { border-bottom: none; }
.hist-row:hover { background: var(--bg-2); }
.hist-row.is-old { background: rgba(220, 38, 38, 0.045); }
.hist-row.is-new { background: rgba(22, 163, 74, 0.05); }
.hist-radios {
display: flex;
gap: 10px;
padding-top: 2px;
}
.hist-radios label {
display: inline-flex;
flex-direction: column;
align-items: center;
font-size: 9px;
font-weight: 700;
letter-spacing: .04em;
text-transform: uppercase;
color: var(--muted);
cursor: pointer;
gap: 2px;
}
.hist-radios input { margin: 0; width: 15px; height: 15px; cursor: pointer; }
.hist-radios input[name="oldid"] { accent-color: var(--danger); }
.hist-radios input[name="newid"] { accent-color: var(--tip); }
.hist-main { min-width: 0; }
.hist-line1 {
display: flex;
flex-wrap: wrap;
align-items: center;
gap: 8px;
font-size: 13px;
}
.hist-cur { color: var(--muted); }
.hist-cur a { font-weight: 500; }
.hist-time {
font-family: var(--mono);
font-size: 12.5px;
font-weight: 600;
color: var(--ink);
}
.hist-time:hover { color: var(--link); }
.hist-dot { color: var(--line-2); }
.hist-editor {
display: inline-flex;
align-items: center;
gap: 6px;
font-weight: 600;
font-size: 13px;
color: var(--link);
}
.hist-editor .avatar { width: 20px; height: 20px; font-size: 9px; }
.byte {
font-family: var(--mono);
font-size: 12px;
font-weight: 600;
padding: 1px 6px;
border-radius: 5px;
}
.byte.pos { color: var(--add-ink); background: var(--add-bg); }
.byte.neg { color: var(--del-ink); background: var(--del-bg); }
.byte.zero { color: var(--muted); background: var(--bg-2); }
.byte-size { font-family: var(--mono); font-size: 11.5px; color: var(--muted); }
.hist-summary {
margin: 4px 0 0;
font-size: 13px;
color: var(--ink-2);
line-height: 1.55;
}
.hist-summary .ital { color: var(--muted); font-style: italic; }
.tag {
display: inline-block;
font-size: 10.5px;
font-weight: 700;
letter-spacing: .03em;
text-transform: uppercase;
padding: 1px 7px;
border-radius: 999px;
margin-left: 6px;
vertical-align: middle;
}
.tag-minor { background: #eef2ff; color: #4338ca; }
.tag-revert { background: var(--del-bg); color: var(--del-ink); }
.tag-bot { background: #f1f5f9; color: #475569; }
.tag-new { background: var(--add-bg); color: var(--add-ink); }
.hist-foot { font-size: 12.5px; color: var(--muted); margin: 10px 2px 0; }
/* ---------- Diff section ---------- */
.diff-section { margin-top: 30px; scroll-margin-top: calc(var(--topbar-h) + 12px); }
.diff-head {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
gap: 12px;
margin-bottom: 14px;
}
.diff-head h2 {
margin: 0;
font-family: var(--serif);
font-size: 22px;
font-weight: 600;
}
.diff-controls { display: flex; align-items: center; gap: 10px; }
.seg {
display: inline-flex;
background: var(--bg-2);
border: 1px solid var(--line-2);
border-radius: var(--r-md);
padding: 3px;
}
.seg-btn {
font: inherit;
font-size: 12.5px;
font-weight: 600;
padding: 5px 11px;
border: none;
background: transparent;
color: var(--ink-2);
border-radius: 7px;
cursor: pointer;
transition: background .14s, color .14s, box-shadow .14s;
}
.seg-btn:hover { color: var(--ink); }
.seg-btn.is-on {
background: var(--panel);
color: var(--accent);
box-shadow: 0 1px 2px rgba(20, 20, 30, 0.12);
}
.diff-summary {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 0;
border: 1px solid var(--line);
border-bottom: none;
border-radius: var(--r-lg) var(--r-lg) 0 0;
overflow: hidden;
}
.ds-col {
display: flex;
flex-direction: column;
gap: 4px;
padding: 12px 16px;
background: var(--bg-2);
}
.ds-col + .ds-col { border-left: 1px solid var(--line); }
.ds-tag {
font-size: 11px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: .05em;
}
.ds-old { color: var(--del-ink); }
.ds-new { color: var(--add-ink); }
.ds-meta { font-size: 12.5px; color: var(--ink-2); }
.ds-meta b { font-family: var(--mono); font-weight: 600; color: var(--ink); }
.diff-empty {
display: flex;
align-items: center;
gap: 16px;
padding: 26px 22px;
border: 1px dashed var(--line-2);
border-radius: var(--r-lg);
background: var(--bg-2);
color: var(--muted);
}
.diff-empty svg { width: 36px; height: 36px; flex: none; color: var(--line-2); }
.diff-empty p { margin: 0; font-size: 14px; line-height: 1.55; max-width: 56ch; }
.diff-empty strong { color: var(--ink-2); }
.diff-body {
border: 1px solid var(--line);
border-radius: 0 0 var(--r-lg) var(--r-lg);
overflow: hidden;
font-family: var(--mono);
font-size: 13px;
line-height: 1.6;
}
/* inline mode */
.diff-row {
display: grid;
grid-template-columns: 30px 1fr;
border-bottom: 1px solid var(--line);
}
.diff-row:last-child { border-bottom: none; }
.diff-gutter {
display: grid;
place-items: center;
font-weight: 700;
color: var(--muted);
background: var(--bg-2);
border-right: 1px solid var(--line);
user-select: none;
}
.diff-text {
padding: 4px 12px;
white-space: pre-wrap;
word-break: break-word;
color: var(--ink);
}
.diff-row.add .diff-gutter { color: var(--add-ink); background: var(--add-bg); }
.diff-row.add .diff-text { background: rgba(231, 247, 236, .6); }
.diff-row.del .diff-gutter { color: var(--del-ink); background: var(--del-bg); }
.diff-row.del .diff-text { background: rgba(253, 234, 234, .6); }
.diff-row.ctx .diff-text { color: var(--ink-2); }
mark.add-mark { background: var(--add-mark); color: var(--add-ink); border-radius: 2px; padding: 0 1px; }
mark.del-mark { background: var(--del-mark); color: var(--del-ink); border-radius: 2px; padding: 0 1px; text-decoration: line-through; }
/* split mode */
.diff-body.split { display: grid; grid-template-columns: 1fr 1fr; font-family: var(--mono); }
.split-side { min-width: 0; }
.split-side + .split-side { border-left: 1px solid var(--line); }
.split-head {
font-family: var(--ui);
font-size: 11px;
font-weight: 700;
text-transform: uppercase;
letter-spacing: .05em;
padding: 7px 12px;
background: var(--bg-2);
border-bottom: 1px solid var(--line);
color: var(--muted);
}
.split-row {
display: grid;
grid-template-columns: 30px 1fr;
border-bottom: 1px solid var(--line);
min-height: 27px;
}
.split-row:last-child { border-bottom: none; }
.split-row .diff-gutter { font-size: 11px; }
.split-row.del .diff-gutter { color: var(--del-ink); background: var(--del-bg); }
.split-row.del .diff-text { background: rgba(253, 234, 234, .6); }
.split-row.add .diff-gutter { color: var(--add-ink); background: var(--add-bg); }
.split-row.add .diff-text { background: rgba(231, 247, 236, .6); }
.split-row.empty .diff-text, .split-row.empty .diff-gutter { background: repeating-linear-gradient(45deg, var(--bg-2), var(--bg-2) 6px, #f0f1f4 6px, #f0f1f4 12px); }
.disclaimer {
margin-top: 40px;
padding-top: 18px;
border-top: 1px solid var(--line);
font-size: 12.5px;
color: var(--muted);
font-style: italic;
}
/* ---------- Toast ---------- */
.toast-wrap {
position: fixed;
right: 18px;
bottom: 18px;
z-index: 80;
display: flex;
flex-direction: column;
gap: 10px;
max-width: min(360px, calc(100vw - 36px));
}
.toast {
display: flex;
align-items: flex-start;
gap: 10px;
padding: 12px 14px;
background: var(--ink);
color: #fff;
border-radius: var(--r-md);
box-shadow: 0 10px 30px rgba(10, 12, 20, 0.28);
font-size: 13.5px;
line-height: 1.45;
animation: toast-in .22s cubic-bezier(.2, .8, .3, 1);
}
.toast.out { animation: toast-out .2s forwards; }
.toast .t-ic {
width: 18px; height: 18px; flex: none; margin-top: 1px;
color: var(--tip);
}
.toast b { font-weight: 700; }
@keyframes toast-in { from { opacity: 0; transform: translateY(12px) scale(.97); } to { opacity: 1; transform: none; } }
@keyframes toast-out { to { opacity: 0; transform: translateY(8px); } }
/* ---------- Responsive ---------- */
@media (max-width: 820px) {
.nav-toggle { display: flex; }
.topbar-actions .tb-link { display: none; }
.shell { grid-template-columns: 1fr; }
.sidebar {
position: fixed;
top: var(--topbar-h);
left: 0;
width: 264px;
max-width: 84vw;
z-index: 30;
transform: translateX(-105%);
transition: transform .22s ease;
box-shadow: 4px 0 24px rgba(10, 12, 20, 0.16);
}
.sidebar.open { transform: none; }
.content { padding-top: 22px; }
}
@media (max-width: 520px) {
.topbar { gap: 10px; padding: 0 12px; }
.brand-name { display: none; }
.topbar-search kbd { display: none; }
.filter-bar { flex-direction: column; align-items: stretch; }
.filter-actions .btn { width: 100%; }
.diff-head { flex-direction: column; align-items: stretch; }
.diff-controls { justify-content: space-between; }
.hist-row {
grid-template-columns: auto 1fr;
column-gap: 10px;
}
.diff-summary { grid-template-columns: 1fr; }
.ds-col + .ds-col { border-left: none; border-top: 1px solid var(--line); }
.diff-body.split { grid-template-columns: 1fr; }
.split-side + .split-side { border-left: none; border-top: 1px solid var(--line); }
}
@media (prefers-reduced-motion: reduce) {
* { scroll-behavior: auto !important; animation-duration: .001ms !important; transition-duration: .001ms !important; }
}(function () {
"use strict";
/* ---------------------------------------------------------------
* Fictional revision data for the article "Aurora DB".
* `text` is the full article body at that revision; the diff is
* computed live (LCS line diff) between any two selected revisions.
* Revisions are ordered newest-first for display.
* ------------------------------------------------------------- */
const REVS = [
{
id: 2184, ts: "2026-06-07T14:08:00Z", date: "7 June 2026, 14:08",
editor: "RowenKell", color: "#7c3aed", summary: "Added benchmark figures for the 0.9 release; tightened the lead paragraph.",
tags: [], size: 38720, current: true,
text:
`Aurora DB is a distributed, multi-region storage engine.
It is designed for low-latency reads across geographic regions.
The engine partitions data into ranges called shards.
Each shard is replicated across at least three availability zones.
Writes are coordinated using a Raft-based consensus group.
Reads default to the nearest healthy replica.
In the 0.9 release, p99 read latency dropped to 4.1 ms.
The query planner supports cost-based join reordering.
Aurora DB exposes a wire protocol compatible with the Verdant SQL dialect.`
},
{
id: 2179, ts: "2026-06-05T09:42:00Z", date: "5 June 2026, 09:42",
editor: "MaeLindqvist", color: "#0891b2", summary: "Clarified replication; noted Raft consensus.",
tags: [], size: 38201,
text:
`Aurora DB is a distributed, multi-region storage engine.
It is designed for low-latency reads across geographic regions.
The engine partitions data into ranges called shards.
Each shard is replicated across at least three availability zones.
Writes are coordinated using a Raft-based consensus group.
Reads default to the nearest healthy replica.
The query planner supports cost-based join reordering.
Aurora DB exposes a wire protocol compatible with the Verdant SQL dialect.`
},
{
id: 2176, ts: "2026-06-04T22:15:00Z", date: "4 June 2026, 22:15",
editor: "VerdantBot", color: "#475569", summary: "Reverted edits by 203.0.113.44 to last revision by MaeLindqvist.",
tags: ["revert", "bot"], minor: true,
text:
`Aurora DB is a distributed, multi-region storage engine.
It is designed for low-latency reads across geographic regions.
The engine partitions data into ranges called shards.
Each shard is replicated across at least three availability zones.
Writes are coordinated using a consensus group.
Reads default to the nearest healthy replica.
The query planner supports cost-based join reordering.
Aurora DB exposes a wire protocol compatible with the Verdant SQL dialect.`,
size: 37980
},
{
id: 2175, ts: "2026-06-04T22:09:00Z", date: "4 June 2026, 22:09",
editor: "203.0.113.44", color: "#9ca3af", summary: "best database ever!!!", anon: true,
tags: [],
text:
`Aurora DB is the BEST distributed storage engine in the whole world.
It is designed for low-latency reads across geographic regions.
The engine partitions data into ranges called shards.
Each shard is replicated across at least three availability zones.
Writes are coordinated using a consensus group.
Reads default to the nearest healthy replica.
The query planner supports cost-based join reordering.
Aurora DB exposes a wire protocol compatible with the Verdant SQL dialect.`,
size: 38150
},
{
id: 2168, ts: "2026-05-29T11:03:00Z", date: "29 May 2026, 11:03",
editor: "MaeLindqvist", color: "#0891b2", summary: "Documented read routing behaviour.",
tags: [],
text:
`Aurora DB is a distributed, multi-region storage engine.
It is designed for low-latency reads across geographic regions.
The engine partitions data into ranges called shards.
Each shard is replicated across at least three availability zones.
Writes are coordinated using a consensus group.
Reads default to the nearest healthy replica.
The query planner supports cost-based join reordering.`,
size: 37640
},
{
id: 2160, ts: "2026-05-21T16:48:00Z", date: "21 May 2026, 16:48",
editor: "Tobiasen-Q", color: "#db2777", summary: "Typo fix in lead.",
tags: ["minor"], minor: true,
text:
`Aurora DB is a distributed, multi-region storage engine.
It is designed for low-latency reads across geographic regions.
The engine partitions data into ranges called shards.
Each shard is replicated across at least three availability zones.
Writes are coordinated using a consensus group.
The query planner supports cost-based join reordering.`,
size: 37380
},
{
id: 2151, ts: "2026-05-12T08:30:00Z", date: "12 May 2026, 08:30",
editor: "RowenKell", color: "#7c3aed", summary: "Added section on the query planner.",
tags: [],
text:
`Aurora DB is a distributed multi-region storage engine.
It is designed for low-latency reads across geographic regions.
The engine partitions data into ranges called shards.
Each shard is replicated across at least three availability zones.
Writes are coordinated using a consensus group.
The query planner supports cost-based join reordering.`,
size: 37260
},
{
id: 2140, ts: "2026-05-03T19:12:00Z", date: "3 May 2026, 19:12",
editor: "IngridSolace", color: "#16a34a", summary: "Expanded the lead and replication details.",
tags: [],
text:
`Aurora DB is a distributed multi-region storage engine.
It is designed for low-latency reads across geographic regions.
The engine partitions data into ranges called shards.
Each shard is replicated across at least three availability zones.
Writes are coordinated using a consensus group.`,
size: 36810
},
{
id: 2122, ts: "2026-04-18T13:55:00Z", date: "18 April 2026, 13:55",
editor: "VerdantBot", color: "#475569", summary: "Added category and infobox links.",
tags: ["bot", "minor"], minor: true,
text:
`Aurora DB is a distributed storage engine.
It is designed for low-latency reads across regions.
The engine partitions data into ranges called shards.
Each shard is replicated across availability zones.`,
size: 36120
},
{
id: 2098, ts: "2026-04-02T07:24:00Z", date: "2 April 2026, 07:24",
editor: "IngridSolace", color: "#16a34a", summary: "Initial draft of the article.",
tags: ["new"],
text:
`Aurora DB is a distributed storage engine.
It is designed for low-latency reads across regions.
The engine partitions data into shards.`,
size: 35804
}
];
const byId = {};
REVS.forEach((r) => (byId[r.id] = r));
/* --------------------------- LCS line diff --------------------------- */
function lineDiff(aText, bText) {
const a = aText.split("\n");
const b = bText.split("\n");
const n = a.length, m = b.length;
const dp = Array.from({ length: n + 1 }, () => new Int32Array(m + 1));
for (let i = n - 1; i >= 0; i--) {
for (let j = m - 1; j >= 0; j--) {
dp[i][j] = a[i] === b[j] ? dp[i + 1][j + 1] + 1 : Math.max(dp[i + 1][j], dp[i][j + 1]);
}
}
const out = [];
let i = 0, j = 0;
while (i < n && j < m) {
if (a[i] === b[j]) { out.push({ type: "ctx", text: a[i] }); i++; j++; }
else if (dp[i + 1][j] >= dp[i][j + 1]) { out.push({ type: "del", text: a[i] }); i++; }
else { out.push({ type: "add", text: b[j] }); j++; }
}
while (i < n) out.push({ type: "del", text: a[i++] });
while (j < m) out.push({ type: "add", text: b[j++] });
return out;
}
/* word-level highlight for adjacent del/add pairs */
function wordMark(oldStr, newStr) {
const ow = oldStr.split(/(\s+)/);
const nw = newStr.split(/(\s+)/);
const n = ow.length, m = nw.length;
const dp = Array.from({ length: n + 1 }, () => new Int32Array(m + 1));
for (let i = n - 1; i >= 0; i--)
for (let j = m - 1; j >= 0; j--)
dp[i][j] = ow[i] === nw[j] ? dp[i + 1][j + 1] + 1 : Math.max(dp[i + 1][j], dp[i][j + 1]);
let i = 0, j = 0, oOut = "", nOut = "";
while (i < n && j < m) {
if (ow[i] === nw[j]) { oOut += esc(ow[i]); nOut += esc(nw[j]); i++; j++; }
else if (dp[i + 1][j] >= dp[i][j + 1]) { oOut += mk("del-mark", ow[i]); i++; }
else { nOut += mk("add-mark", nw[j]); j++; }
}
while (i < n) oOut += mk("del-mark", ow[i++]);
while (j < m) nOut += mk("add-mark", nw[j++]);
return [oOut, nOut];
}
function mk(cls, s) { return s.trim() === "" ? esc(s) : `<mark class="${cls}">${esc(s)}</mark>`; }
function esc(s) {
return String(s).replace(/[&<>"]/g, (c) => ({ "&": "&", "<": "<", ">": ">", '"': """ }[c]));
}
/* ----------------------------- State ----------------------------- */
let oldId = null, newId = null, mode = "inline", compared = false;
const $ = (s, r = document) => r.querySelector(s);
const histList = $("#histList");
const compareBtn = $("#compareBtn");
const restoreBtn = $("#restoreBtn");
const diffEmpty = $("#diffEmpty");
const diffBody = $("#diffBody");
const diffSummary = $("#diffSummary");
const selOld = $("#selOld");
const selNew = $("#selNew");
/* --------------------------- Render list --------------------------- */
function bytePill(delta) {
if (delta == null) return `<span class="byte zero">—</span>`;
if (delta > 0) return `<span class="byte pos">+${delta}</span>`;
if (delta < 0) return `<span class="byte neg">${delta}</span>`;
return `<span class="byte zero">0</span>`;
}
function tagHtml(tags) {
if (!tags || !tags.length) return "";
const cls = { minor: "tag-minor", revert: "tag-revert", bot: "tag-bot", new: "tag-new" };
return tags.map((t) => `<span class="tag ${cls[t] || "tag-minor"}">${t}</span>`).join("");
}
function renderList() {
const hideMinor = $("#hideMinor").checked;
const hideBots = $("#hideBots").checked;
histList.innerHTML = "";
REVS.forEach((r, idx) => {
if (hideMinor && r.minor) return;
if (hideBots && r.tags && r.tags.includes("bot")) return;
const prev = REVS[idx + 1];
const delta = prev ? r.size - prev.size : null;
const li = document.createElement("li");
li.className = "hist-row";
li.dataset.id = r.id;
if (r.id === oldId) li.classList.add("is-old");
if (r.id === newId) li.classList.add("is-new");
const initials = r.anon
? "IP"
: r.editor.replace(/[^A-Za-z]/g, "").slice(0, 2).toUpperCase();
li.innerHTML = `
<div class="hist-radios">
<label title="Select as older revision">
<span>old</span>
<input type="radio" name="oldid" value="${r.id}" ${r.id === oldId ? "checked" : ""} ${r.current ? "disabled" : ""}>
</label>
<label title="Select as newer revision">
<span>new</span>
<input type="radio" name="newid" value="${r.id}" ${r.id === newId ? "checked" : ""}>
</label>
</div>
<div class="byte-block">${bytePill(delta)}</div>
<div class="hist-main">
<div class="hist-line1">
<a href="#diff" class="hist-time">${r.date}</a>
<span class="hist-dot">·</span>
<span class="hist-editor"><span class="avatar" style="--c:${r.color}">${initials}</span>${r.editor}</span>
<span class="hist-dot">·</span>
<span class="byte-size">${r.size.toLocaleString()} bytes</span>
${r.current ? `<span class="tag tag-minor">cur</span>` : ""}
${tagHtml(r.tags)}
</div>
<p class="hist-summary">${r.summary ? esc(r.summary) : `<span class="ital">No edit summary</span>`}</p>
</div>`;
histList.appendChild(li);
});
histList.querySelectorAll('input[name="oldid"]').forEach((el) =>
el.addEventListener("change", () => { oldId = Number(el.value); afterPick(); }));
histList.querySelectorAll('input[name="newid"]').forEach((el) =>
el.addEventListener("change", () => { newId = Number(el.value); afterPick(); }));
}
function afterPick() {
// Keep oldId strictly older than newId by revision id.
if (oldId != null && newId != null && oldId > newId) {
const t = oldId; oldId = newId; newId = t;
}
updateLegend();
syncRowClasses();
compareBtn.disabled = !(oldId != null && newId != null && oldId !== newId);
}
function syncRowClasses() {
histList.querySelectorAll(".hist-row").forEach((row) => {
const id = Number(row.dataset.id);
row.classList.toggle("is-old", id === oldId);
row.classList.toggle("is-new", id === newId);
});
// reflect possible swap back into the radios
histList.querySelectorAll('input[name="oldid"]').forEach((el) => (el.checked = Number(el.value) === oldId));
histList.querySelectorAll('input[name="newid"]').forEach((el) => (el.checked = Number(el.value) === newId));
}
function updateLegend() {
if (oldId != null) {
selOld.textContent = `older — ${byId[oldId].date}`;
selOld.classList.add("set");
} else { selOld.textContent = "older — none"; selOld.classList.remove("set"); }
if (newId != null) {
selNew.textContent = `newer — ${byId[newId].date}`;
selNew.classList.add("set");
} else { selNew.textContent = "newer — none"; selNew.classList.remove("set"); }
}
/* ----------------------------- Diff render ----------------------------- */
function renderDiff() {
if (!compared || oldId == null || newId == null) return;
const o = byId[oldId], nw = byId[newId];
$("#dsOldMeta").innerHTML = `<b>${o.date}</b> · ${esc(o.editor)} · rev ${o.id}`;
$("#dsNewMeta").innerHTML = `<b>${nw.date}</b> · ${esc(nw.editor)} · rev ${nw.id}`;
diffSummary.hidden = false;
diffEmpty.hidden = true;
diffBody.hidden = false;
const ops = lineDiff(o.text, nw.text);
diffBody.className = "diff-body" + (mode === "split" ? " split" : "");
diffBody.innerHTML = mode === "split" ? renderSplit(ops) : renderInline(ops);
restoreBtn.disabled = false;
}
function renderInline(ops) {
let html = "";
for (let k = 0; k < ops.length; k++) {
const op = ops[k];
// pair a del immediately followed by an add for word-level marks
if (op.type === "del" && ops[k + 1] && ops[k + 1].type === "add") {
const [oMark, nMark] = wordMark(op.text, ops[k + 1].text);
html += `<div class="diff-row del"><span class="diff-gutter">−</span><span class="diff-text">${oMark}</span></div>`;
html += `<div class="diff-row add"><span class="diff-gutter">+</span><span class="diff-text">${nMark}</span></div>`;
k++;
continue;
}
const sym = op.type === "add" ? "+" : op.type === "del" ? "−" : "";
html += `<div class="diff-row ${op.type}"><span class="diff-gutter">${sym}</span><span class="diff-text">${esc(op.text)}</span></div>`;
}
return html;
}
function renderSplit(ops) {
const left = [], right = [];
let ln = 0, rn = 0, k = 0;
while (k < ops.length) {
const op = ops[k];
if (op.type === "ctx") {
ln++; rn++;
left.push(rowL(ln, op.text, "ctx"));
right.push(rowL(rn, op.text, "ctx"));
k++;
} else if (op.type === "del" && ops[k + 1] && ops[k + 1].type === "add") {
const [oMark, nMark] = wordMark(op.text, ops[k + 1].text);
ln++; rn++;
left.push(rowRaw(ln, oMark, "del"));
right.push(rowRaw(rn, nMark, "add"));
k += 2;
} else if (op.type === "del") {
ln++;
left.push(rowL(ln, op.text, "del"));
right.push(rowEmpty());
k++;
} else { // add
rn++;
left.push(rowEmpty());
right.push(rowL(rn, op.text, "add"));
k++;
}
}
return (
`<div class="split-side"><div class="split-head">Older · rev ${oldId}</div>${left.join("")}</div>` +
`<div class="split-side"><div class="split-head">Newer · rev ${newId}</div>${right.join("")}</div>`
);
}
function rowL(n, text, cls) { return `<div class="split-row ${cls}"><span class="diff-gutter">${n}</span><span class="diff-text">${esc(text)}</span></div>`; }
function rowRaw(n, html, cls) { return `<div class="split-row ${cls}"><span class="diff-gutter">${n}</span><span class="diff-text">${html}</span></div>`; }
function rowEmpty() { return `<div class="split-row empty"><span class="diff-gutter"></span><span class="diff-text"></span></div>`; }
/* ----------------------------- Toast ----------------------------- */
const toastWrap = $("#toastWrap");
function toast(msg) {
const el = document.createElement("div");
el.className = "toast";
el.setAttribute("role", "status");
el.innerHTML = `<svg class="t-ic" viewBox="0 0 24 24" aria-hidden="true"><path d="M20 6L9 17l-5-5" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"/></svg><span>${msg}</span>`;
toastWrap.appendChild(el);
setTimeout(() => {
el.classList.add("out");
el.addEventListener("animationend", () => el.remove(), { once: true });
}, 3200);
}
/* ----------------------------- Wiring ----------------------------- */
compareBtn.addEventListener("click", () => {
compared = true;
renderDiff();
$("#diff").scrollIntoView({ behavior: "smooth", block: "start" });
toast(`Comparing rev <b>${oldId}</b> → rev <b>${newId}</b>.`);
});
$("#viewMode").addEventListener("click", (e) => {
const btn = e.target.closest(".seg-btn");
if (!btn) return;
mode = btn.dataset.mode;
$("#viewMode").querySelectorAll(".seg-btn").forEach((b) => {
const on = b === btn;
b.classList.toggle("is-on", on);
b.setAttribute("aria-pressed", String(on));
});
renderDiff();
});
restoreBtn.addEventListener("click", () => {
if (newId == null) return;
const r = byId[newId];
toast(`Restored revision from <b>${r.date}</b> by ${esc(r.editor)}. A new edit was queued for review.`);
});
$("#hideMinor").addEventListener("change", renderList);
$("#hideBots").addEventListener("change", () => {
renderList();
afterPick();
});
$("#hideMinor").addEventListener("change", afterPick);
/* sidebar drawer */
const sidebar = $("#sidebar");
const navToggle = $("#navToggle");
const scrim = $("#scrim");
function setDrawer(open) {
sidebar.classList.toggle("open", open);
navToggle.setAttribute("aria-expanded", String(open));
scrim.hidden = !open;
}
navToggle.addEventListener("click", () => setDrawer(!sidebar.classList.contains("open")));
scrim.addEventListener("click", () => setDrawer(false));
document.addEventListener("keydown", (e) => {
if (e.key === "Escape" && sidebar.classList.contains("open")) setDrawer(false);
if (e.key === "/" && document.activeElement.tagName !== "INPUT") {
e.preventDefault();
$(".topbar-search input").focus();
}
});
sidebar.querySelectorAll(".side-toc a, .side-link").forEach((a) =>
a.addEventListener("click", () => { if (window.innerWidth <= 820) setDrawer(false); }));
/* ----------------------------- Init ----------------------------- */
// Preselect a sensible default pair: latest vs. the one before it.
newId = REVS[0].id;
oldId = REVS[1].id;
renderList();
afterPick();
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Aurora DB — Revision history</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>
<header class="topbar">
<button class="nav-toggle" id="navToggle" aria-label="Toggle navigation" aria-expanded="false">
<span></span><span></span><span></span>
</button>
<a class="brand" href="#">
<span class="brand-mark" aria-hidden="true">W</span>
<span class="brand-name">Verdant<span class="brand-dim">Wiki</span></span>
</a>
<div class="topbar-search" role="search">
<svg viewBox="0 0 24 24" aria-hidden="true" class="search-ic"><path d="M21 21l-4.3-4.3M11 19a8 8 0 1 1 0-16 8 8 0 0 1 0 16z" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>
<input type="search" placeholder="Search VerdantWiki" aria-label="Search VerdantWiki" />
<kbd>/</kbd>
</div>
<nav class="topbar-actions" aria-label="User">
<a href="#" class="tb-link">Talk</a>
<a href="#" class="tb-link">Watch</a>
<span class="avatar tb-avatar" style="--c:#7c3aed">RK</span>
</nav>
</header>
<div class="shell">
<div class="scrim" id="scrim" hidden></div>
<aside class="sidebar" id="sidebar" aria-label="Wiki navigation">
<nav class="side-nav">
<p class="side-h">Article</p>
<ul>
<li><a href="#" class="side-link">Read</a></li>
<li><a href="#" class="side-link">Edit source</a></li>
<li><a href="#" class="side-link is-active" aria-current="page">View history</a></li>
<li><a href="#" class="side-link">Talk</a></li>
</ul>
<p class="side-h">Tools</p>
<ul>
<li><a href="#" class="side-link">What links here</a></li>
<li><a href="#" class="side-link">Related changes</a></li>
<li><a href="#" class="side-link">Permanent link</a></li>
<li><a href="#" class="side-link">Page information</a></li>
<li><a href="#" class="side-link">Cite this page</a></li>
</ul>
<p class="side-h">In this article</p>
<ul class="side-toc">
<li><a href="#filters" class="side-link">Filter revisions</a></li>
<li><a href="#revisions" class="side-link">Revision list</a></li>
<li><a href="#diff" class="side-link">Comparison</a></li>
</ul>
</nav>
</aside>
<main class="content" id="main">
<nav class="crumbs" aria-label="Breadcrumb">
<a href="#">Database systems</a>
<span aria-hidden="true">›</span>
<a href="#">Distributed databases</a>
<span aria-hidden="true">›</span>
<span aria-current="page">Aurora DB</span>
</nav>
<header class="page-head">
<p class="page-kicker">Revision history of</p>
<h1>Aurora DB</h1>
<p class="page-sub">
This page shows the edit history for the article <em>Aurora DB</em>, a fictional
distributed, multi-region storage engine. Select two revisions with the radio buttons,
then compare them to see exactly what changed between edits.
</p>
<ul class="meta-strip">
<li><strong>2,184</strong> total revisions</li>
<li><strong>41</strong> distinct editors</li>
<li>Created <time datetime="2019-03-02">2 Mar 2019</time></li>
<li>Page size <strong>38,720</strong> bytes</li>
</ul>
</header>
<section id="filters" class="panel filter-bar" aria-label="Filter revisions">
<div class="filter-group">
<label class="check"><input type="checkbox" id="hideMinor"> <span>Hide minor edits</span></label>
<label class="check"><input type="checkbox" id="hideBots"> <span>Hide bots</span></label>
</div>
<div class="filter-actions">
<button class="btn btn-primary" id="compareBtn" disabled>Compare selected revisions</button>
</div>
</section>
<section id="revisions" aria-label="Revision list">
<div class="hist-legend" role="note">
<span>Selected pair:</span>
<span id="selOld" class="leg-pill leg-old">older — none</span>
<span class="leg-arrow" aria-hidden="true">→</span>
<span id="selNew" class="leg-pill leg-new">newer — none</span>
</div>
<ol class="hist" id="histList" aria-label="Revisions, newest first"></ol>
<p class="hist-foot">Showing the 10 most recent revisions. Older revisions are paginated on the full history page.</p>
</section>
<section id="diff" class="diff-section" aria-label="Revision comparison">
<div class="diff-head">
<h2>Comparison of revisions</h2>
<div class="diff-controls" role="group" aria-label="Diff view mode">
<div class="seg" id="viewMode">
<button class="seg-btn is-on" data-mode="inline" aria-pressed="true">Inline</button>
<button class="seg-btn" data-mode="split" aria-pressed="false">Side‑by‑side</button>
</div>
<button class="btn btn-ghost" id="restoreBtn" disabled>Restore this version</button>
</div>
</div>
<div class="diff-summary" id="diffSummary" hidden>
<div class="ds-col">
<span class="ds-tag ds-old">Older revision</span>
<span id="dsOldMeta" class="ds-meta"></span>
</div>
<div class="ds-col">
<span class="ds-tag ds-new">Newer revision</span>
<span id="dsNewMeta" class="ds-meta"></span>
</div>
</div>
<div class="diff-empty" id="diffEmpty">
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M9 3v18M15 3v18M4 8h16M4 16h16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round"/></svg>
<p>Pick an <strong>older</strong> and a <strong>newer</strong> revision above, then choose
<em>Compare selected revisions</em> to see the line-by-line difference here.</p>
</div>
<div class="diff-body" id="diffBody" hidden></div>
</section>
<p class="disclaimer">Illustrative UI only — fictional articles, products, and data.</p>
</main>
</div>
<div class="toast-wrap" id="toastWrap" aria-live="polite" aria-atomic="true"></div>
<script src="script.js"></script>
</body>
</html>Revision History / Diff View
A clean, MediaWiki-flavoured revision history for a fictional encyclopedia article — Aurora DB, a multi-region storage engine. The page keeps the familiar wiki chrome: a persistent left sidebar with article and tool links, a search bar with a / hotkey, and a breadcrumb trail. The history itself is an ordered, newest-first list where every row carries a monospace timestamp, the editor’s avatar and name, the edit summary, a colored byte-delta pill (green for additions, red for removals), the resulting page size, and status tags such as minor, revert, bot, or new.
Each row exposes two radio buttons — old and new — exactly like MediaWiki’s history page. Pick an older and a newer revision and the engine normalizes their order automatically, then Compare selected revisions renders the diff below. The diff is computed live with a longest-common-subsequence line algorithm, so added and removed lines are detected from the underlying article text rather than hard-coded. Changed lines get word-level highlighting, with struck-through red marks for removed words and green marks for added ones.
The comparison panel toggles between an inline unified view and a side-by-side split view, each with line gutters and a per-revision header. A Restore this version button fires a toast confirming the rollback, and checkbox filters let you hide minor edits or bot edits to declutter long histories. Everything is keyboard-accessible with visible focus rings, semantic landmarks, and a sidebar that collapses into a drawer on narrow screens.
Illustrative UI only — fictional articles, products, and data.