Add per-story views, trending, and reader reading-experience upgrades

Surface read counts and sharpen the reader:
- story_views table + RecordStoryView on /api/article (background,
  filter-guarded); "Popular this week" home rail via TrendingStories;
  read-count badge and reading-time chip decorated onto every listing
- reader: signed-in-only read-aloud (TTS), native share/copy, and an
  Aa typography popover (size/serif/sepia) persisted per device
- real alt text on card/reader/related/search images; time-of-day Pete
  greeting on the home hero
- harden exec() to skip (not panic) on a nil DB so background writes
  can't crash on a closed handle

Tests: story_views_test.go, trending_test.go. Suite green, CSS rebuilt.
This commit is contained in:
prosolis
2026-07-07 22:17:23 -07:00
parent 2ea5f7a6f7
commit 616055a704
14 changed files with 737 additions and 12 deletions

View File

@@ -232,6 +232,56 @@ html[data-phase="night"] {
.pete-reader-btn:disabled { opacity: 0.35; cursor: default; transform: none; }
.pete-reader-btn-open { background: var(--accent); border-color: transparent; color: #1c1305; text-decoration: none; }
.pete-reader-btn-open:hover { background: var(--accent); filter: brightness(1.08); }
/* Active/pressed toolbar toggle (Listen while speaking, Aa while menu open). */
.pete-reader-btn[aria-pressed="true"],
.pete-reader-btn[aria-expanded="true"] {
background: var(--accent);
border-color: transparent;
color: #1c1305;
}
/* Text-options popover, anchored under the Aa button in the reader bar. */
.pete-reader-type { position: relative; display: inline-flex; }
.pete-reader-typemenu {
position: absolute;
top: calc(100% + 0.5rem);
right: 0;
z-index: 10;
width: 15rem;
padding: 0.75rem;
border-radius: 1rem;
background: var(--card);
color: var(--ink);
border: 2px solid rgba(0, 0, 0, 0.08);
box-shadow: 0 10px 30px rgba(20, 14, 6, 0.28);
display: flex;
flex-direction: column;
gap: 0.6rem;
}
html[data-phase="night"] .pete-reader-typemenu { border-color: rgba(255, 255, 255, 0.12); }
.pete-reader-typerow { display: flex; align-items: center; justify-content: space-between; gap: 0.6rem; }
.pete-reader-typelabel { font-size: 0.8rem; font-weight: 700; opacity: 0.7; }
.pete-reader-typebtns { display: inline-flex; gap: 0.25rem; }
.pete-reader-typebtns button {
min-width: 2rem;
height: 2rem;
padding: 0 0.55rem;
border-radius: 0.6rem;
background: rgba(20, 14, 6, 0.06);
color: var(--ink);
font-weight: 700;
font-size: 0.82rem;
border: 2px solid transparent;
cursor: pointer;
transition: background 0.15s ease;
}
html[data-phase="night"] .pete-reader-typebtns button { background: rgba(255, 255, 255, 0.08); }
.pete-reader-typebtns button:hover { background: rgba(20, 14, 6, 0.12); }
.pete-reader-typebtns button.is-active {
background: var(--accent);
color: #1c1305;
border-color: transparent;
}
.pete-reader-scroll {
flex: 1 1 auto;
@@ -287,9 +337,33 @@ html[data-phase="night"] {
background: rgba(0, 0, 0, 0.05);
}
.pete-reader-body { font-size: 1.08rem; line-height: 1.75; }
/* Reader typography, driven by the Aa popover and persisted per-device. The
size lives as a CSS var on the scroll container; font + paper are classes. */
.pete-reader-scroll { --reader-fs: 1.08rem; }
.pete-reader-body { font-size: var(--reader-fs); line-height: 1.75; }
.pete-reader-body p { margin-bottom: 1.05em; }
.pete-reader-body p:last-child { margin-bottom: 0; }
.pete-reader-scroll.is-size-s { --reader-fs: 0.98rem; }
.pete-reader-scroll.is-size-m { --reader-fs: 1.08rem; }
.pete-reader-scroll.is-size-l { --reader-fs: 1.22rem; }
.pete-reader-scroll.is-size-xl { --reader-fs: 1.4rem; }
.pete-reader-scroll.is-serif .pete-reader-body {
font-family: Georgia, "Iowan Old Style", "Times New Roman", serif;
}
/* Sepia paper: warm background + ink regardless of day/night phase. */
.pete-reader-scroll.is-sepia .pete-reader-article {
background: #f6ecd6;
color: #4a3a28;
border-color: rgba(74, 58, 40, 0.16);
}
.pete-reader-scroll.is-sepia .pete-reader-note { border-top-color: rgba(74, 58, 40, 0.18); opacity: 0.8; }
.pete-reader-scroll.is-sepia .pete-reader-hero { background: rgba(74, 58, 40, 0.08); }
/* Currently-spoken paragraph highlight while reading aloud. */
.pete-reader-body p.is-speaking {
background: color-mix(in srgb, var(--accent) 32%, transparent);
border-radius: 0.4rem;
box-shadow: 0 0 0 0.35rem color-mix(in srgb, var(--accent) 32%, transparent);
}
.pete-reader-note {
margin-top: 1.25rem;
@@ -313,6 +387,26 @@ html[data-phase="night"] {
}
.pete-reader-empty-emoji { font-size: 2.5rem; }
/* Transient confirmation toast (e.g. "Link copied") shown over the reader. */
.pete-reader-toast {
position: absolute;
left: 50%;
bottom: 4.5rem;
transform: translate(-50%, 0.5rem);
z-index: 20;
padding: 0.5rem 0.9rem;
border-radius: 9999px;
background: rgba(20, 14, 6, 0.9);
color: #fff;
font-weight: 700;
font-size: 0.85rem;
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.3);
opacity: 0;
pointer-events: none;
transition: opacity 0.2s ease, transform 0.2s ease;
}
.pete-reader-toast.is-shown { opacity: 1; transform: translate(-50%, 0); }
.pete-reader-hint {
text-align: center;
color: rgba(255, 255, 255, 0.85);
@@ -328,6 +422,9 @@ html[data-phase="night"] {
@media (max-width: 640px) {
.pete-reader-hint { display: none; }
/* With Listen/Share/Aa added, let the toolbar wrap instead of overflowing. */
.pete-reader-bar { flex-wrap: wrap; }
.pete-reader-nav { flex-wrap: wrap; justify-content: flex-end; row-gap: 0.4rem; }
}
/* "You might also like" rail, shown under the article in feed mode. Lives

File diff suppressed because one or more lines are too long