Answer the review: hold the offer to the same test as the button

`canPlot()` guarded the Plot button and not `raiseTo`, which is the other way
into `plot()`. So with a departure typed but never chosen, clicking "1.8 pc
would reach." moved the range control and plotted nothing: the panel then read
"No route at this range. 1.8 pc would reach." beside a control already set to
1.8. The offer carries the same `disabled` as the button, since it is the same
request by another route.

And the departure guard is trimmed, as the scene trims the same text before
offering matches for it: one space in the field left it looking empty, with no
suggestions to pick from, and Plot dead for no reason on screen.

Measured in the app, from inside Barnard's Star with Sirius as the destination:
offer enabled with the field empty, disabled once "Sol" is typed and never
chosen — a forced click then moves nothing — and enabled again when the field is
cleared, where it raises the range to 2.40 pc and plots 7 jumps.

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-09-18 15:05:42 +02:00
co-authored by Claude Opus 5
parent 365252f534
commit dd56eafba4
2 changed files with 20 additions and 2 deletions
@@ -281,6 +281,21 @@ describe('HudDockComponent', () => {
type('from', ''); type('from', '');
expect(plot().disabled).toBe(false); expect(plot().disabled).toBe(false);
// A space is not text that names a star: the field looks empty, the scene offers nothing to
// choose for it, and the button going dead would have nothing on screen to explain it.
type('from', ' ');
expect(plot().disabled).toBe(false);
// The offer beside a refusal is the same request by another route, so it is held to the same
// test: moving the range with nothing to plot leaves the panel contradicting itself.
fixture.componentRef.setInput('routeResult', { stars: [], totalPc: 0, neededRangePc: 1.8, gaveUp: false });
fixture.detectChanges();
const offer = () => host().querySelector<HTMLButtonElement>('[data-testid="route-summary"] button')!;
expect(offer().disabled).toBe(false);
type('from', 'Sol');
expect(offer().disabled).toBe(true);
}); });
it('does not replay the acquire wipe over the Routes panel, whose entries survive the trip', () => { it('does not replay the acquire wipe over the Routes panel, whose entries survive the trip', () => {
@@ -117,8 +117,9 @@ type Field = 'from' | 'to';
@if (plotted.neededRangePc !== null) { @if (plotted.neededRangePc !== null) {
<button <button
type="button" type="button"
[disabled]="!canPlot() || pending()"
(click)="raiseTo(plotted.neededRangePc)" (click)="raiseTo(plotted.neededRangePc)"
class="text-accent underline decoration-accent/40 underline-offset-2 hover:decoration-accent focus-visible:outline-1 focus-visible:outline-accent" class="text-accent underline decoration-accent/40 underline-offset-2 disabled:opacity-40 disabled:no-underline enabled:hover:decoration-accent focus-visible:outline-1 focus-visible:outline-accent"
> >
{{ format(plotted.neededRangePc) }} would reach. {{ format(plotted.neededRangePc) }} would reach.
</button> </button>
@@ -181,7 +182,9 @@ export class RoutesPanelComponent {
* while the field is empty. Text left in it that names no chosen star used to fall back all the * while the field is empty. Text left in it that names no chosen star used to fall back all the
* same, so the panel read "Sol" and the route left from whatever the view had since flown to. * same, so the panel read "Sol" and the route left from whatever the view had since flown to.
*/ */
private readonly departure = computed(() => this.chosen().from ?? (this.typed().from ? null : this.currentStar())); // Trimmed, as the scene trims the same text before offering matches for it: a field holding one
// space looks empty, offers nothing to choose, and would otherwise count as a departure.
private readonly departure = computed(() => this.chosen().from ?? (this.typed().from.trim() ? null : this.currentStar()));
readonly canPlot = computed(() => this.departure() !== null && this.chosen().to !== null); readonly canPlot = computed(() => this.departure() !== null && this.chosen().to !== null);
readonly rangeLabel = computed(() => formatParsecs(this.rangePc())); readonly rangeLabel = computed(() => formatParsecs(this.rangePc()));