Consolidate Boundaries + History under the account; sectioned /account
The inline Boundaries/History panels lived on the home page, so opening them while scrolled left you stranded. Move everything "yours" behind the account icon: - Home header slims to: Saved (opens a right-side flyout, signed-in) · shield (Boundaries indicator — filled when active — linking to the Boundaries section) · avatar. The inline panels + the home "saved" view are gone. - /account is now a sectioned hub (left sidebar on desktop, top tabs on mobile), OPEN TO EVERYONE with each section self-gating: Profile (sign-in), Saved (sign-in), History (device/account), Boundaries (device/account), Admin (admins). This keeps Boundaries/History usable without an account (they're device-local) while consolidating the UI — and every section loads at the top, fixing the scroll bug. - Lift Calm Filters and History into shared stores (prefs.svelte.js, history.svelte.js) so the home feed (applies/records) and the account page (edits/manages) share one source of truth. New SavedFlyout component. Card boundary actions only render when a handler is provided. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -155,9 +155,9 @@
|
||||
</div>
|
||||
{/if}
|
||||
</span>
|
||||
{#if article.topic}<button class="mute" onclick={() => act('notToday', article.topic)}>Not today</button>{/if}
|
||||
{#if article.flavor}<button class="mute" onclick={() => act('lessLikeThis', article.flavor)}>Less like this</button>{/if}
|
||||
{#if article.topic}<button class="mute" onclick={() => act('alwaysHide', article.topic)}>Hide {article.topic}</button>{/if}
|
||||
{#if onaction && article.topic}<button class="mute" onclick={() => act('notToday', article.topic)}>Not today</button>{/if}
|
||||
{#if onaction && article.flavor}<button class="mute" onclick={() => act('lessLikeThis', article.flavor)}>Less like this</button>{/if}
|
||||
{#if onaction && article.topic}<button class="mute" onclick={() => act('alwaysHide', article.topic)}>Hide {article.topic}</button>{/if}
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
|
||||
@@ -42,9 +42,9 @@
|
||||
<section class="panel rise">
|
||||
<div class="head">
|
||||
<h2>Your boundaries</h2>
|
||||
<button class="close" onclick={() => onclose?.()} aria-label="close">done</button>
|
||||
{#if onclose}<button class="close" onclick={() => onclose?.()} aria-label="close">done</button>{/if}
|
||||
</div>
|
||||
<p class="reassure">Kept on this device. Nothing leaves it, and there's no account.</p>
|
||||
<p class="reassure">Your calm filters — kept on this device, and synced to your account when you're signed in.</p>
|
||||
|
||||
<div class="group">
|
||||
<label class="label" for="avoid">Avoid words or phrases</label>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script>
|
||||
import Avatar from './Avatar.svelte';
|
||||
let { onBoundaries, onHistory, onaccount, user = null, filtersOn = false } = $props();
|
||||
let { onSaved, onaccount, user = null, boundariesActive = false } = $props();
|
||||
</script>
|
||||
|
||||
<header class="appbar">
|
||||
@@ -10,17 +10,19 @@
|
||||
</a>
|
||||
|
||||
<nav class="utils" aria-label="Your controls">
|
||||
<button class:on={filtersOn} onclick={onBoundaries} title="Your boundaries">
|
||||
{#if user}
|
||||
<button class="util" onclick={onSaved} 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.8" stroke-linejoin="round" /></svg>
|
||||
<span>Saved</span>
|
||||
</button>
|
||||
{/if}
|
||||
<a class="util shield" class:on={boundariesActive} href="/account?section=boundaries"
|
||||
title={boundariesActive ? '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="none" stroke="currentColor" stroke-width="1.8" stroke-linejoin="round" /></svg>
|
||||
<span>Boundaries</span>
|
||||
</button>
|
||||
<button onclick={onHistory} title="What you've seen">
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="8.5" fill="none"
|
||||
stroke="currentColor" stroke-width="1.8" /><path d="M12 7v5l3.5 2" fill="none"
|
||||
stroke="currentColor" stroke-width="1.8" stroke-linecap="round" /></svg>
|
||||
<span>History</span>
|
||||
</button>
|
||||
fill={boundariesActive ? 'currentColor' : 'none'} stroke="currentColor" stroke-width="1.8"
|
||||
stroke-linejoin="round" /></svg>
|
||||
</a>
|
||||
{#if user}
|
||||
<button class="acct" onclick={onaccount} title={user.email} aria-label="Your account">
|
||||
<Avatar {user} size={30} />
|
||||
@@ -39,29 +41,30 @@
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 20;
|
||||
/* a whisper of warmth under the bar */
|
||||
box-shadow: 0 1px 0 rgba(40, 38, 28, 0.02);
|
||||
}
|
||||
.bar { display: flex; align-items: center; justify-content: space-between; height: 78px; }
|
||||
|
||||
.brand { display: inline-flex; align-items: center; }
|
||||
.logo { height: 54px; width: auto; display: block; }
|
||||
|
||||
.utils { display: flex; gap: 6px; }
|
||||
.utils button {
|
||||
.utils { display: flex; align-items: center; gap: 4px; }
|
||||
.utils .util {
|
||||
display: inline-flex; align-items: center; gap: 7px;
|
||||
background: none; border: 1px solid transparent; border-radius: 999px;
|
||||
padding: 7px 13px; color: var(--muted); font-size: 0.86rem; cursor: pointer;
|
||||
padding: 7px 12px; color: var(--muted); font-size: 0.86rem; cursor: pointer;
|
||||
transition: background 0.14s ease, color 0.14s ease;
|
||||
}
|
||||
.utils button svg { width: 17px; height: 17px; }
|
||||
.utils button:hover { background: var(--accent-soft); color: var(--accent-deep); }
|
||||
.utils button.on { color: var(--accent-deep); }
|
||||
.utils .signin { border-color: var(--line); color: var(--accent-deep); }
|
||||
.utils .signin:hover { background: var(--accent-soft); }
|
||||
.acct { padding: 4px; display: inline-flex; }
|
||||
.utils .util svg { width: 18px; height: 18px; }
|
||||
.utils .util:hover { background: var(--accent-soft); color: var(--accent-deep); }
|
||||
.shield.on { color: var(--accent); }
|
||||
.acct { background: none; border: none; padding: 4px; display: inline-flex; cursor: pointer; }
|
||||
.signin {
|
||||
background: none; border: 1px solid var(--line); border-radius: 999px;
|
||||
padding: 7px 13px; color: var(--accent-deep); font-size: 0.86rem; cursor: pointer;
|
||||
}
|
||||
.signin:hover { background: var(--accent-soft); }
|
||||
|
||||
/* On phones the utilities live in the bottom tab bar ("You") instead. */
|
||||
/* On phones the bottom tab bar handles navigation; keep the bar to the logo. */
|
||||
@media (max-width: 720px) {
|
||||
.bar { height: 66px; justify-content: center; }
|
||||
.utils { display: none; }
|
||||
|
||||
@@ -0,0 +1,93 @@
|
||||
<script>
|
||||
import { onMount } from 'svelte';
|
||||
import { getJSON } from '$lib/api.js';
|
||||
import { savedIds, toggleSave } from '$lib/auth.svelte.js';
|
||||
import { track } from '$lib/analytics.js';
|
||||
|
||||
let { onclose } = $props();
|
||||
let items = $state([]);
|
||||
let loading = $state(true);
|
||||
|
||||
onMount(async () => {
|
||||
try {
|
||||
items = (await getJSON('/api/saved')).items;
|
||||
} catch {
|
||||
/* not signed in / transient */
|
||||
} finally {
|
||||
loading = false;
|
||||
}
|
||||
});
|
||||
|
||||
// Filter by the live saved set so an unsave removes it immediately.
|
||||
let shown = $derived(items.filter((a) => savedIds.has(a.id)));
|
||||
|
||||
function onkey(e) {
|
||||
if (e.key === 'Escape') onclose?.();
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window onkeydown={onkey} />
|
||||
|
||||
<div class="overlay" onclick={onclose} role="presentation">
|
||||
<aside class="drawer" onclick={(e) => e.stopPropagation()} aria-label="Saved articles">
|
||||
<div class="head">
|
||||
<h2>Saved</h2>
|
||||
<button class="x" onclick={onclose} aria-label="Close">×</button>
|
||||
</div>
|
||||
|
||||
{#if loading}
|
||||
<p class="muted">Loading…</p>
|
||||
{:else if shown.length}
|
||||
<ul>
|
||||
{#each shown as a (a.id)}
|
||||
<li>
|
||||
<a class="t" href={a.url} target="_blank" rel="noopener" onclick={() => track('open', a.id)}>{a.title}</a>
|
||||
<div class="meta">
|
||||
<span class="src">{a.source}</span>
|
||||
<button class="rm" onclick={() => toggleSave(a.id)}>Remove</button>
|
||||
</div>
|
||||
</li>
|
||||
{/each}
|
||||
</ul>
|
||||
<a class="all" href="/account?section=saved" onclick={onclose}>Open Saved in account →</a>
|
||||
{:else}
|
||||
<p class="muted">Nothing saved yet — tap <strong>Save</strong> on a story to keep it here.</p>
|
||||
{/if}
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
<style>
|
||||
.overlay {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
background: rgba(10, 22, 38, 0.32);
|
||||
z-index: 50;
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
.drawer {
|
||||
background: var(--surface);
|
||||
width: min(380px, 100%);
|
||||
height: 100%;
|
||||
box-shadow: -10px 0 30px rgba(40, 38, 28, 0.12);
|
||||
padding: 20px 22px;
|
||||
overflow-y: auto;
|
||||
animation: slide 0.2s ease both;
|
||||
}
|
||||
@keyframes slide { from { transform: translateX(20px); opacity: 0.4; } to { transform: none; opacity: 1; } }
|
||||
@media (prefers-reduced-motion: reduce) { .drawer { animation: none; } }
|
||||
.head { display: flex; align-items: baseline; justify-content: space-between; margin-bottom: 14px; }
|
||||
.head h2 { font-size: 1.4rem; }
|
||||
.x { background: none; border: none; font-size: 1.6rem; line-height: 1; color: var(--muted); cursor: pointer; }
|
||||
.muted { color: var(--muted); }
|
||||
ul { list-style: none; margin: 0; padding: 0; }
|
||||
li { padding: 12px 0; border-bottom: 1px solid var(--line); }
|
||||
li:last-of-type { border-bottom: none; }
|
||||
.t { color: var(--ink); font-weight: 600; font-size: 0.95rem; display: block; }
|
||||
.t:hover { color: var(--accent-deep); }
|
||||
.meta { display: flex; align-items: center; justify-content: space-between; margin-top: 5px; }
|
||||
.src { color: var(--muted); font-size: 0.78rem; }
|
||||
.rm { background: none; border: none; color: var(--muted); font-size: 0.78rem; text-decoration: underline; cursor: pointer; }
|
||||
.rm:hover { color: var(--accent-deep); }
|
||||
.all { display: inline-block; margin-top: 16px; color: var(--accent-deep); font-size: 0.88rem; }
|
||||
</style>
|
||||
Reference in New Issue
Block a user