/news: surface Saved + Boundaries in the view-head (don't bury them in Account)

Per Codex: HubBar stays purely site-level, so the feed's own utilities live with
the feed. Beside the existing Search toggle (hub chrome only, so `/`'s Header
keeps its own — no duplication): a Saved button (opens the existing flyout) and a
Boundaries/Tune control with a visible active indicator (links to its account
section for now). Same pill styling as Search.

Also flagged the Back-condition trap in-code: once bare /news becomes Latest,
Back must be suppressed for 'latest' too (only genuine drill-ins show it) — to be
fixed at the behavior split, not now (would alter the frozen `/`).

32 tests green; build clean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
jay
2026-06-28 15:42:40 -04:00
parent e974fc4942
commit 036e7ed7e8
@@ -666,12 +666,29 @@
<button class="searchtoggle" class:on={searchOpen || selected === 'search'} onclick={toggleSearch} aria-label="Search articles" title="Search articles"> <button class="searchtoggle" class:on={searchOpen || selected === 'search'} onclick={toggleSearch} aria-label="Search articles" title="Search articles">
<svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="11" cy="11" r="7" fill="none" stroke="currentColor" stroke-width="2"/><path d="M21 21l-4.4-4.4" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg> <svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="11" cy="11" r="7" fill="none" stroke="currentColor" stroke-width="2"/><path d="M21 21l-4.4-4.4" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>
</button> </button>
<!-- News-local utilities: HubBar stays purely site-level, so Saved + Boundaries
live here (where the legacy Header carried them) instead of being buried in Account. -->
{#if chrome === 'hub'}
{#if auth.user}
<button class="vh-util" onclick={() => (showSaved = true)} aria-label="Saved articles" title="Saved articles">
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M6 3h12v18l-6-4-6 4z" fill="none" stroke="currentColor" stroke-width="1.9" stroke-linejoin="round"/></svg>
</button>
{/if}
<a class="vh-util" class:on={filtersOn} href="/account?section=boundaries"
aria-label={filtersOn ? 'Boundaries are on' : 'Your boundaries'} title={filtersOn ? 'Boundaries are on' : 'Your boundaries'}>
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M12 3l7 3v5c0 4.4-3 7.6-7 9-4-1.4-7-4.6-7-9V6l7-3z" fill={filtersOn ? 'currentColor' : 'none'} stroke="currentColor" stroke-width="1.8" stroke-linejoin="round"/></svg>
</a>
{/if}
{#if auth.user && followTarget} {#if auth.user && followTarget}
<button class="followbtn" class:on={isFollowing(followTarget.kind, followTarget.value)} <button class="followbtn" class:on={isFollowing(followTarget.kind, followTarget.value)}
onclick={() => toggleFollow(followTarget.kind, followTarget.value)}> onclick={() => toggleFollow(followTarget.kind, followTarget.value)}>
{isFollowing(followTarget.kind, followTarget.value) ? '✓ Following' : 'Follow ' + followTarget.noun} {isFollowing(followTarget.kind, followTarget.value) ? '✓ Following' : 'Follow ' + followTarget.noun}
</button> </button>
{/if} {/if}
<!-- TODO(latest-default): once bare /news becomes Latest, suppress Back for BOTH
'latest' and 'today'; show it only for genuine drill-ins (tag/source/search),
or Back lands on the top-level Latest page. Don't change this until then —
it would alter the frozen `/` feed. -->
{#if selected !== 'today'} {#if selected !== 'today'}
<button class="viewback" onclick={goBack} aria-label="Go back"> <button class="viewback" onclick={goBack} aria-label="Go back">
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M19 12H5M11 6l-6 6 6 6" fill="none" stroke="currentColor" stroke-width="2.1" stroke-linecap="round" stroke-linejoin="round"/></svg> <svg viewBox="0 0 24 24" aria-hidden="true"><path d="M19 12H5M11 6l-6 6 6 6" fill="none" stroke="currentColor" stroke-width="2.1" stroke-linecap="round" stroke-linejoin="round"/></svg>
@@ -908,6 +925,13 @@
.searchtoggle:hover { border-color: var(--accent); } .searchtoggle:hover { border-color: var(--accent); }
.searchtoggle.on { background: var(--accent); border-color: var(--accent); color: #fff; } .searchtoggle.on { background: var(--accent); border-color: var(--accent); color: #fff; }
.searchtoggle svg { width: 17px; height: 17px; display: block; } .searchtoggle svg { width: 17px; height: 17px; display: block; }
/* news-local Saved / Boundaries — same pill as the search toggle */
.vh-util { display: inline-flex; align-items: center; justify-content: center; width: 34px; height: 34px;
background: none; border: 1px solid var(--line); color: var(--accent-deep); border-radius: 999px;
cursor: pointer; text-decoration: none; transition: border-color 0.14s ease, background 0.14s ease; }
.vh-util:hover { border-color: var(--accent); }
.vh-util.on { background: var(--accent); border-color: var(--accent); color: #fff; }
.vh-util svg { width: 17px; height: 17px; display: block; }
.searchbar { display: flex; gap: 8px; margin: 0 0 18px; } .searchbar { display: flex; gap: 8px; margin: 0 0 18px; }
.searchbar input { flex: 1; min-width: 0; font: inherit; font-size: 1rem; padding: 10px 14px; .searchbar input { flex: 1; min-width: 0; font: inherit; font-size: 1rem; padding: 10px 14px;
border: 1px solid var(--line); border-radius: 10px; background: var(--surface); color: var(--ink); } border: 1px solid var(--line); border-radius: 10px; background: var(--surface); color: var(--ink); }