Mirror lane picker into the Account page
Add a "Lanes" section under Account that reuses LanePicker inline, completing the round trip with Boundaries. Refactor LanePicker to support an `inline` variant (bare panel vs modal) and apply changes immediately on toggle — so the account panel needs no explicit save and the home modal now previews the nav rail live as you pick. Selection still persists through the shared prefs store. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,71 +1,76 @@
|
|||||||
<script>
|
<script>
|
||||||
// pool: { pinned, default, groups:[{name, lanes:[{key,label,description,count?}]}] }
|
// pool: { pinned, default, groups:[{name, lanes:[{key,label,description,count?}]}] }
|
||||||
// selected: array of currently-pinned lane keys (excludes the always-on 'today')
|
// selected: array of currently-pinned lane keys (excludes the always-on 'today')
|
||||||
let { pool, selected, onsave, onclose } = $props();
|
// onsave(keys): called on every change — selection persists immediately, so the
|
||||||
|
// nav rail updates live behind the modal and the account panel needs no "save".
|
||||||
|
// inline: render as a bare panel (account page) instead of a modal overlay.
|
||||||
|
let { pool, selected, onsave, onclose, inline = false } = $props();
|
||||||
|
|
||||||
// Work on a local copy; only commit on Done.
|
let chosen = $derived(new Set(selected ?? []));
|
||||||
let chosen = $state(new Set(selected ?? []));
|
// The pool's natural order, so the rail always reads predictably.
|
||||||
|
let order = $derived((pool?.groups ?? []).flatMap((g) => g.lanes.map((l) => l.key)));
|
||||||
|
|
||||||
function toggle(key) {
|
function toggle(key) {
|
||||||
if (chosen.has(key)) chosen.delete(key);
|
const next = new Set(chosen);
|
||||||
else chosen.add(key);
|
if (next.has(key)) next.delete(key);
|
||||||
chosen = new Set(chosen); // reassign so Svelte re-renders
|
else next.add(key);
|
||||||
|
onsave?.(order.filter((k) => next.has(k)));
|
||||||
}
|
}
|
||||||
function resetDefault() {
|
function resetDefault() {
|
||||||
chosen = new Set(pool?.default ?? []);
|
onsave?.([...(pool?.default ?? [])]);
|
||||||
}
|
|
||||||
function done() {
|
|
||||||
// Preserve each group's natural order so the rail reads predictably.
|
|
||||||
const order = [];
|
|
||||||
for (const g of pool?.groups ?? []) for (const l of g.lanes) order.push(l.key);
|
|
||||||
onsave?.(order.filter((k) => chosen.has(k)));
|
|
||||||
onclose?.();
|
|
||||||
}
|
}
|
||||||
function onkey(e) {
|
function onkey(e) {
|
||||||
if (e.key === 'Escape') onclose?.();
|
if (e.key === 'Escape') onclose?.();
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
{#snippet body()}
|
||||||
|
<h2>Your lanes</h2>
|
||||||
|
<p class="sub">Pick the quick-access lanes above the feed. <strong>Today</strong> always stays — choose the rest. Changes apply right away.</p>
|
||||||
|
|
||||||
|
<div class="pinned-row">
|
||||||
|
<span class="chip pinned" title="Always shown">Today <span class="lock">📌</span></span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#each pool?.groups ?? [] as g (g.name)}
|
||||||
|
{#if g.lanes.length}
|
||||||
|
<div class="group">
|
||||||
|
<span class="label">{g.name}</span>
|
||||||
|
<div class="chips">
|
||||||
|
{#each g.lanes as l (l.key)}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
class="chip"
|
||||||
|
class:on={chosen.has(l.key)}
|
||||||
|
title={l.description ?? ''}
|
||||||
|
onclick={() => toggle(l.key)}
|
||||||
|
>
|
||||||
|
{l.label}{#if l.count > 0}<small>{l.count}</small>{/if}
|
||||||
|
</button>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
{/each}
|
||||||
|
|
||||||
|
<div class="actions">
|
||||||
|
<button class="reset" onclick={resetDefault}>Reset to default</button>
|
||||||
|
{#if !inline}<button class="primary" onclick={onclose}>Done</button>{/if}
|
||||||
|
</div>
|
||||||
|
{/snippet}
|
||||||
|
|
||||||
<svelte:window onkeydown={onkey} />
|
<svelte:window onkeydown={onkey} />
|
||||||
|
|
||||||
<div class="overlay" onclick={onclose} role="presentation">
|
{#if inline}
|
||||||
<div class="sheet rise" role="dialog" aria-modal="true" aria-label="Customize lanes" onclick={(e) => e.stopPropagation()}>
|
<section class="panel rise">{@render body()}</section>
|
||||||
<button class="x" onclick={onclose} aria-label="Close">×</button>
|
{:else}
|
||||||
|
<div class="overlay" onclick={onclose} role="presentation">
|
||||||
<h2>Your lanes</h2>
|
<div class="sheet rise" role="dialog" aria-modal="true" aria-label="Customize lanes" onclick={(e) => e.stopPropagation()}>
|
||||||
<p class="sub">Pick the quick-access lanes above the feed. <strong>Today</strong> always stays — choose the rest.</p>
|
<button class="x" onclick={onclose} aria-label="Close">×</button>
|
||||||
|
{@render body()}
|
||||||
<div class="pinned-row">
|
|
||||||
<span class="chip pinned" title="Always shown">Today <span class="lock">📌</span></span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{#each pool?.groups ?? [] as g (g.name)}
|
|
||||||
{#if g.lanes.length}
|
|
||||||
<div class="group">
|
|
||||||
<span class="label">{g.name}</span>
|
|
||||||
<div class="chips">
|
|
||||||
{#each g.lanes as l (l.key)}
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
class="chip"
|
|
||||||
class:on={chosen.has(l.key)}
|
|
||||||
title={l.description ?? ''}
|
|
||||||
onclick={() => toggle(l.key)}
|
|
||||||
>
|
|
||||||
{l.label}{#if l.count > 0}<small>{l.count}</small>{/if}
|
|
||||||
</button>
|
|
||||||
{/each}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
{/each}
|
|
||||||
|
|
||||||
<div class="actions">
|
|
||||||
<button class="reset" onclick={resetDefault}>Reset to default</button>
|
|
||||||
<button class="primary" onclick={done}>Done</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
{/if}
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
.overlay {
|
.overlay {
|
||||||
@@ -77,6 +82,10 @@
|
|||||||
box-shadow: var(--shadow); width: 100%; max-width: 480px; padding: 26px 24px 20px;
|
box-shadow: var(--shadow); width: 100%; max-width: 480px; padding: 26px 24px 20px;
|
||||||
position: relative; max-height: 86vh; overflow-y: auto;
|
position: relative; max-height: 86vh; overflow-y: auto;
|
||||||
}
|
}
|
||||||
|
.panel {
|
||||||
|
background: var(--surface); border: 1px solid var(--line); border-radius: var(--radius);
|
||||||
|
box-shadow: var(--shadow); padding: 22px 22px 18px;
|
||||||
|
}
|
||||||
.x { position: absolute; top: 12px; right: 14px; background: none; border: none; font-size: 1.5rem; line-height: 1; color: var(--muted); cursor: pointer; }
|
.x { position: absolute; top: 12px; right: 14px; background: none; border: none; font-size: 1.5rem; line-height: 1; color: var(--muted); cursor: pointer; }
|
||||||
h2 { font-size: 1.4rem; margin: 0 0 6px; }
|
h2 { font-size: 1.4rem; margin: 0 0 6px; }
|
||||||
.sub { margin: 0 0 16px; color: var(--muted); font-size: 0.92rem; }
|
.sub { margin: 0 0 16px; color: var(--muted); font-size: 0.92rem; }
|
||||||
|
|||||||
@@ -9,6 +9,7 @@
|
|||||||
import { openFeedback } from '$lib/feedback.svelte.js';
|
import { openFeedback } from '$lib/feedback.svelte.js';
|
||||||
import AccountPanel from '$lib/components/AccountPanel.svelte';
|
import AccountPanel from '$lib/components/AccountPanel.svelte';
|
||||||
import BoundariesPanel from '$lib/components/BoundariesPanel.svelte';
|
import BoundariesPanel from '$lib/components/BoundariesPanel.svelte';
|
||||||
|
import LanePicker from '$lib/components/LanePicker.svelte';
|
||||||
import ArticleCard from '$lib/components/ArticleCard.svelte';
|
import ArticleCard from '$lib/components/ArticleCard.svelte';
|
||||||
|
|
||||||
let section = $derived($page.url.searchParams.get('section') || 'profile');
|
let section = $derived($page.url.searchParams.get('section') || 'profile');
|
||||||
@@ -16,11 +17,13 @@
|
|||||||
|
|
||||||
let savedItems = $state([]);
|
let savedItems = $state([]);
|
||||||
let savedReady = $state(false);
|
let savedReady = $state(false);
|
||||||
|
let lanePool = $state(null);
|
||||||
|
|
||||||
const SECTIONS = [
|
const SECTIONS = [
|
||||||
{ key: 'profile', label: 'Profile' },
|
{ key: 'profile', label: 'Profile' },
|
||||||
{ key: 'saved', label: 'Saved' },
|
{ key: 'saved', label: 'Saved' },
|
||||||
{ key: 'history', label: 'History' },
|
{ key: 'history', label: 'History' },
|
||||||
|
{ key: 'lanes', label: 'Lanes' },
|
||||||
{ key: 'boundaries', label: 'Boundaries' },
|
{ key: 'boundaries', label: 'Boundaries' },
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -29,8 +32,17 @@
|
|||||||
initPrefs();
|
initPrefs();
|
||||||
initHistory();
|
initHistory();
|
||||||
if (auth.user) loadServerHistory();
|
if (auth.user) loadServerHistory();
|
||||||
|
try { lanePool = await getJSON('/api/lanes'); } catch { lanePool = null; }
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let pinnedLaneKeys = $derived(
|
||||||
|
prefs.data.lanes?.length ? prefs.data.lanes : (lanePool?.default ?? [])
|
||||||
|
);
|
||||||
|
function saveLanes(keys) {
|
||||||
|
prefs.data.lanes = keys;
|
||||||
|
persistPrefs();
|
||||||
|
}
|
||||||
|
|
||||||
// Load the saved grid when entering that section while signed in.
|
// Load the saved grid when entering that section while signed in.
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (section === 'saved' && auth.user && !savedReady) {
|
if (section === 'saved' && auth.user && !savedReady) {
|
||||||
@@ -64,7 +76,14 @@
|
|||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<div class="content">
|
<div class="content">
|
||||||
{#if section === 'boundaries'}
|
{#if section === 'lanes'}
|
||||||
|
{#if lanePool}
|
||||||
|
<LanePicker inline pool={lanePool} selected={pinnedLaneKeys} onsave={saveLanes} />
|
||||||
|
{:else}
|
||||||
|
<p class="muted">Loading…</p>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{:else if section === 'boundaries'}
|
||||||
<BoundariesPanel prefs={prefs.data} onchange={persistPrefs} />
|
<BoundariesPanel prefs={prefs.data} onchange={persistPrefs} />
|
||||||
|
|
||||||
{:else if section === 'history'}
|
{:else if section === 'history'}
|
||||||
|
|||||||
Reference in New Issue
Block a user