Keep a place, and come back to it

Restores the commit reverted off the routing branch, which is where it was
committed by mistake. The change is unmodified; only its branch is.

The map had no memory. Every visit started at the same overview, and a system
worth returning to had to be found again by name each time. A mark on the
readout and on a body's panel now keeps it, a Bookmarks tab lists what has been
kept, and choosing one goes there — a star by flying into its system, a body by
opening its page.

Local storage, not an account. This map asks nobody to sign in, and a list of
stars somebody liked is not worth a server. Every read of that store is
defensive, because it is a string a person can edit, another tab can write, and
a browser can refuse to hand over at all: a bad entry is skipped rather than
losing the rest, duplicates are collapsed since two entries for one place would
each toggle the other's control, the list is bounded so a hand-edited store
cannot decide how much this renders, and where storage is denied outright the
bookmarks still work for the visit — they just do not outlive it.

The name is stored alongside the id rather than looked up, so the list reads
before the catalogues have loaded, and a bookmark to something a later
catalogue no longer holds still says what it was instead of decaying into a
bare number.

The tab is offered even when it is empty, and says what the mark does: a tab
that appears only once you have already found the feature is a tab that never
taught anyone anything.

Choosing a kept place hands the panel back to the readout, the same move as
choosing a search result and for the same reason. That behaviour is what the
end-to-end spec caught missing — the readout it asserted on did not exist,
because the panel just used was still covering it.

Verified: build clean, 587/587 unit, 11/11 end-to-end, design detector clean,
screenshots at 1440x900 and 390x844.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jxMkwA2rbicdGxHosecYi
This commit is contained in:
2026-08-20 19:12:58 +02:00
co-authored by Claude Fable 5
parent 9cdd8f9388
commit 307fd41be8
10 changed files with 513 additions and 18 deletions
@@ -1,6 +1,8 @@
import { Component, computed, input } from '@angular/core';
import { Component, computed, inject, input } from '@angular/core';
import { Router } from '@angular/router';
import { BookmarksStore } from '../../shared/state/bookmarks.store';
import { BookmarkIconComponent } from '../../shared/ui/bookmark-icon.component';
import { ChevronIconComponent } from '../../shared/ui/chevron-icon.component';
import { bodyReadouts } from './body-readouts';
import { BodyDetailViewModel } from './body-detail.model';
@@ -17,7 +19,7 @@ import { ReadoutSectionsComponent } from './readout-sections.component';
*/
@Component({
selector: 'app-info-panel',
imports: [ChevronIconComponent, ReadoutSectionsComponent],
imports: [BookmarkIconComponent, ChevronIconComponent, ReadoutSectionsComponent],
template: `
<!-- Top-right, clear of the dock along the bottom; nothing else shares the top edge here. -->
<div class="hud-brackets hud-acquire hud-surface absolute top-4 right-4 w-80 max-w-[calc(100%-2rem)] font-body text-text">
@@ -30,9 +32,21 @@ import { ReadoutSectionsComponent } from './readout-sections.component';
System
</button>
<header class="px-4 pt-4 pb-3">
<h1 class="truncate text-lg leading-tight font-bold tracking-[0.04em] text-text uppercase">{{ body().name }}</h1>
<p class="type-eyebrow mt-1 truncate text-accent">{{ readouts().kindLabel }} · {{ body().hostStarName }}</p>
<header class="flex items-start gap-2 px-4 pt-4 pb-3">
<div class="min-w-0 flex-1">
<h1 class="truncate text-lg leading-tight font-bold tracking-[0.04em] text-text uppercase">{{ body().name }}</h1>
<p class="type-eyebrow mt-1 truncate text-accent">{{ readouts().kindLabel }} · {{ body().hostStarName }}</p>
</div>
<button
type="button"
[attr.aria-label]="(bookmarks.has('body', body().id) ? 'Forget ' : 'Keep ') + body().name"
[attr.aria-pressed]="bookmarks.has('body', body().id)"
(click)="bookmarks.toggle({ kind: 'body', id: body().id, name: body().name })"
class="shrink-0 p-1 transition-colors focus-visible:outline-1 focus-visible:-outline-offset-1 focus-visible:outline-accent"
[class]="bookmarks.has('body', body().id) ? 'text-accent' : 'text-muted hover:text-accent'"
>
<app-bookmark-icon class="h-3.5 w-3.5" [kept]="bookmarks.has('body', body().id)" />
</button>
</header>
<app-readout-sections [readouts]="readouts()" />
@@ -42,6 +56,8 @@ import { ReadoutSectionsComponent } from './readout-sections.component';
export class InfoPanelComponent {
readonly body = input.required<BodyDetailViewModel>();
readonly bookmarks = inject(BookmarksStore);
readonly readouts = computed(() => bodyReadouts(this.body()));
constructor(private readonly router: Router) {}