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
@@ -13,6 +13,7 @@ import { ExoplanetRecord } from '../../shared/models/exoplanet.model';
import { applyMilkyWaySkybox, createGlowSprite } from '../../shared/rendering/skybox';
import { loadCachedTexture, MILKY_WAY_SKYBOX_PATH, SUN_TEXTURE_PATH } from '../../shared/rendering/texture-catalog';
import { StarRecord } from '../../shared/models/star.model';
import { Bookmark } from '../../shared/state/bookmarks.store';
import { NavigationStore, ViewLevel } from '../../shared/state/navigation.store';
import { CameraRigController } from './camera-rig-controller';
import { DeepSkyRenderer } from './deep-sky-renderer';
@@ -225,12 +226,14 @@ function galacticOverviewPose(): { position: THREE.Vector3; target: THREE.Vector
[routeResult]="routeResult()"
[routeOptions]="routeOptions()"
[currentStar]="currentStarOption()"
[keepableStarId]="navigationStore.selectedStarId()"
defaultTab="readout"
(displayChange)="display.set($event)"
(routeQuery)="onRouteQuery($event)"
(routeRequested)="onRouteRequested($event)"
(routeStarSelected)="navigationStore.selectStar($event)"
(jumpRangeChange)="jumpRangePc.set($event)"
(bookmarkChosen)="goToBookmark($event)"
/>
</div>
`
@@ -1204,6 +1207,15 @@ export class GalaxySystemSceneComponent implements AfterViewInit, OnDestroy {
this.clearObjectCard();
}
/** A kept place, revisited: a star is a system to fly into, a body is a page to open. */
goToBookmark(bookmark: Bookmark): void {
if (bookmark.kind === 'star') {
this.navigationStore.selectStar(Number(bookmark.id));
} else {
this.openObjectDetail(String(bookmark.id));
}
}
/** The deliberate step out to the dedicated route, from the card's own control. */
openObjectDetail(id: string): void {
this.navigationStore.selectBody(id);
@@ -1,7 +1,9 @@
import { ChangeDetectionStrategy, Component, computed, input, output } from '@angular/core';
import { ChangeDetectionStrategy, Component, computed, inject, input, output } from '@angular/core';
import { BodyDetailViewModel } from '../body-detail/body-detail.model';
import { bodyReadouts } from '../body-detail/body-readouts';
import { BookmarksStore } from '../../shared/state/bookmarks.store';
import { BookmarkIconComponent } from '../../shared/ui/bookmark-icon.component';
import { ReadoutSectionsComponent } from '../body-detail/readout-sections.component';
import { ChevronIconComponent } from '../../shared/ui/chevron-icon.component';
@@ -19,7 +21,7 @@ import { ChevronIconComponent } from '../../shared/ui/chevron-icon.component';
@Component({
selector: 'app-system-object-card',
changeDetection: ChangeDetectionStrategy.OnPush,
imports: [ChevronIconComponent, ReadoutSectionsComponent],
imports: [BookmarkIconComponent, ChevronIconComponent, ReadoutSectionsComponent],
// Positioned and full-bleed like the HUD's own host, so the panel inside it resolves against
// the scene rather than against whatever box the inline default would have left it in — which
// put the card off the bottom-left corner of the viewport entirely.
@@ -36,6 +38,16 @@ import { ChevronIconComponent } from '../../shared/ui/chevron-icon.component';
<p class="truncate text-lg leading-tight font-bold tracking-[0.04em] text-text uppercase">{{ body().name }}</p>
<p class="type-eyebrow mt-1 truncate text-accent">{{ readouts().kindLabel }} · {{ body().hostStarName }}</p>
</header>
<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>
<button
type="button"
(click)="dismissed.emit()"
@@ -69,5 +81,7 @@ export class SystemObjectCardComponent {
/** Request for the full `/body/:id` route. */
readonly openRequested = output<void>();
readonly bookmarks = inject(BookmarksStore);
readonly readouts = computed(() => bodyReadouts(this.body()));
}