News — Newsroom Dashboard
A late-city newsroom CMS dashboard for the fictional Meridian Ledger, set in a cream newsprint palette with Playfair Display mastheads and a single accent red. A five-tile KPI strip counts up on view, a filterable publishing queue lists headlines with author, desk, status pills and schedule slots, a needs-attention rail surfaces breaking and awaiting-review items beside a duotone press photo, and an activity feed logs every move. Advancing a story through draft to published animates the queue, the counters and the feed.
MCP
Code
:root {
--cream: #f4efe4;
--paper: #faf7f0;
--white: #ffffff;
--newsprint: #efe9da;
--ink: #16130f;
--ink-2: #2b2620;
--ink-3: #4a443b;
--muted: #7a7164;
--red: #b4291f;
--red-d: #8f1f17;
--red-50: #f3dcd9;
--rule: rgba(22, 19, 15, 0.16);
--rule-2: rgba(22, 19, 15, 0.30);
--rule-hair: rgba(22, 19, 15, 0.10);
--ok: #2f7d4f;
--warn: #b67a18;
--danger: #b4291f;
--r-sm: 4px;
--r-md: 8px;
--r-lg: 12px;
--serif: "Playfair Display", "Times New Roman", Georgia, serif;
--sans: "Inter", system-ui, -apple-system, sans-serif;
}
* { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
body {
margin: 0;
font-family: var(--sans);
font-size: 15px;
line-height: 1.5;
color: var(--ink);
background: var(--cream);
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.visually-hidden {
position: absolute;
width: 1px; height: 1px;
padding: 0; margin: -1px;
overflow: hidden; clip: rect(0 0 0 0);
white-space: nowrap; border: 0;
}
::selection { background: var(--red-50); color: var(--ink); }
/* ====================== APP SHELL ====================== */
.app {
display: grid;
grid-template-columns: 264px 1fr;
min-height: 100vh;
}
/* ====================== SIDEBAR ====================== */
.sidebar {
display: flex;
flex-direction: column;
background: var(--paper);
border-right: 1px solid var(--rule);
padding: 22px 18px;
position: sticky;
top: 0;
height: 100vh;
overflow-y: auto;
}
.brand {
display: flex;
align-items: center;
gap: 12px;
padding-bottom: 18px;
margin-bottom: 16px;
border-bottom: 2px solid var(--ink);
}
.brand-mark {
width: 42px; height: 42px;
display: grid; place-items: center;
background: var(--ink);
color: var(--paper);
font-family: var(--serif);
font-weight: 800;
font-size: 18px;
letter-spacing: 0.02em;
border-radius: var(--r-sm);
}
.brand-text { display: flex; flex-direction: column; line-height: 1.15; }
.brand-name {
font-family: var(--serif);
font-weight: 800;
font-size: 17px;
color: var(--ink);
}
.brand-sub {
font-size: 10px;
letter-spacing: 0.16em;
text-transform: uppercase;
color: var(--muted);
margin-top: 3px;
}
.nav { display: flex; flex-direction: column; gap: 1px; flex: 1; }
.nav-label {
font-size: 10px;
letter-spacing: 0.18em;
text-transform: uppercase;
color: var(--muted);
margin: 16px 6px 7px;
padding-top: 8px;
border-top: 1px solid var(--rule-hair);
}
.nav-label:first-child { border-top: 0; padding-top: 0; margin-top: 2px; }
.nav-item {
display: flex;
align-items: center;
gap: 11px;
padding: 8px 10px;
border-radius: var(--r-sm);
color: var(--ink-2);
text-decoration: none;
font-size: 14px;
font-weight: 500;
transition: background 0.14s ease, color 0.14s ease;
}
.nav-ico { width: 16px; text-align: center; color: var(--muted); font-size: 13px; }
.nav-item:hover { background: var(--newsprint); color: var(--ink); }
.nav-item:focus-visible { outline: 2px solid var(--red); outline-offset: 1px; }
.nav-item.is-active {
background: var(--ink);
color: var(--paper);
}
.nav-item.is-active .nav-ico { color: var(--paper); }
.nav-badge {
margin-left: auto;
font-size: 11px;
font-weight: 700;
background: var(--newsprint);
color: var(--ink-3);
padding: 1px 7px;
border-radius: 999px;
}
.nav-badge--red { background: var(--red); color: var(--white); }
.nav-item.is-active .nav-badge { background: rgba(255, 255, 255, 0.2); color: var(--paper); }
.sidebar-foot { margin-top: 16px; padding-top: 14px; border-top: 1px solid var(--rule); }
.editor-card { display: flex; align-items: center; gap: 11px; }
.editor-avatar {
width: 38px; height: 38px;
display: grid; place-items: center;
background: var(--red-50);
color: var(--red-d);
font-weight: 700;
font-size: 13px;
border-radius: 50%;
border: 1px solid var(--rule);
}
.editor-meta { display: flex; flex-direction: column; line-height: 1.2; }
.editor-name { font-weight: 600; font-size: 13px; }
.editor-role { font-size: 11px; color: var(--muted); }
/* ====================== MAIN ====================== */
.main {
padding: 26px 30px 40px;
max-width: 1320px;
}
.topbar {
display: flex;
justify-content: space-between;
align-items: flex-end;
gap: 24px;
padding-bottom: 18px;
border-bottom: 2px solid var(--ink);
margin-bottom: 4px;
}
.kicker {
margin: 0;
font-size: 11px;
letter-spacing: 0.2em;
text-transform: uppercase;
font-weight: 600;
color: var(--red);
}
.topbar-title {
font-family: var(--serif);
font-weight: 900;
font-size: clamp(28px, 4vw, 40px);
line-height: 1.02;
margin: 6px 0 5px;
letter-spacing: -0.01em;
}
.dateline { margin: 0; font-size: 12px; color: var(--muted); }
.dateline time { color: var(--ink-3); font-weight: 600; }
.topbar-right { display: flex; align-items: center; gap: 10px; flex-shrink: 0; }
.search {
display: flex; align-items: center; gap: 7px;
background: var(--white);
border: 1px solid var(--rule);
border-radius: var(--r-sm);
padding: 0 10px;
height: 38px;
}
.search-ico { color: var(--muted); font-size: 15px; }
.search input {
border: 0; outline: 0; background: transparent;
font-family: var(--sans); font-size: 13px; color: var(--ink);
width: 190px;
}
.search:focus-within { border-color: var(--ink-3); }
.btn {
font-family: var(--sans);
font-size: 13px;
font-weight: 600;
height: 38px;
padding: 0 16px;
border-radius: var(--r-sm);
border: 1px solid var(--ink);
cursor: pointer;
transition: transform 0.06s ease, background 0.14s ease, color 0.14s ease;
}
.btn:active { transform: translateY(1px); }
.btn:focus-visible { outline: 2px solid var(--red); outline-offset: 2px; }
.btn--ghost { background: var(--white); color: var(--ink); }
.btn--ghost:hover { background: var(--newsprint); }
.btn--red { background: var(--red); border-color: var(--red); color: var(--white); }
.btn--red:hover { background: var(--red-d); border-color: var(--red-d); }
/* ====================== KPI STRIP ====================== */
.kpis {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 0;
margin: 22px 0 26px;
background: var(--paper);
border: 1px solid var(--rule);
border-radius: var(--r-sm);
overflow: hidden;
}
.kpi {
padding: 16px 18px;
border-right: 1px solid var(--rule-hair);
display: flex;
flex-direction: column;
gap: 6px;
}
.kpi:last-child { border-right: 0; }
.kpi-label {
font-size: 10px;
letter-spacing: 0.16em;
text-transform: uppercase;
color: var(--muted);
font-weight: 600;
}
.kpi-num {
font-family: var(--serif);
font-weight: 800;
font-size: clamp(26px, 3vw, 34px);
line-height: 1;
color: var(--ink);
font-variant-numeric: tabular-nums;
}
.kpi-foot { font-size: 11.5px; color: var(--ink-3); }
.kpi-foot--ok { color: var(--ok); font-weight: 600; }
.kpi-foot--warn { color: var(--warn); font-weight: 600; }
.kpi--accent { background: var(--ink); }
.kpi--accent .kpi-label { color: rgba(244, 239, 228, 0.65); }
.kpi--accent .kpi-num { color: var(--paper); }
.kpi--accent .kpi-foot { color: #8fd0a6; }
/* ====================== GRID ====================== */
.grid {
display: grid;
grid-template-columns: minmax(0, 1.85fr) minmax(300px, 1fr);
gap: 26px;
align-items: start;
}
.panel {
background: var(--paper);
border: 1px solid var(--rule);
border-radius: var(--r-sm);
padding: 0;
overflow: hidden;
}
.panel-head {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
padding: 16px 18px 14px;
border-bottom: 2px solid var(--ink);
}
.panel-title {
font-family: var(--serif);
font-weight: 800;
font-size: 19px;
margin: 0;
letter-spacing: -0.01em;
}
.panel-sub { margin: 2px 0 0; font-size: 12px; color: var(--muted); }
/* ===== Filters ===== */
.filters { padding: 14px 18px 4px; }
.filter-row {
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
margin-bottom: 12px;
}
.filter-cap {
font-size: 10px;
letter-spacing: 0.16em;
text-transform: uppercase;
color: var(--muted);
font-weight: 700;
width: 48px;
flex-shrink: 0;
}
.chips { display: flex; gap: 6px; flex-wrap: wrap; }
.chip {
font-family: var(--sans);
font-size: 12.5px;
font-weight: 600;
padding: 5px 13px;
border: 1px solid var(--rule);
background: var(--white);
color: var(--ink-2);
border-radius: 999px;
cursor: pointer;
transition: all 0.13s ease;
}
.chip:hover { border-color: var(--ink-3); }
.chip:focus-visible { outline: 2px solid var(--red); outline-offset: 1px; }
.chip.is-on { background: var(--ink); color: var(--paper); border-color: var(--ink); }
.select {
font-family: var(--sans);
font-size: 13px;
font-weight: 500;
padding: 6px 30px 6px 11px;
border: 1px solid var(--rule);
background: var(--white);
color: var(--ink);
border-radius: var(--r-sm);
cursor: pointer;
appearance: none;
background-image: linear-gradient(45deg, transparent 50%, var(--ink-3) 50%), linear-gradient(135deg, var(--ink-3) 50%, transparent 50%);
background-position: calc(100% - 16px) center, calc(100% - 11px) center;
background-size: 5px 5px, 5px 5px;
background-repeat: no-repeat;
}
.select:focus-visible { outline: 2px solid var(--red); outline-offset: 1px; }
/* ===== Queue table ===== */
.table-wrap { overflow-x: auto; }
.queue {
width: 100%;
border-collapse: collapse;
font-size: 13.5px;
}
.queue thead th {
text-align: left;
font-size: 10px;
letter-spacing: 0.13em;
text-transform: uppercase;
color: var(--muted);
font-weight: 700;
padding: 8px 14px;
border-bottom: 1px solid var(--rule);
background: var(--newsprint);
white-space: nowrap;
}
.queue tbody tr {
border-bottom: 1px solid var(--rule-hair);
transition: background 0.12s ease;
}
.queue tbody tr:hover { background: var(--newsprint); }
.queue tbody tr:last-child { border-bottom: 0; }
.queue td { padding: 12px 14px; vertical-align: middle; }
.story-head {
font-family: var(--serif);
font-weight: 700;
font-size: 15px;
line-height: 1.2;
color: var(--ink);
}
.story-head .breaking-tag {
display: inline-block;
font-family: var(--sans);
font-size: 9px;
font-weight: 700;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--white);
background: var(--red);
padding: 1px 5px;
border-radius: 2px;
margin-right: 7px;
vertical-align: middle;
}
.story-sub { font-size: 11.5px; color: var(--muted); margin-top: 3px; }
.author-cell { display: flex; flex-direction: column; line-height: 1.25; }
.author-name { font-weight: 600; font-size: 13px; }
.author-desk {
font-size: 10px;
letter-spacing: 0.08em;
text-transform: uppercase;
color: var(--muted);
margin-top: 2px;
}
.status-pill {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 11px;
font-weight: 600;
letter-spacing: 0.04em;
text-transform: uppercase;
padding: 3px 9px;
border-radius: 999px;
border: 1px solid transparent;
white-space: nowrap;
}
.status-pill::before {
content: "";
width: 6px; height: 6px;
border-radius: 50%;
background: currentColor;
}
.status-pill[data-s="draft"] { color: var(--ink-3); background: var(--newsprint); border-color: var(--rule); }
.status-pill[data-s="review"] { color: var(--warn); background: #f5ead4; border-color: #e6d3a8; }
.status-pill[data-s="scheduled"] { color: #2f5d8f; background: #dde8f3; border-color: #c2d6ea; }
.status-pill[data-s="published"] { color: var(--ok); background: #dcefe2; border-color: #bfe0cc; }
.time-cell { font-size: 12.5px; color: var(--ink-2); font-variant-numeric: tabular-nums; white-space: nowrap; }
.time-cell .time-rel { display: block; font-size: 10.5px; color: var(--muted); margin-top: 2px; }
.row-actions { display: flex; gap: 6px; justify-content: flex-end; }
.act {
font-family: var(--sans);
font-size: 11.5px;
font-weight: 600;
padding: 4px 10px;
border: 1px solid var(--rule);
background: var(--white);
color: var(--ink-2);
border-radius: var(--r-sm);
cursor: pointer;
white-space: nowrap;
transition: all 0.12s ease;
}
.act:hover { border-color: var(--ink); background: var(--newsprint); }
.act:focus-visible { outline: 2px solid var(--red); outline-offset: 1px; }
.act--advance { color: var(--red-d); border-color: var(--red-50); }
.act--advance:hover { background: var(--red); color: var(--white); border-color: var(--red); }
.act:disabled { opacity: 0.4; cursor: default; }
.act:disabled:hover { border-color: var(--rule); background: var(--white); color: var(--ink-2); }
.empty {
padding: 30px 18px;
text-align: center;
color: var(--muted);
font-style: italic;
font-family: var(--serif);
font-size: 15px;
}
/* ====================== RAIL ====================== */
.rail { display: flex; flex-direction: column; gap: 26px; }
.pill {
font-size: 10px;
font-weight: 700;
letter-spacing: 0.1em;
text-transform: uppercase;
padding: 3px 9px;
border-radius: 2px;
}
.pill--breaking { background: var(--red); color: var(--white); animation: pulse 2.2s ease-in-out infinite; }
.pill--soft { background: var(--newsprint); color: var(--ink-3); }
@keyframes pulse {
0%, 100% { box-shadow: 0 0 0 0 rgba(180, 41, 31, 0.4); }
50% { box-shadow: 0 0 0 5px rgba(180, 41, 31, 0); }
}
/* Press photos (no network) */
.attn-figure { margin: 0; padding: 14px 18px 0; }
.press-photo {
aspect-ratio: 16 / 9;
border-radius: var(--r-sm);
border: 1px solid var(--rule);
}
.press-photo--breaking {
background:
radial-gradient(120% 90% at 78% 18%, rgba(180, 41, 31, 0.42) 0%, transparent 46%),
radial-gradient(80% 60% at 22% 80%, rgba(43, 38, 32, 0.55) 0%, transparent 55%),
linear-gradient(165deg, #3a3027 0%, #564636 30%, #2b2620 62%, #15110d 100%);
}
.attn-figure figcaption {
display: flex;
justify-content: space-between;
gap: 10px;
font-size: 11px;
color: var(--muted);
margin-top: 7px;
padding-bottom: 4px;
border-bottom: 1px solid var(--rule-hair);
}
.attn-figure em { color: var(--ink-3); font-style: italic; }
.attn-figure .credit {
font-size: 9.5px;
letter-spacing: 0.1em;
text-transform: uppercase;
flex-shrink: 0;
}
.attn-list { list-style: none; margin: 4px 0 0; padding: 8px 0 6px; }
.attn-item {
display: flex;
flex-direction: column;
gap: 5px;
padding: 12px 18px;
border-bottom: 1px solid var(--rule-hair);
}
.attn-item:last-child { border-bottom: 0; }
.attn-top { display: flex; align-items: center; gap: 8px; }
.attn-flag {
font-size: 9px;
font-weight: 700;
letter-spacing: 0.1em;
text-transform: uppercase;
padding: 2px 6px;
border-radius: 2px;
}
.attn-flag--breaking { background: var(--red); color: var(--white); }
.attn-flag--review { background: #f5ead4; color: var(--warn); border: 1px solid #e6d3a8; }
.attn-time { margin-left: auto; font-size: 11px; color: var(--muted); font-variant-numeric: tabular-nums; }
.attn-title {
font-family: var(--serif);
font-weight: 700;
font-size: 15px;
line-height: 1.2;
color: var(--ink);
}
.attn-meta { font-size: 11.5px; color: var(--muted); }
.attn-cta {
align-self: flex-start;
margin-top: 3px;
font-size: 11.5px;
font-weight: 600;
color: var(--red-d);
background: none;
border: 0;
border-bottom: 1px solid var(--red-50);
padding: 1px 0;
cursor: pointer;
}
.attn-cta:hover { color: var(--red); border-color: var(--red); }
.attn-cta:focus-visible { outline: 2px solid var(--red); outline-offset: 2px; }
/* Pull quote */
.pull {
margin: 0;
padding: 16px 18px 14px;
border-bottom: 1px solid var(--rule-hair);
}
.pull p {
font-family: var(--serif);
font-weight: 600;
font-style: italic;
font-size: 19px;
line-height: 1.28;
color: var(--ink);
margin: 0 0 8px;
text-indent: -0.4em;
}
.pull cite { font-style: normal; font-size: 11.5px; font-weight: 600; color: var(--muted); }
/* Activity feed */
.feed { list-style: none; margin: 0; padding: 6px 0; counter-reset: none; }
.feed-item {
position: relative;
display: grid;
grid-template-columns: 18px 1fr;
gap: 10px;
padding: 11px 18px;
border-bottom: 1px solid var(--rule-hair);
}
.feed-item:last-child { border-bottom: 0; }
.feed-dot {
width: 9px; height: 9px;
border-radius: 50%;
margin-top: 5px;
background: var(--muted);
margin-left: 4px;
}
.feed-dot--red { background: var(--red); }
.feed-dot--ok { background: var(--ok); }
.feed-dot--warn { background: var(--warn); }
.feed-body { line-height: 1.35; }
.feed-text { font-size: 12.5px; color: var(--ink-2); }
.feed-text strong { font-weight: 600; color: var(--ink); }
.feed-time { font-size: 10.5px; color: var(--muted); margin-top: 2px; letter-spacing: 0.02em; }
/* ====================== FOOTER ====================== */
.foot {
display: flex;
align-items: center;
gap: 14px;
margin-top: 26px;
padding-top: 14px;
border-top: 2px solid var(--ink);
font-size: 11px;
letter-spacing: 0.04em;
color: var(--muted);
text-transform: uppercase;
}
.foot-rule { flex: 1; height: 1px; background: var(--rule-hair); }
/* ====================== TOAST ====================== */
.toast {
position: fixed;
left: 50%;
bottom: 26px;
transform: translate(-50%, 16px);
background: var(--ink);
color: var(--paper);
font-size: 13px;
font-weight: 500;
padding: 11px 18px;
border-radius: var(--r-sm);
border-left: 3px solid var(--red);
box-shadow: 0 6px 22px rgba(22, 19, 15, 0.22);
opacity: 0;
pointer-events: none;
transition: opacity 0.22s ease, transform 0.22s ease;
z-index: 60;
max-width: 90vw;
}
.toast.is-on { opacity: 1; transform: translate(-50%, 0); }
/* ====================== RESPONSIVE ====================== */
@media (max-width: 1080px) {
.grid { grid-template-columns: 1fr; }
.kpis { grid-template-columns: repeat(3, 1fr); }
.kpi:nth-child(3) { border-right: 0; }
.kpi:nth-child(4), .kpi:nth-child(5) { border-top: 1px solid var(--rule-hair); }
}
@media (max-width: 860px) {
.app { grid-template-columns: 1fr; }
.sidebar {
position: static;
height: auto;
flex-direction: row;
flex-wrap: wrap;
align-items: center;
gap: 10px 18px;
}
.brand { border-bottom: 0; padding-bottom: 0; margin-bottom: 0; }
.nav { flex-direction: row; flex-wrap: wrap; gap: 4px; flex-basis: 100%; }
.nav-label { display: none; }
.nav-item { padding: 6px 10px; }
.sidebar-foot { display: none; }
}
@media (max-width: 720px) {
.main { padding: 20px 16px 32px; }
.topbar { flex-direction: column; align-items: flex-start; gap: 14px; }
.topbar-right { flex-wrap: wrap; }
.search input { width: 130px; }
.kpis { grid-template-columns: repeat(2, 1fr); }
.kpi { border-right: 1px solid var(--rule-hair); border-top: 1px solid var(--rule-hair); }
.kpi:nth-child(odd) { border-left: 0; }
.kpi:nth-child(2n) { border-right: 0; }
.kpi:nth-child(1), .kpi:nth-child(2) { border-top: 0; }
}
@media (max-width: 480px) {
body { font-size: 14px; }
.kpis { grid-template-columns: 1fr; }
.kpi { border-right: 0; border-bottom: 1px solid var(--rule-hair); border-top: 0; }
.kpi:last-child { border-bottom: 0; }
.col-author, .author-cell { display: none; }
.queue td, .queue th { padding: 10px 10px; }
.topbar-right { width: 100%; }
.search { flex: 1; }
.search input { width: 100%; }
}
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after { animation-duration: 0.001ms !important; transition-duration: 0.001ms !important; }
}(function () {
"use strict";
/* ----------------------------- Data ----------------------------- */
// status order used for "advance": draft -> review -> scheduled -> published
var FLOW = ["draft", "review", "scheduled", "published"];
var STATUS_LABEL = {
draft: "Draft",
review: "In review",
scheduled: "Scheduled",
published: "Published",
};
var stories = [
{
id: "s1",
headline: "City Council clears funding for the harbour seawall",
sub: "Eleven-hour session ends with a 6–3 vote",
author: "Daniela Brun",
desk: "Politics",
status: "review",
time: "11:00",
rel: "in 1h 48m",
breaking: false,
},
{
id: "s2",
headline: "Storm surge swamps the lower harbour overnight",
sub: "Wire copy filed; awaiting confirmation on casualties",
author: "Marcus Okonkwo",
desk: "Politics",
status: "review",
time: "—",
rel: "needs sign-off",
breaking: true,
},
{
id: "s3",
headline: "Quarterly earnings surprise lifts Meridian Rail shares",
sub: "Up 9% in pre-market trading",
author: "Priya Anand",
desk: "Business",
status: "scheduled",
time: "12:30",
rel: "in 3h 18m",
breaking: false,
},
{
id: "s4",
headline: "Inside the warehouse turning yesterday’s bread into beer",
sub: "A feature on the city’s zero-waste brewers",
author: "Theo Vance",
desk: "Culture",
status: "draft",
time: "—",
rel: "no slot yet",
breaking: false,
},
{
id: "s5",
headline: "Astronomers trace a stray comet to the outer belt",
sub: "Findings published in this morning’s journal",
author: "Lena Castellanos",
desk: "Science",
status: "published",
time: "08:05",
rel: "1h 7m ago",
breaking: false,
},
{
id: "s6",
headline: "Meridian United edge the derby in stoppage time",
sub: "Match report and reaction",
author: "Owen Fitzgerald",
desk: "Sports",
status: "published",
time: "07:40",
rel: "1h 32m ago",
breaking: false,
},
{
id: "s7",
headline: "The mayor’s budget, line by line: where the money goes",
sub: "An interactive breakdown for subscribers",
author: "Daniela Brun",
desk: "Politics",
status: "scheduled",
time: "14:00",
rel: "in 4h 48m",
breaking: false,
},
{
id: "s8",
headline: "A small press bets the city still reads poetry",
sub: "Profile of the Lamplight imprint",
author: "Theo Vance",
desk: "Culture",
status: "draft",
time: "—",
rel: "no slot yet",
breaking: false,
},
{
id: "s9",
headline: "Regulators open inquiry into ferry operator’s safety record",
sub: "Documents obtained by the Ledger",
author: "Priya Anand",
desk: "Business",
status: "review",
time: "—",
rel: "needs legal read",
breaking: false,
},
{
id: "s10",
headline: "Lab-grown coral gives the bay’s reef a fighting chance",
sub: "Scientists report first signs of recovery",
author: "Lena Castellanos",
desk: "Science",
status: "scheduled",
time: "16:30",
rel: "in 7h 18m",
breaking: false,
},
{
id: "s11",
headline: "Veteran striker calls time on a sixteen-year career",
sub: "Exclusive sit-down interview",
author: "Owen Fitzgerald",
desk: "Sports",
status: "draft",
time: "—",
rel: "no slot yet",
breaking: false,
},
{
id: "s12",
headline: "Opinion: The seawall vote is only the first bill we’ll pay",
sub: "Editorial board",
author: "Rosalind Hale",
desk: "Politics",
status: "published",
time: "06:15",
rel: "2h 57m ago",
breaking: false,
},
{
id: "s13",
headline: "How the night markets reshaped the old textile quarter",
sub: "Photo essay",
author: "Theo Vance",
desk: "Culture",
status: "published",
time: "07:00",
rel: "2h 12m ago",
breaking: false,
},
{
id: "s14",
headline: "Rail unions and operator return to the table",
sub: "Talks resume after a week of silence",
author: "Priya Anand",
desk: "Business",
status: "scheduled",
time: "10:45",
rel: "in 1h 33m",
breaking: false,
},
];
var attention = [
{
kind: "breaking",
title: "Storm surge swamps the lower harbour overnight",
meta: "Marcus Okonkwo · Politics desk",
time: "08:54",
cta: "Open for review",
target: "s2",
},
{
kind: "review",
title: "Ferry operator inquiry — pending legal read",
meta: "Priya Anand · Business desk",
time: "08:31",
cta: "Send to copy",
target: "s9",
},
{
kind: "review",
title: "Seawall funding vote — fact-check the tally",
meta: "Daniela Brun · Politics desk",
time: "08:10",
cta: "Assign checker",
target: "s1",
},
];
var feed = [
{ dot: "ok", html: "<strong>Lena Castellanos</strong> published “Astronomers trace a stray comet to the outer belt.”", time: "08:05 · Science desk" },
{ dot: "red", html: "<strong>Marcus Okonkwo</strong> filed a breaking wire from the harbour district.", time: "08:54 · Politics desk" },
{ dot: "warn", html: "<strong>Copy desk</strong> flagged two figures for verification on the seawall story.", time: "08:22 · Standards" },
{ dot: "", html: "<strong>Priya Anand</strong> moved “Quarterly earnings surprise” to the noon slot.", time: "07:58 · Business desk" },
{ dot: "ok", html: "<strong>Owen Fitzgerald</strong> published the derby match report.", time: "07:40 · Sports desk" },
{ dot: "", html: "<strong>Rosalind Hale</strong> scheduled the budget breakdown for 14:00.", time: "07:31 · Editor" },
];
/* --------------------------- Helpers --------------------------- */
function $(sel, ctx) { return (ctx || document).querySelector(sel); }
function el(tag, cls, html) {
var n = document.createElement(tag);
if (cls) n.className = cls;
if (html != null) n.innerHTML = html;
return n;
}
function escapeHtml(s) {
return String(s).replace(/[&<>"]/g, function (c) {
return { "&": "&", "<": "<", ">": ">", '"': """ }[c];
});
}
var toastEl = $("#toast");
var toastTimer;
function toast(msg) {
toastEl.textContent = msg;
toastEl.classList.add("is-on");
clearTimeout(toastTimer);
toastTimer = setTimeout(function () {
toastEl.classList.remove("is-on");
}, 2600);
}
/* --------------------------- State ---------------------------- */
var filters = { status: "all", section: "all", query: "" };
/* ----------------------- Render: queue ------------------------ */
var queueBody = $("#queueBody");
var emptyState = $("#emptyState");
function matches(story) {
if (filters.status !== "all" && story.status !== filters.status) return false;
if (filters.section !== "all" && story.desk !== filters.section) return false;
if (filters.query) {
var hay = (story.headline + " " + story.author + " " + story.desk + " " + story.sub).toLowerCase();
if (hay.indexOf(filters.query) === -1) return false;
}
return true;
}
function renderQueue() {
queueBody.innerHTML = "";
var shown = 0;
stories.forEach(function (story) {
if (!matches(story)) return;
shown++;
var tr = el("tr");
tr.dataset.id = story.id;
// headline
var tdHead = el("td");
var breakingTag = story.breaking ? '<span class="breaking-tag">Breaking</span>' : "";
tdHead.innerHTML =
'<div class="story-head">' + breakingTag + escapeHtml(story.headline) + "</div>" +
'<div class="story-sub">' + escapeHtml(story.sub) + "</div>";
tr.appendChild(tdHead);
// author / desk
var tdAuthor = el("td", "col-author");
tdAuthor.innerHTML =
'<span class="author-cell">' +
'<span class="author-name">' + escapeHtml(story.author) + "</span>" +
'<span class="author-desk">' + escapeHtml(story.desk) + "</span></span>";
tr.appendChild(tdAuthor);
// status pill
var tdStatus = el("td", "col-status");
tdStatus.innerHTML =
'<span class="status-pill" data-s="' + story.status + '">' + STATUS_LABEL[story.status] + "</span>";
tr.appendChild(tdStatus);
// time
var tdTime = el("td", "col-time");
tdTime.innerHTML =
'<span class="time-cell">' + escapeHtml(story.time) +
'<span class="time-rel">' + escapeHtml(story.rel) + "</span></span>";
tr.appendChild(tdTime);
// actions
var tdAct = el("td", "col-act");
var isLast = story.status === "published";
var nextLabel = isLast ? "Published" : "Advance →";
tdAct.innerHTML =
'<div class="row-actions">' +
'<button class="act" type="button" data-action="open">Open</button>' +
'<button class="act act--advance" type="button" data-action="advance"' +
(isLast ? " disabled" : "") + ">" + nextLabel + "</button>" +
"</div>";
tr.appendChild(tdAct);
queueBody.appendChild(tr);
});
emptyState.hidden = shown !== 0;
}
function advance(story, rowEl) {
var idx = FLOW.indexOf(story.status);
if (idx === -1 || idx >= FLOW.length - 1) return;
story.status = FLOW[idx + 1];
if (story.status === "published") {
story.time = nowHM();
story.rel = "just now";
story.breaking = false;
} else if (story.status === "scheduled" && story.time === "—") {
story.time = "soon";
story.rel = "slot pending";
}
toast("Moved to " + STATUS_LABEL[story.status] + ": “" + truncate(story.headline, 42) + "”");
renderQueue();
flashRow(story.id);
pushActivity(story);
bumpKpisForStatus(story.status);
}
function flashRow(id) {
var row = queueBody.querySelector('tr[data-id="' + id + '"]');
if (!row) return;
row.animate(
[{ background: "rgba(180,41,31,0.14)" }, { background: "transparent" }],
{ duration: 700, easing: "ease-out" }
);
}
function truncate(s, n) { return s.length > n ? s.slice(0, n - 1) + "…" : s; }
function nowHM() {
var d = new Date();
return ("0" + d.getHours()).slice(-2) + ":" + ("0" + d.getMinutes()).slice(-2);
}
// delegated clicks on queue
queueBody.addEventListener("click", function (e) {
var btn = e.target.closest("button.act");
if (!btn) return;
var row = e.target.closest("tr");
var story = stories.filter(function (s) { return s.id === row.dataset.id; })[0];
if (!story) return;
if (btn.dataset.action === "advance") {
advance(story, row);
} else if (btn.dataset.action === "open") {
toast("Opening “" + truncate(story.headline, 46) + "” in the editor…");
}
});
/* --------------------- Render: attention ---------------------- */
function renderAttention() {
var list = $("#attnList");
list.innerHTML = "";
attention.forEach(function (a) {
var li = el("li", "attn-item");
var flagCls = a.kind === "breaking" ? "attn-flag--breaking" : "attn-flag--review";
var flagText = a.kind === "breaking" ? "Breaking" : "Awaiting review";
li.innerHTML =
'<div class="attn-top">' +
'<span class="attn-flag ' + flagCls + '">' + flagText + "</span>" +
'<span class="attn-time">' + a.time + "</span></div>" +
'<div class="attn-title">' + escapeHtml(a.title) + "</div>" +
'<div class="attn-meta">' + escapeHtml(a.meta) + "</div>" +
'<button class="attn-cta" type="button" data-target="' + a.target + '">' + a.cta + "</button>";
list.appendChild(li);
});
list.addEventListener("click", function (e) {
var cta = e.target.closest(".attn-cta");
if (!cta) return;
var story = stories.filter(function (s) { return s.id === cta.dataset.target; })[0];
if (story) {
// surface the story in the queue and advance it
setStatusFilter("all");
$("#sectionFilter").value = "all";
filters.section = "all";
advance(story, null);
var row = queueBody.querySelector('tr[data-id="' + story.id + '"]');
if (row) row.scrollIntoView({ behavior: "smooth", block: "center" });
} else {
toast("Handled — item cleared from the desk.");
}
});
}
/* ----------------------- Render: feed ------------------------- */
function renderFeed() {
var list = $("#feedList");
list.innerHTML = "";
feed.forEach(function (f) { list.appendChild(feedNode(f)); });
}
function feedNode(f) {
var li = el("li", "feed-item");
var dotCls = "feed-dot" + (f.dot ? " feed-dot--" + f.dot : "");
li.innerHTML =
'<span class="' + dotCls + '" aria-hidden="true"></span>' +
'<div class="feed-body"><div class="feed-text">' + f.html + "</div>" +
'<div class="feed-time">' + f.time + "</div></div>";
return li;
}
function pushActivity(story) {
var dot = story.status === "published" ? "ok" : story.status === "review" ? "warn" : "";
var verb = {
review: "sent to review",
scheduled: "scheduled",
published: "published",
}[story.status] || "updated";
var node = feedNode({
dot: dot,
html: "<strong>You</strong> " + verb + " “" + escapeHtml(truncate(story.headline, 48)) + ".”",
time: nowHM() + " · " + story.desk + " desk",
});
var list = $("#feedList");
list.insertBefore(node, list.firstChild);
node.animate(
[{ opacity: 0, transform: "translateY(-6px)" }, { opacity: 1, transform: "translateY(0)" }],
{ duration: 320, easing: "ease-out" }
);
}
/* --------------------------- KPIs ----------------------------- */
function formatNum(n, compact) {
if (compact && n >= 1000) {
return (n / 1000).toFixed(n >= 10000 ? 0 : 1).replace(/\.0$/, "") + "K";
}
return n.toLocaleString("en-US");
}
function animateKpi(node) {
var target = parseInt(node.getAttribute("data-count"), 10) || 0;
var compact = node.getAttribute("data-format") === "compact";
var reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
if (reduce) {
node.textContent = formatNum(target, compact);
return;
}
var dur = 1100;
var start = null;
function step(ts) {
if (start === null) start = ts;
var p = Math.min((ts - start) / dur, 1);
var eased = 1 - Math.pow(1 - p, 3); // easeOutCubic
node.textContent = formatNum(Math.round(target * eased), compact);
if (p < 1) requestAnimationFrame(step);
else node.textContent = formatNum(target, compact);
}
requestAnimationFrame(step);
}
function animateAllKpis() {
document.querySelectorAll(".kpi-num").forEach(animateKpi);
}
// map status changes to a KPI label so the strip stays believable
function bumpKpisForStatus(status) {
var labelMap = {
review: "In review",
scheduled: "Scheduled",
published: "Published today",
};
var label = labelMap[status];
if (!label) return;
document.querySelectorAll(".kpi").forEach(function (kpi) {
var l = kpi.querySelector(".kpi-label");
if (l && l.textContent.trim() === label) {
var num = kpi.querySelector(".kpi-num");
var cur = parseInt(num.getAttribute("data-count"), 10) || 0;
num.setAttribute("data-count", cur + 1);
animateKpi(num);
kpi.animate(
[{ background: "rgba(180,41,31,0.10)" }, { background: getComputedStyle(kpi).backgroundColor }],
{ duration: 600, easing: "ease-out" }
);
}
});
}
/* ------------------------- Filters UI ------------------------- */
function setStatusFilter(status) {
filters.status = status;
document.querySelectorAll('[data-filter-group="status"] .chip').forEach(function (chip) {
var on = chip.dataset.status === status;
chip.classList.toggle("is-on", on);
chip.setAttribute("aria-pressed", on ? "true" : "false");
});
renderQueue();
}
document.querySelectorAll('[data-filter-group="status"] .chip').forEach(function (chip) {
chip.addEventListener("click", function () {
setStatusFilter(chip.dataset.status);
});
});
$("#sectionFilter").addEventListener("change", function (e) {
filters.section = e.target.value;
renderQueue();
});
$("#globalSearch").addEventListener("input", function (e) {
filters.query = e.target.value.trim().toLowerCase();
renderQueue();
});
/* ----------------------- Topbar buttons ----------------------- */
$("#newStoryBtn").addEventListener("click", function () {
var draft = {
id: "s" + (stories.length + 1) + "-" + Date.now(),
headline: "Untitled story — assign a desk to begin",
sub: "New draft · started just now",
author: "Rosalind Hale",
desk: "Politics",
status: "draft",
time: "—",
rel: "no slot yet",
breaking: false,
};
stories.unshift(draft);
setStatusFilter("all");
$("#sectionFilter").value = "all";
filters.section = "all";
filters.query = "";
$("#globalSearch").value = "";
renderQueue();
flashRow(draft.id);
// bump the "In draft" KPI to reflect the new item entering production
document.querySelectorAll(".kpi").forEach(function (kpi) {
var l = kpi.querySelector(".kpi-label");
if (l && l.textContent.trim() === "In draft") {
var num = kpi.querySelector(".kpi-num");
num.setAttribute("data-count", (parseInt(num.getAttribute("data-count"), 10) || 0) + 1);
animateKpi(num);
}
});
toast("New draft created — top of the queue.");
});
$("#refreshBtn").addEventListener("click", function () {
animateAllKpis();
toast("Feed refreshed · " + nowHM());
});
/* ------------------------- Init ------------------------------- */
renderQueue();
renderAttention();
renderFeed();
// animate KPIs once the strip is in view (or immediately if already visible)
var kpiStrip = $(".kpis");
if ("IntersectionObserver" in window) {
var io = new IntersectionObserver(function (entries, obs) {
entries.forEach(function (entry) {
if (entry.isIntersecting) {
animateAllKpis();
obs.disconnect();
}
});
}, { threshold: 0.3 });
io.observe(kpiStrip);
} else {
animateAllKpis();
}
})();<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>The Meridian Ledger — Newsroom 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=Playfair+Display:wght@500;600;700;800;900&family=Inter:wght@400;500;600;700&display=swap"
rel="stylesheet"
/>
<link rel="stylesheet" href="style.css" />
</head>
<body>
<div class="app">
<!-- ============ SIDEBAR ============ -->
<aside class="sidebar" aria-label="Primary">
<div class="brand">
<div class="brand-mark" aria-hidden="true">ML</div>
<div class="brand-text">
<span class="brand-name">The Meridian Ledger</span>
<span class="brand-sub">Newsroom CMS</span>
</div>
</div>
<nav class="nav" aria-label="Sections">
<span class="nav-label">Newsroom</span>
<a href="#" class="nav-item is-active" aria-current="page">
<span class="nav-ico" aria-hidden="true">◧</span> Dashboard
</a>
<a href="#" class="nav-item">
<span class="nav-ico" aria-hidden="true">▤</span> Publishing queue
<span class="nav-badge">14</span>
</a>
<a href="#" class="nav-item">
<span class="nav-ico" aria-hidden="true">✎</span> Drafts
</a>
<a href="#" class="nav-item">
<span class="nav-ico" aria-hidden="true">⚑</span> Needs attention
<span class="nav-badge nav-badge--red">3</span>
</a>
<span class="nav-label">Desks</span>
<a href="#" class="nav-item"><span class="nav-ico" aria-hidden="true">◆</span> Politics</a>
<a href="#" class="nav-item"><span class="nav-ico" aria-hidden="true">◆</span> Business</a>
<a href="#" class="nav-item"><span class="nav-ico" aria-hidden="true">◆</span> Culture</a>
<a href="#" class="nav-item"><span class="nav-ico" aria-hidden="true">◆</span> Science</a>
<a href="#" class="nav-item"><span class="nav-ico" aria-hidden="true">◆</span> Sports</a>
<span class="nav-label">Tools</span>
<a href="#" class="nav-item"><span class="nav-ico" aria-hidden="true">◷</span> Schedule</a>
<a href="#" class="nav-item"><span class="nav-ico" aria-hidden="true">▦</span> Analytics</a>
<a href="#" class="nav-item"><span class="nav-ico" aria-hidden="true">⚙</span> Settings</a>
</nav>
<div class="sidebar-foot">
<div class="editor-card">
<div class="editor-avatar" aria-hidden="true">RH</div>
<div class="editor-meta">
<span class="editor-name">Rosalind Hale</span>
<span class="editor-role">Managing Editor</span>
</div>
</div>
</div>
</aside>
<!-- ============ MAIN ============ -->
<div class="main">
<header class="topbar">
<div class="topbar-left">
<p class="kicker">Tuesday Edition · Late City</p>
<h1 class="topbar-title">Newsroom Dashboard</h1>
<p class="dateline">Updated <time datetime="2026-06-08T09:12">09:12 EDT</time> · June 8, 2026</p>
</div>
<div class="topbar-right">
<div class="search" role="search">
<span class="search-ico" aria-hidden="true">⌕</span>
<input type="search" id="globalSearch" placeholder="Search stories, authors…" aria-label="Search the newsroom" />
</div>
<button class="btn btn--ghost" type="button" id="refreshBtn">Refresh feed</button>
<button class="btn btn--red" type="button" id="newStoryBtn">+ New story</button>
</div>
</header>
<!-- ===== KPI STRIP ===== -->
<section class="kpis" aria-label="Newsroom metrics">
<article class="kpi">
<span class="kpi-label">In draft</span>
<strong class="kpi-num" data-count="38">0</strong>
<span class="kpi-foot">across 5 desks</span>
</article>
<article class="kpi">
<span class="kpi-label">In review</span>
<strong class="kpi-num" data-count="11">0</strong>
<span class="kpi-foot kpi-foot--warn">4 awaiting copy desk</span>
</article>
<article class="kpi">
<span class="kpi-label">Scheduled</span>
<strong class="kpi-num" data-count="9">0</strong>
<span class="kpi-foot">next at 11:00</span>
</article>
<article class="kpi">
<span class="kpi-label">Published today</span>
<strong class="kpi-num" data-count="27">0</strong>
<span class="kpi-foot kpi-foot--ok">+6 vs. yesterday</span>
</article>
<article class="kpi kpi--accent">
<span class="kpi-label">Page views · today</span>
<strong class="kpi-num" data-count="184320" data-format="compact">0</strong>
<span class="kpi-foot kpi-foot--ok">▲ 12.4% on the week</span>
</article>
</section>
<div class="grid">
<!-- ===== QUEUE ===== -->
<section class="panel panel--queue" aria-labelledby="queueHead">
<div class="panel-head">
<div>
<h2 class="panel-title" id="queueHead">Publishing queue</h2>
<p class="panel-sub">14 stories moving through production</p>
</div>
</div>
<div class="filters" role="group" aria-label="Filter the queue">
<div class="filter-row">
<span class="filter-cap">Status</span>
<div class="chips" data-filter-group="status">
<button class="chip is-on" data-status="all" aria-pressed="true">All</button>
<button class="chip" data-status="draft" aria-pressed="false">Draft</button>
<button class="chip" data-status="review" aria-pressed="false">In review</button>
<button class="chip" data-status="scheduled" aria-pressed="false">Scheduled</button>
<button class="chip" data-status="published" aria-pressed="false">Published</button>
</div>
</div>
<div class="filter-row">
<span class="filter-cap">Desk</span>
<label class="visually-hidden" for="sectionFilter">Filter by desk</label>
<select id="sectionFilter" class="select">
<option value="all">All desks</option>
<option value="Politics">Politics</option>
<option value="Business">Business</option>
<option value="Culture">Culture</option>
<option value="Science">Science</option>
<option value="Sports">Sports</option>
</select>
</div>
</div>
<div class="table-wrap">
<table class="queue" aria-describedby="queueHead">
<thead>
<tr>
<th scope="col" class="col-head">Headline</th>
<th scope="col" class="col-author">Author · Desk</th>
<th scope="col" class="col-status">Status</th>
<th scope="col" class="col-time">Schedule</th>
<th scope="col" class="col-act"><span class="visually-hidden">Actions</span></th>
</tr>
</thead>
<tbody id="queueBody"><!-- rows injected by script.js --></tbody>
</table>
<p class="empty" id="emptyState" hidden>No stories match these filters.</p>
</div>
</section>
<!-- ===== SIDE RAIL ===== -->
<div class="rail">
<!-- Needs attention -->
<section class="panel panel--attention" aria-labelledby="attnHead">
<div class="panel-head">
<h2 class="panel-title" id="attnHead">Needs attention</h2>
<span class="pill pill--breaking">Live</span>
</div>
<figure class="attn-figure">
<div class="press-photo press-photo--breaking" role="img" aria-label="Wire photo: harbour district at dusk under storm light"></div>
<figcaption>
<em>Storm light over the harbour district, filed 08:54.</em>
<span class="credit">Wire / M. Okonkwo</span>
</figcaption>
</figure>
<ul class="attn-list" id="attnList"><!-- injected --></ul>
</section>
<!-- Activity feed -->
<section class="panel panel--activity" aria-labelledby="actHead">
<div class="panel-head">
<h2 class="panel-title" id="actHead">Activity</h2>
<span class="pill pill--soft">Newsroom</span>
</div>
<blockquote class="pull">
<p>“We don’t publish to be first. We publish to be right, then we’re fast about it.”</p>
<cite>— Rosalind Hale, Managing Editor</cite>
</blockquote>
<ol class="feed" id="feedList"><!-- injected --></ol>
</section>
</div>
</div>
<footer class="foot">
<span>The Meridian Ledger · Newsroom CMS — illustrative interface, fictional content.</span>
<span class="foot-rule" aria-hidden="true"></span>
<span>Composed in EDT · Late City run</span>
</footer>
</div>
</div>
<div class="toast" id="toast" role="status" aria-live="polite"></div>
<script src="script.js"></script>
</body>
</html>Newsroom Dashboard
A production dashboard for the late-city desk of the fictional Meridian Ledger, art-directed like the paper it serves: a cream newsprint background, Playfair Display mastheads over Inter UI, thin hairline rules instead of heavy shadows, and a single accent red reserved for kickers, breaking tags and links. A fixed sidebar carries the brand mark, section nav with live badges, and an editor card; the topbar pairs an oversized serif title with a dateline and a global search.
The five-tile KPI strip — in draft, in review, scheduled, published today, and page views — counts up the first time it scrolls into view, with tabular figures so the numbers stay aligned. Below it, the publishing queue lists each story with its headline and standfirst, author and desk, a colour- coded status pill, and a schedule slot with a relative time. Filter chips and a desk dropdown narrow the table instantly, the search box matches across headlines and bylines, and an Advance button walks a story from draft to review to scheduled to published — flashing the row, bumping the matching counter and writing a line into the activity feed. A needs-attention rail flags breaking and awaiting- review items beside a duotone-ink press photo with a caption and credit, and a pull quote anchors the feed.
Everything is vanilla JavaScript with no libraries: an IntersectionObserver drives the count-up,
the Web Animations API handles the row and KPI flashes, and a small toast() helper confirms each
action. The layout uses a strict two-column grid that collapses to a single column under ~1080px and
reflows the sidebar and KPI tiles down to ~360px.
Illustrative UI only — masthead, headlines, bylines, and articles are fictional; not a real news publication.