Restyle the HUD as an observatory instrument

Replaces the generic rounded dark cards floating over the scene with a
single instrument language: hairline frames cut by accent corner ticks, a
viewport frame that reads the whole screen as one panel, and readouts whose
figures are what the eye lands on.

- styles.css: retune the tokens (deeper void/panel, brighter text/muted so
  every label clears 4.5:1 even over the brightest part of the skybox) and
  add two utilities — `hud-brackets` draws the four corner ticks from eight
  background gradients so no panel pays for them in DOM, `hud-acquire` wipes
  a panel down on mount as its one authored moment (reduced-motion aware).
- Drop Orbitron: every string in this UI is a measurement, an identifier, or
  a catalog label, so one readout face carries the hierarchy through weight,
  size, and tracking — and the HUD paints one font request sooner.
- Search: reticle mark instead of a magnifier, transparent field inside a
  framed shell, results as a dense two-column list with a match count, and
  the previously missing empty state.
- Info panel: return rail, name/class header, and a readout table with
  tabular figures and the unit tinted back off the number.
- Star labels: a hairline leader back to the star they name. The offset moved
  from `translate-*` to a margin because CSS2DRenderer overwrites the inline
  transform every frame, which silently beat the old classes.
- Both top-anchored overlays drop below the search field under `sm` so they
  no longer stack on top of it on a phone, and focus moved from a removed
  outline plus ring to real `focus-visible` outlines.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016jxMkwA2rbicdGxHosecYi
This commit is contained in:
2026-08-07 17:42:14 +02:00
co-authored by Claude Opus 5
parent d7e8ea1d4d
commit 836816ffbd
8 changed files with 161 additions and 55 deletions
+30 -20
View File
@@ -34,11 +34,11 @@ const KIND_LABELS: Record<SearchResultKind, string> = {
@Component({
selector: 'app-search',
template: `
<div class="fixed top-4 left-1/2 z-20 w-[22rem] max-w-[calc(100%-2rem)] -translate-x-1/2 font-body">
<div class="relative">
<svg class="pointer-events-none absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2 text-muted" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="11" cy="11" r="7" />
<path d="M21 21l-4.3-4.3" />
<div class="fixed top-4 left-1/2 z-20 w-[26rem] max-w-[calc(100%-2rem)] -translate-x-1/2 font-body">
<div class="hud-brackets relative border border-border/60 bg-panel/85 backdrop-blur-md transition-colors focus-within:border-accent/70">
<svg class="pointer-events-none absolute top-1/2 left-3 h-4 w-4 -translate-y-1/2 text-accent/70" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round">
<circle cx="12" cy="12" r="5" />
<path d="M12 2v3M12 19v3M2 12h3M19 12h3" />
</svg>
<input
type="text"
@@ -46,24 +46,30 @@ const KIND_LABELS: Record<SearchResultKind, string> = {
[value]="query()"
(input)="onInput($event)"
(keydown.escape)="clear()"
class="w-full rounded-md border border-border bg-panel/85 py-2 pl-9 pr-3 text-sm text-text backdrop-blur-md transition-colors placeholder:text-muted focus:border-accent focus:outline-none focus:ring-1 focus:ring-accent/50"
class="w-full bg-transparent py-2.5 pr-3 pl-10 text-sm tracking-[0.02em] text-text caret-accent placeholder:text-muted focus:outline-none"
/>
</div>
@if (results().length) {
<ul data-testid="search-results" class="mt-1 divide-y divide-border/40 overflow-hidden rounded-md border border-border bg-panel/90 backdrop-blur-md">
@for (result of results(); track result.bodyId ?? result.starId) {
<li>
<button
type="button"
(click)="select(result)"
class="flex w-full flex-col gap-0.5 px-3 py-2 text-left transition-colors hover:bg-accent/10 focus:bg-accent/10 focus:outline-none"
>
<span class="text-sm text-text">{{ result.name }}</span>
<span class="text-xs tracking-wide text-muted uppercase">{{ kindLabel(result.kind) }} · {{ result.subtitle }}</span>
</button>
</li>
}
</ul>
<div class="hud-acquire mt-2 border border-border/60 bg-panel/92 backdrop-blur-md">
<p class="border-b border-border/40 px-3 py-1.5 text-[10px] tracking-[0.16em] text-muted uppercase">Matches <span class="text-accent tabular-nums">{{ results().length }}</span></p>
<ul data-testid="search-results" class="divide-y divide-border/25">
@for (result of results(); track result.bodyId ?? result.starId) {
<li>
<button
type="button"
(click)="select(result)"
class="flex w-full items-baseline gap-3 border-l border-transparent px-3 py-2 text-left transition-colors hover:border-l-accent hover:bg-accent/8 focus-visible:border-l-accent focus-visible:bg-accent/12 focus-visible:outline-1 focus-visible:-outline-offset-1 focus-visible:outline-accent"
>
<span class="min-w-0 flex-1 truncate text-sm text-text">{{ result.name }}</span>
<span class="max-w-[45%] shrink-0 truncate text-[10px] tracking-[0.14em] text-muted uppercase">{{ kindLabel(result.kind) }} · {{ result.subtitle }}</span>
</button>
</li>
}
</ul>
</div>
} @else if (hasQuery()) {
<p class="hud-acquire mt-2 border border-border/60 bg-panel/92 px-3 py-2.5 text-center text-[11px] tracking-[0.14em] text-muted uppercase backdrop-blur-md">Nothing in the catalog matches <span class="normal-case text-text">“{{ query().trim() }}”</span></p>
}
</div>
`
@@ -72,6 +78,10 @@ export class SearchComponent {
readonly query = signal('');
private readonly index = signal<SearchEntry[]>([]);
/** True once the query is long enough to have been searched — so "no matches" is only ever
* reported about a search that actually ran, never about a half-typed word. */
readonly hasQuery = computed(() => this.query().trim().length >= MIN_QUERY_LENGTH);
readonly results = computed(() => {
const query = this.query().trim().toLowerCase();
if (query.length < MIN_QUERY_LENGTH) {