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:
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user