diff --git a/src/app/features/body-detail/body-detail-scene.component.ts b/src/app/features/body-detail/body-detail-scene.component.ts index 3f03892..71a8f29 100644 --- a/src/app/features/body-detail/body-detail-scene.component.ts +++ b/src/app/features/body-detail/body-detail-scene.component.ts @@ -13,6 +13,7 @@ import { BodyRecord } from '../../shared/models/body.model'; import { ExoplanetRecord } from '../../shared/models/exoplanet.model'; import { StarRecord } from '../../shared/models/star.model'; import { NavigationStore } from '../../shared/state/navigation.store'; +import { ChevronIconComponent } from '../../shared/ui/chevron-icon.component'; import { BodyDetailViewModel } from './body-detail.model'; import { buildBodyViewModel } from './body-view-model'; import { InfoPanelComponent } from './info-panel.component'; @@ -36,23 +37,21 @@ const GLOW_SCALE = 2.6; @Component({ selector: 'app-body-detail-scene', providers: [EngineService], - imports: [InfoPanelComponent, RouterLink], + imports: [ChevronIconComponent, InfoPanelComponent, RouterLink], template: `
@if (viewModel()) { } @else if (notFound()) { -
-

No record

+
+

No record

That id isn't in the catalog — it may have been renamed or mistyped.

- - - + Back to the galaxy
diff --git a/src/app/features/body-detail/info-panel.component.ts b/src/app/features/body-detail/info-panel.component.ts index 5e786ee..0cdbda9 100644 --- a/src/app/features/body-detail/info-panel.component.ts +++ b/src/app/features/body-detail/info-panel.component.ts @@ -1,64 +1,43 @@ import { Component, computed, input } from '@angular/core'; import { Router } from '@angular/router'; +import { ChevronIconComponent } from '../../shared/ui/chevron-icon.component'; import { bodyReadouts } from './body-readouts'; import { BodyDetailViewModel } from './body-detail.model'; +import { ReadoutSectionsComponent } from './readout-sections.component'; /** * Displays the real NASA data for the currently selected body/exoplanet: kind, physical * size/mass, orbital elements, and (for exoplanets) discovery year. Presentational only — * `BodyDetailSceneComponent` supplies the view model and owns navigation state. * - * The rows come from `bodyReadouts`, shared with the system view's object card so the same body - * cannot read differently in the two places it can be inspected. + * The rows come from `bodyReadouts` via `ReadoutSectionsComponent`, both shared with the system + * view's object card so the same body cannot read differently in the two places it can be + * inspected. */ @Component({ selector: 'app-info-panel', + imports: [ChevronIconComponent, ReadoutSectionsComponent], template: ` - -
+ +

{{ body().name }}

-

{{ readouts().kindLabel }} · {{ body().hostStarName }}

+

{{ readouts().kindLabel }} · {{ body().hostStarName }}

- - @if (readouts().measured.length) { -

Measured

-
- @for (row of readouts().measured; track row.label) { -
-
{{ row.label }}
-
{{ row.value }}
-
- } -
- } - -

Derived

-
- @for (row of readouts().derived; track row.label) { -
-
{{ row.label }}
-
{{ row.value }}
-
- } -
- -

{{ readouts().provenance }}

+
` }) diff --git a/src/app/features/body-detail/readout-sections.component.ts b/src/app/features/body-detail/readout-sections.component.ts new file mode 100644 index 0000000..3909783 --- /dev/null +++ b/src/app/features/body-detail/readout-sections.component.ts @@ -0,0 +1,43 @@ +import { ChangeDetectionStrategy, Component, input } from '@angular/core'; + +import { BodyReadouts } from './body-readouts'; + +/** + * The Measured/Derived rows and the provenance footnote, shared by the detail page's info panel + * and the system view's object card. `bodyReadouts` already guarantees the two surfaces agree + * on the numbers; this guarantees they agree on the pixels — the markup used to be pasted four + * times across the two templates, which is exactly the drift the shared data model exists to + * prevent. + */ +@Component({ + selector: 'app-readout-sections', + changeDetection: ChangeDetectionStrategy.OnPush, + template: ` + @if (readouts().measured.length) { +

Measured

+
+ @for (row of readouts().measured; track row.label) { +
+
{{ row.label }}
+
{{ row.value }}
+
+ } +
+ } + +

Derived

+
+ @for (row of readouts().derived; track row.label) { +
+
{{ row.label }}
+
{{ row.value }}
+
+ } +
+ +

{{ readouts().provenance }}

+ ` +}) +export class ReadoutSectionsComponent { + readonly readouts = input.required(); +} diff --git a/src/app/features/galaxy-system/star-label-overlay.ts b/src/app/features/galaxy-system/star-label-overlay.ts index 05979ee..dad2d3f 100644 --- a/src/app/features/galaxy-system/star-label-overlay.ts +++ b/src/app/features/galaxy-system/star-label-overlay.ts @@ -98,6 +98,11 @@ export class StarLabelOverlay { } const object = new CSS2DObject(element); + // Anchor the label's left edge at the point, vertically centred. The default center of + // (0.5, 0.5) makes CSS2DRenderer emit translate(-50%,-50%), keeping the box centred on the + // star — under which `.map-label`'s margin offset only nudges the centred box sideways and + // the leader line points at empty space half the label's width from the star. + object.center.set(0, 0.5); object.position.set(point.x, point.y, point.z); this.scene.add(object); this.labelObjects.set(point.id, object); diff --git a/src/app/features/galaxy-system/starmap-hud.component.spec.ts b/src/app/features/galaxy-system/starmap-hud.component.spec.ts index 79ed7a0..980dce8 100644 --- a/src/app/features/galaxy-system/starmap-hud.component.spec.ts +++ b/src/app/features/galaxy-system/starmap-hud.component.spec.ts @@ -94,11 +94,11 @@ describe('StarmapHudComponent', () => { fixture.componentRef.setInput('title', 'Sol'); fixture.detectChanges(); - expect((fixture.nativeElement as HTMLElement).querySelector('.hud-banner')?.textContent?.trim()).toBe('Sol'); + expect((fixture.nativeElement as HTMLElement).querySelector('[data-testid="hud-banner"]')?.textContent?.trim()).toBe('Sol'); }); it('shows no banner when the view is holding nothing', () => { // An empty nameplate is worse than none: it reads as a selection that failed to resolve. - expect(render('galaxy').querySelector('.hud-banner')).toBeNull(); + expect(render('galaxy').querySelector('[data-testid="hud-banner"]')).toBeNull(); }); }); diff --git a/src/app/features/galaxy-system/starmap-hud.component.ts b/src/app/features/galaxy-system/starmap-hud.component.ts index 9019aaa..cc9371d 100644 --- a/src/app/features/galaxy-system/starmap-hud.component.ts +++ b/src/app/features/galaxy-system/starmap-hud.component.ts @@ -1,6 +1,7 @@ import { ChangeDetectionStrategy, Component, computed, input, output } from '@angular/core'; import { ViewLevel } from '../../shared/state/navigation.store'; +import { ReticleIconComponent } from '../../shared/ui/reticle-icon.component'; export interface HudReadout { readonly label: string; @@ -45,6 +46,7 @@ const LADDER: readonly { level: ViewLevel; label: string }[] = [ @Component({ selector: 'app-starmap-hud', changeDetection: ChangeDetectionStrategy.OnPush, + imports: [ReticleIconComponent], host: { class: 'pointer-events-none absolute inset-0 block select-none' }, template: `
@@ -52,21 +54,18 @@ const LADDER: readonly { level: ViewLevel; label: string }[] = [ @if (showReticle()) { - + } -