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: `
-
-
- @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()) {
-
+
}
-