Stream the page shell first so a loader shows while feeds fetch

Opening the page used to block on refreshStale() before a single byte went
out, so the browser sat on a blank tab for the whole fetch. index.php now
renders templates/shell.php (head, header, filters, skeleton loader) and
flushes it, then refreshes and appends templates/content.php, which removes
the loader as the feed HTML arrives.

- templates/layout.php is split into shell.php + content.php
- refresh chip moved to a right-aligned row above the feed: the toolbar
  paints before the refresh report exists
- header story count shows the cached total, then corrects to the real one
- skeleton styles match the card grid and keep the sources' chroma; a
  noscript style hides the loader for clients without JS
- README: file map and page-load lifecycle updated
This commit is contained in:
Harwinder Sekhon
2026-10-08 15:28:51 -04:00
parent 20f4fd7095
commit 3c2d90fb7e
5 changed files with 216 additions and 74 deletions
+59
View File
@@ -399,6 +399,65 @@ main {
font-family: var(--mono);
}
/* ---------- Feed loader ---------- */
/* Skeleton cards hold the feed's place while the first page is fetched. The
spine stays neutral: no source colour is known yet, so the sources keep
their chroma to themselves. */
.skeleton-card {
--src: var(--line-2);
pointer-events: none;
}
.skeleton-thumb,
.skeleton-chip,
.skeleton-line {
display: block;
background: linear-gradient(90deg, var(--surface-2) 25%, var(--line-2) 37%, var(--surface-2) 63%);
background-size: 400% 100%;
border-radius: 6px;
animation: skeleton-sweep 1.5s ease-in-out infinite;
}
.skeleton-thumb {
width: 100%;
aspect-ratio: 16 / 9;
border-radius: 0;
}
.skeleton-chip {
width: 46px;
height: 18px;
border-radius: 999px;
flex-shrink: 0;
}
.skeleton-line {
height: 0.62rem;
margin-bottom: 0.5rem;
}
.skeleton-title {
height: 1.02rem;
margin-bottom: 0.75rem;
}
.skeleton-timestamp {
width: 3.25rem;
height: 0.72rem;
margin-bottom: 0;
}
@keyframes skeleton-sweep {
from {
background-position: 100% 50%;
}
to {
background-position: 0 50%;
}
}
/* ---------- Empty state ---------- */
.empty-state {