Step 29.8: Shipyard, Places, Start, Welcome and the remaining screens

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
fireshaper 2026-11-04 18:41:02 -05:00
parent 4a33aed970
commit 613b06a83d
21 changed files with 81 additions and 60 deletions

View File

@ -352,7 +352,7 @@ function Totals({ summary }: { summary: Summary }) {
return ( return (
<dl <dl
aria-label="Company totals" aria-label="Company totals"
className="grid grid-flow-col gap-2 rounded-xl border border-cyan-400/30 bg-cyan-950/30 p-3" className="grid grid-flow-col gap-2 rounded-xl border border-glass-line bg-white/5 p-3"
> >
{figures.map(([label, value, tone]) => ( {figures.map(([label, value, tone]) => (
<div key={label}> <div key={label}>

View File

@ -4,13 +4,35 @@ import { useLoadedPlayer } from "../state/actions";
/** Fleet's two views, Ships and Crew (decision 0.5.167). */ /** Fleet's two views, Ships and Crew (decision 0.5.167). */
export function FleetViews() { export function FleetViews() {
const state = useLoadedPlayer(); const state = useLoadedPlayer();
const views = [ return (
{ to: "/fleet", label: `Ships ${state.ships.length}` }, <Views
{ to: "/pilots", label: `Crew ${state.pilots.length}` }, label="Fleet views"
]; views={[
{ to: "/fleet", label: `Ships ${state.ships.length}` },
{ to: "/pilots", label: `Crew ${state.pilots.length}` },
]}
/>
);
}
/** Places and HQs, under More (decision 0.5.167). */
export function PlacesViews() {
const state = useLoadedPlayer();
return (
<Views
label="Places views"
views={[
{ to: "/locations", label: "Places" },
{ to: "/hqs", label: `HQs ${state.bays.length}` },
]}
/>
);
}
function Views({ label, views }: { label: string; views: { to: string; label: string }[] }) {
return ( return (
<nav <nav
aria-label="Fleet views" aria-label={label}
className="flex gap-0.5 rounded-xl border border-glass-line bg-black/25 p-0.5" className="flex gap-0.5 rounded-xl border border-glass-line bg-black/25 p-0.5"
> >
{views.map((view) => ( {views.map((view) => (

View File

@ -124,7 +124,7 @@ function EntryLine({ entry, state }: { entry: LogEntry; state: PlayerState }) {
{entry.loanRepayment > 0 && ( {entry.loanRepayment > 0 && (
<span className="text-ink-400">lender {formatCredits(entry.loanRepayment)}</span> <span className="text-ink-400">lender {formatCredits(entry.loanRepayment)}</span>
)} )}
{entry.xp > 0 && <span className="text-violet-300">+{entry.xp} XP</span>} {entry.xp > 0 && <span className="text-ink-300">+{entry.xp} XP</span>}
</> </>
); );
case "refuel": case "refuel":
@ -202,7 +202,7 @@ function PilotSection({ log, state, open }: { log: PilotLog; state: PlayerState;
<details open={open} className="rounded-xl border border-glass-line bg-black/25 p-3"> <details open={open} className="rounded-xl border border-glass-line bg-black/25 p-3">
<summary className="flex cursor-pointer items-baseline justify-between gap-2"> <summary className="flex cursor-pointer items-baseline justify-between gap-2">
<span className="font-display font-bold">{log.name}</span> <span className="font-display font-bold">{log.name}</span>
{log.xp > 0 && <span className="text-xs text-violet-300">+{log.xp} XP</span>} {log.xp > 0 && <span className="text-xs text-ink-300">+{log.xp} XP</span>}
</summary> </summary>
{log.now && ( {log.now && (
<p aria-label={`${log.name} now`} className="mt-1 text-ink-100"> <p aria-label={`${log.name} now`} className="mt-1 text-ink-100">
@ -238,7 +238,7 @@ export function PilotLogbook({ logbook, state }: { logbook: Logbook; state: Play
<section aria-label="Pilot logbook" className="space-y-3 text-sm"> <section aria-label="Pilot logbook" className="space-y-3 text-sm">
<div <div
aria-label="Logbook totals" aria-label="Logbook totals"
className="grid grid-cols-2 gap-x-3 gap-y-1 rounded-xl border border-cyan-400/40 bg-cyan-950/30 p-3 sm:grid-cols-4" className="grid grid-cols-2 gap-x-3 gap-y-1 rounded-xl border border-glass-line bg-white/5 p-3 sm:grid-cols-4"
> >
<span>{totals.jobs} jobs</span> <span>{totals.jobs} jobs</span>
<span>{totals.deadheads} deadheads</span> <span>{totals.deadheads} deadheads</span>
@ -249,7 +249,7 @@ export function PilotLogbook({ logbook, state }: { logbook: Logbook; state: Play
<span className={totals.net < 0 ? "text-rose-300" : "text-emerald-300"}> <span className={totals.net < 0 ? "text-rose-300" : "text-emerald-300"}>
{formatCredits(totals.net)} net {formatCredits(totals.net)} net
</span> </span>
<span className="text-violet-300">+{totals.xp} XP</span> <span className="text-ink-300">+{totals.xp} XP</span>
</div> </div>
{logbook.pilots.length === 0 ? ( {logbook.pilots.length === 0 ? (
<p className="text-ink-400">Nothing logged in this time.</p> <p className="text-ink-400">Nothing logged in this time.</p>
@ -285,7 +285,7 @@ export function CatchingUp({ busy, delayMs = 500 }: { busy: boolean; delayMs?: n
return ( return (
<p <p
role="status" role="status"
className="fixed top-3 left-1/2 z-40 -translate-x-1/2 rounded-full border border-cyan-400/50 bg-white/5 px-4 py-1.5 font-display text-sm text-ink-100 shadow-panel" className="fixed top-3 left-1/2 z-40 -translate-x-1/2 rounded-full border border-glass-line bg-white/5 px-4 py-1.5 font-display text-sm text-ink-100 shadow-panel"
> >
Catching up on your fleet… Catching up on your fleet…
</p> </p>

View File

@ -80,7 +80,7 @@ export function ServiceTag({ ship }: { ship: Ship }) {
<p <p
role="status" role="status"
aria-label={`${ship.name} service`} aria-label={`${ship.name} service`}
className={`rounded-lg border px-3 py-1.5 text-sm ${warn ? "border-amber-400/40 bg-amber-950/30 text-ink-100" : "border-cyan-400/40 bg-cyan-950/30 text-ink-100"}`} className={`rounded-lg border px-3 py-1.5 text-sm ${warn ? "border-amber-400/40 bg-amber-950/30 text-ink-100" : "border-glass-line bg-white/5 text-ink-100"}`}
> >
{tag} {tag}
</p> </p>

View File

@ -22,8 +22,8 @@ function useViewportWidth(): number {
const TONES: Record<Toast["tone"], string> = { const TONES: Record<Toast["tone"], string> = {
good: "border-emerald-400/40 text-emerald-50", good: "border-emerald-400/40 text-emerald-50",
info: "border-cyan-400/40 text-cyan-50", info: "border-glass-line text-ink-50",
warn: "border-amber-400/50 text-amber-50", warn: "border-action/50 text-ink-50",
}; };
/** /**

View File

@ -85,13 +85,13 @@ export function WelcomeCards({
if (from - to > 50) advance(); if (from - to > 50) advance();
else if (to - from > 50 && index > 0) setIndex(index - 1); else if (to - from > 50 && index > 0) setIndex(index - 1);
}} }}
className="w-full max-w-md overflow-hidden rounded-t-2xl border border-amber-400/50 bg-white/5 p-6 pb-[max(1.5rem,env(safe-area-inset-bottom))] shadow-panel sm:rounded-2xl" className="w-full max-w-md overflow-hidden rounded-t-2xl border border-glass-line bg-space-850 p-6 pb-[max(1.5rem,env(safe-area-inset-bottom))] shadow-panel sm:rounded-2xl"
> >
<p aria-live="polite" className="font-display text-xs font-semibold text-ink-300"> <p aria-live="polite" className="font-display text-xs font-semibold text-ink-300">
{index + 1} of {cards.length} {index + 1} of {cards.length}
</p> </p>
<div key={index} data-testid="welcome-card" className={reduced ? "" : "animate-slide"}> <div key={index} data-testid="welcome-card" className={reduced ? "" : "animate-slide"}>
<h2 id="welcome-title" className="mt-2 font-display text-2xl font-bold text-amber-300"> <h2 id="welcome-title" className="mt-2 font-title text-lg leading-snug">
{card.title} {card.title}
</h2> </h2>
<p className="mt-3 text-ink-200">{welcomeBody(card.body)}</p> <p className="mt-3 text-ink-200">{welcomeBody(card.body)}</p>
@ -100,7 +100,7 @@ export function WelcomeCards({
{cards.map((_, i) => ( {cards.map((_, i) => (
<span <span
key={i} key={i}
className={`h-1.5 rounded-full ${i === index ? "w-5 bg-amber-300" : "w-1.5 bg-space-600"}`} className={`h-1.5 rounded-full ${i === index ? "w-5 bg-action" : "w-1.5 bg-white/25"}`}
/> />
))} ))}
</div> </div>

View File

@ -126,7 +126,7 @@ export function PanelHost({ children }: { children: ReactNode }) {
"--sheet-h": `${dragPx ?? detentPx(detent, globalThis.innerHeight ?? 800)}px`, "--sheet-h": `${dragPx ?? detentPx(detent, globalThis.innerHeight ?? 800)}px`,
} as React.CSSProperties } as React.CSSProperties
} }
className={`absolute inset-x-0 bottom-(--nav-h) z-10 flex h-(--sheet-h) flex-col rounded-t-3xl border-t border-glass-line bg-glass shadow-panel md:top-4 md:right-4 md:bottom-auto md:left-auto md:h-auto md:max-h-[calc(100%-2rem)] md:w-(--panel-w) md:rounded-3xl md:border md:backdrop-blur-xl ${ className={`absolute inset-x-0 bottom-(--nav-h) z-10 flex h-(--sheet-h) flex-col rounded-t-3xl border-t border-glass-line bg-space-850/97 shadow-panel md:bg-glass md:top-4 md:right-4 md:bottom-auto md:left-auto md:h-auto md:max-h-[calc(100%-2rem)] md:w-(--panel-w) md:rounded-3xl md:border md:backdrop-blur-xl ${
dragPx === null ? "transition-[height] duration-300 ease-out" : "" dragPx === null ? "transition-[height] duration-300 ease-out" : ""
}`} }`}
> >

View File

@ -138,7 +138,7 @@ function ShipRow(props: {
? `${locationName(dispatch.originId)} → ${locationName(dispatch.destinationId)}` ? `${locationName(dispatch.originId)} → ${locationName(dispatch.destinationId)}`
: `At ${locationName(ship.locationId ?? "")}`} : `At ${locationName(ship.locationId ?? "")}`}
{" · "} {" · "}
<span className={pilotName ? "text-ink-300" : "text-amber-300"}> <span className={pilotName ? "text-ink-300" : "text-problem"}>
{pilotName ?? "No pilot"} {pilotName ?? "No pilot"}
</span> </span>
</span> </span>

View File

@ -1,3 +1,4 @@
import { PlacesViews } from "../components/FleetViews";
import { content } from "@deadhead/content"; import { content } from "@deadhead/content";
import type { Bay } from "@deadhead/schema"; import type { Bay } from "@deadhead/schema";
import { Link } from "react-router"; import { Link } from "react-router";
@ -22,6 +23,7 @@ export function HQs() {
icon="hq" icon="hq"
subtitle={state.bays.length > 0 ? HQ_ALLOWS : undefined} subtitle={state.bays.length > 0 ? HQ_ALLOWS : undefined}
/> />
<PlacesViews />
{state.bays.length === 0 ? ( {state.bays.length === 0 ? (
<Panel tone="muted" className="space-y-3 text-sm"> <Panel tone="muted" className="space-y-3 text-sm">
<p className="text-ink-200">You don&apos;t have an HQ yet.</p> <p className="text-ink-200">You don&apos;t have an HQ yet.</p>
@ -58,7 +60,7 @@ function HqCard({ bay }: { bay: Bay }) {
); );
return ( return (
<Panel as="li" tone="emerald" aria-label={`HQ at ${name}`} className="text-sm"> <Panel as="li" aria-label={`HQ at ${name}`} className="text-sm">
<div className="flex items-center gap-4"> <div className="flex items-center gap-4">
{location && ( {location && (
<Art <Art

View File

@ -96,7 +96,7 @@ export function JobsTable({
New New
</Pill> </Pill>
)} )}
<ClassChip contract={contract} /> {contract.cargoTypeId !== "standard" && <ClassChip contract={contract} />}
{contract.priority && contract.deliveryWindowGameHours !== null && ( {contract.priority && contract.deliveryWindowGameHours !== null && (
<PriorityChip windowHours={contract.deliveryWindowGameHours} /> <PriorityChip windowHours={contract.deliveryWindowGameHours} />
)} )}
@ -132,7 +132,7 @@ export function JobsTable({
<div className="hidden text-xs leading-snug text-ink-400 @2xl:block"> <div className="hidden text-xs leading-snug text-ink-400 @2xl:block">
Pays <span>{formatCredits(contract.payout)}</span> ·{" "} Pays <span>{formatCredits(contract.payout)}</span> ·{" "}
{formatMillionKm(distanceAt(contract.originId, contract.destinationId))} ·{" "} {formatMillionKm(distanceAt(contract.originId, contract.destinationId))} ·{" "}
{requirements ? `Needs ${requirements}` : "No skills needed"} · expires in{" "} {requirements && `Needs ${requirements} · `}expires in{" "}
<Countdown to={contract.expiresAt} done="expired" /> <Countdown to={contract.expiresAt} done="expired" />
</div> </div>
</td> </td>

View File

@ -175,7 +175,7 @@ function LocationBoard({
{fresh.size > 0 && ( {fresh.size > 0 && (
<span <span
role="status" role="status"
className="ml-2 animate-rise rounded-full border border-cyan-400/50 bg-cyan-500/15 px-2 py-0.5 text-ink-100 normal-case" className="ml-2 animate-rise rounded-full border border-glass-line bg-white/5 px-2 py-0.5 text-ink-100 normal-case"
> >
New contracts posted New contracts posted
</span> </span>
@ -278,7 +278,6 @@ function NothingHere({
key={ship.id} key={ship.id}
role="group" role="group"
aria-label={`Nothing for ${ship.name}`} aria-label={`Nothing for ${ship.name}`}
tone="cyan"
className="text-sm" className="text-sm"
> >
<p className="flex items-center gap-2 text-ink-100"> <p className="flex items-center gap-2 text-ink-100">

View File

@ -1,3 +1,4 @@
import { PlacesViews } from "../components/FleetViews";
import { content, type Location } from "@deadhead/content"; import { content, type Location } from "@deadhead/content";
import { FuelPrice } from "../components/Fuel"; import { FuelPrice } from "../components/Fuel";
import { Link } from "react-router"; import { Link } from "react-router";
@ -27,7 +28,8 @@ export function Locations() {
return ( return (
<Screen> <Screen>
<ScreenHeader title="Places" eyebrow="Known space" icon="places" /> <ScreenHeader title="Places" icon="places" />
<PlacesViews />
{lease.isError && ( {lease.isError && (
<p role="alert" className="text-sm text-rose-300"> <p role="alert" className="text-sm text-rose-300">
{errorMessage(lease.error)} {errorMessage(lease.error)}

View File

@ -57,7 +57,7 @@ export function Logbook() {
} }
return ( return (
<Screen className="max-w-3xl"> <Screen>
<ScreenHeader title="Logbook" eyebrow="The last 7 days" icon="route" /> <ScreenHeader title="Logbook" eyebrow="The last 7 days" icon="route" />
<Panel className="flex flex-wrap items-end gap-3 text-sm" aria-label="Logbook filters"> <Panel className="flex flex-wrap items-end gap-3 text-sm" aria-label="Logbook filters">
<label className="flex flex-col gap-1"> <label className="flex flex-col gap-1">

View File

@ -30,7 +30,6 @@ export function Market() {
<Screen> <Screen>
<ScreenHeader <ScreenHeader
title="Shipyard" title="Shipyard"
eyebrow="Shipyard"
icon="market" icon="market"
subtitle="New ships are delivered with no pilot to one of your HQs." subtitle="New ships are delivered with no pilot to one of your HQs."
/> />
@ -53,17 +52,16 @@ export function Market() {
const affordable = state.company.credits >= model.price; const affordable = state.company.credits >= model.price;
return ( return (
<Panel as="li" key={model.id} className="flex flex-col text-sm"> <Panel as="li" key={model.id} className="flex flex-col text-sm">
<div className="relative -mx-4 -mt-4 grid h-40 place-items-center overflow-hidden rounded-t-xl bg-radial from-cyan-400/20 via-space-850/40 to-transparent to-75%"> <div className="relative -mt-1 grid h-32 place-items-center">
<div className="absolute inset-x-8 bottom-4 h-3 rounded-[50%] bg-cyan-300/15 blur-md" />
<Art <Art
sprite={model.sprite} sprite={model.sprite}
className="relative size-32 rotate-[30deg] drop-shadow-[0_10px_14px_rgb(0_0_0/0.8)]" className="relative size-28 drop-shadow-[0_10px_14px_rgb(0_0_0/0.8)]"
/> />
</div> </div>
<div className="mt-2 flex items-baseline justify-between gap-2"> <div className="mt-2 flex items-baseline justify-between gap-2">
<span className="font-display text-xl font-bold">{model.name}</span> <span className="text-lg font-semibold">{model.name}</span>
<span <span
className={`font-display text-base font-bold tabular-nums ${affordable ? "text-amber-300" : "text-ink-400"}`} className={`font-semibold tabular-nums ${affordable ? "text-ink-50" : "text-ink-300"}`}
> >
{formatCredits(model.price)} {formatCredits(model.price)}
</span> </span>
@ -94,6 +92,7 @@ export function Market() {
<OwnedUpgraded modelId={model.id} /> <OwnedUpgraded modelId={model.id} />
<Button <Button
aria-label={`Buy ${model.name}`} aria-label={`Buy ${model.name}`}
variant="secondary"
block block
disabled={!affordable || state.bays.length === 0 || buy.isPending} disabled={!affordable || state.bays.length === 0 || buy.isPending}
onClick={() => setBuying(model)} onClick={() => setBuying(model)}
@ -133,7 +132,7 @@ function Spec(props: { icon: IconName; label: string; value: string; share?: num
{props.share !== undefined && ( {props.share !== undefined && (
<div aria-hidden="true" className="mt-1 h-1 overflow-hidden rounded-full bg-black/25"> <div aria-hidden="true" className="mt-1 h-1 overflow-hidden rounded-full bg-black/25">
<div <div
className="h-full rounded-full bg-linear-to-r from-cyan-500 to-sky-300" className="h-full rounded-full bg-ink-300"
style={{ width: `${Math.max(4, props.share * 100)}%` }} style={{ width: `${Math.max(4, props.share * 100)}%` }}
/> />
</div> </div>

View File

@ -3,8 +3,12 @@ import { Icon, Screen, ScreenHeader, type IconName } from "../ui";
const ROWS: { to: string; title: string; detail: string; icon: IconName }[] = [ const ROWS: { to: string; title: string; detail: string; icon: IconName }[] = [
{ to: "/market", title: "Shipyard", detail: "Compare and buy ships", icon: "market" }, { to: "/market", title: "Shipyard", detail: "Compare and buy ships", icon: "market" },
{ to: "/locations", title: "Places", detail: "Everywhere you can fly", icon: "places" }, {
{ to: "/hqs", title: "HQs", detail: "Your bases and who's there", icon: "hq" }, to: "/locations",
title: "Places & HQs",
detail: "Where you can fly, and your bases",
icon: "places",
},
{ to: "/logbook", title: "Logbook", detail: "Every job and refuel, 7 days", icon: "route" }, { to: "/logbook", title: "Logbook", detail: "Every job and refuel, 7 days", icon: "route" },
{ to: "/settings", title: "Settings", detail: "Company, display and save", icon: "gear" }, { to: "/settings", title: "Settings", detail: "Company, display and save", icon: "gear" },
]; ];

View File

@ -25,7 +25,7 @@ export function Settings() {
const [howToPlay, setHowToPlay] = useState(false); const [howToPlay, setHowToPlay] = useState(false);
return ( return (
<Screen className="max-w-2xl"> <Screen>
<ScreenHeader title="Settings" eyebrow={state.company.name} icon="gear" /> <ScreenHeader title="Settings" eyebrow={state.company.name} icon="gear" />
<Section title="Company"> <Section title="Company">

View File

@ -22,27 +22,23 @@ export function StartScreen() {
return ( return (
<main className="relative flex min-h-dvh flex-col items-center justify-center gap-8 overflow-hidden p-6 text-center text-ink-50"> <main className="relative flex min-h-dvh flex-col items-center justify-center gap-8 overflow-hidden p-6 text-center text-ink-50">
<div aria-hidden="true" className="pointer-events-none absolute inset-0"> <div
aria-hidden="true"
className="pointer-events-none absolute inset-0 overflow-hidden bg-[url(/assets/map-gen/bg.webp)] bg-cover bg-center"
>
<Art <Art
sprite="sun" sprite="body-earth"
className="absolute -top-24 -right-24 size-72 opacity-80 blur-[1px] sm:size-96" className="absolute -bottom-[82vmin] left-1/2 sm:-bottom-[96vmin] size-[120vmin] max-w-none -translate-x-1/2 drop-shadow-[0_-8px_60px_rgb(90_160_255/0.45)]"
/>
<Art sprite="body-earth" className="absolute bottom-[14%] -left-10 size-40 sm:size-56" />
<Art
sprite="body-moon"
className="absolute bottom-[34%] left-[30%] size-10 sm:left-[18%]"
/> />
<Art <Art
sprite="ship-courier" sprite="ship-clipper"
className="absolute top-[22%] left-[12%] size-16 rotate-[60deg] drop-shadow-[0_0_10px_rgb(251_191_36/0.5)]" className="absolute bottom-[20vmin] left-1/2 size-[22vmin] max-h-32 max-w-32 translate-x-16 sm:bottom-[10vmin] sm:translate-x-40 rotate-[34deg] drop-shadow-[0_8px_14px_rgb(0_0_0/0.8)]"
/> />
</div> </div>
<div className="relative animate-rise"> <div className="relative animate-rise">
<h1 className="bg-linear-to-b from-amber-100 via-amber-300 to-amber-600 bg-clip-text font-title text-3xl sm:text-5xl text-transparent drop-shadow-[0_0_24px_rgb(251_191_36/0.35)] sm:text-7xl"> <h1 className="font-title text-3xl sm:text-5xl">DEADHEAD</h1>
DEADHEAD <p className="mt-3 text-sm font-medium text-ink-200">Space Freight Tycoon</p>
</h1>
<p className="mt-2 font-display text-sm text-ink-300">Space Freight Tycoon</p>
</div> </div>
<p className="relative max-w-sm animate-rise text-base text-ink-200"> <p className="relative max-w-sm animate-rise text-base text-ink-200">
Fly company runs between Earth and the Moon, save up for your own ship, and build a freight Fly company runs between Earth and the Moon, save up for your own ship, and build a freight

View File

@ -68,7 +68,7 @@ export function TutorialBoard() {
Company runs in the loaner {loaner.name}. You keep a share of each payout. Company runs in the loaner {loaner.name}. You keep a share of each payout.
</p> </p>
{runInFlight && ( {runInFlight && (
<p className="flex items-center gap-2 rounded-lg border border-cyan-400/40 bg-cyan-950/50 px-3 py-2 text-sm text-ink-100"> <p className="flex items-center gap-2 rounded-lg border border-glass-line bg-white/5 px-3 py-2 text-sm text-ink-100">
<Icon name="send" className="size-4 shrink-0 text-ink-300" /> <Icon name="send" className="size-4 shrink-0 text-ink-300" />
Your run is in flight. Wait for it to land. Your run is in flight. Wait for it to land.
</p> </p>
@ -105,13 +105,13 @@ export function TutorialBoard() {
key={contract.id} key={contract.id}
data-suggested={suggested || undefined} data-suggested={suggested || undefined}
data-scripted={scripted ? true : undefined} data-scripted={scripted ? true : undefined}
tone={scripted ? "fuchsia" : suggested ? "amber" : "default"} tone={scripted || suggested ? "amber" : "default"}
className={`flex flex-col gap-2 ${suggested ? "shadow-glow-amber" : ""}`} className={`flex flex-col gap-2 ${suggested ? "shadow-glow-amber" : ""}`}
> >
<div className="flex items-start justify-between gap-2"> <div className="flex items-start justify-between gap-2">
<span className="font-display font-bold"> <span className="font-display font-bold">
{scripted ? ( {scripted ? (
<span className="text-fuchsia-200">{scripted.name}</span> <span className="text-ink-300">{scripted.name}</span>
) : ( ) : (
<> <>
{locationName(contract.originId)} → {locationName(contract.destinationId)} {locationName(contract.originId)} → {locationName(contract.destinationId)}
@ -129,7 +129,7 @@ export function TutorialBoard() {
</span> </span>
</div> </div>
{scripted && ( {scripted && (
<p className="text-xs text-fuchsia-200"> <p className="text-xs text-ink-300">
{locationName(contract.originId)} → {locationName(contract.destinationId)} · {locationName(contract.originId)} → {locationName(contract.destinationId)} ·
double pay, no questions asked double pay, no questions asked
</p> </p>
@ -160,7 +160,7 @@ export function TutorialBoard() {
<dt className="font-display text-xs text-ink-400">XP</dt> <dt className="font-display text-xs text-ink-400">XP</dt>
<dd <dd
aria-label="XP" aria-label="XP"
className="font-display text-base text-violet-300 tabular-nums" className="font-display text-base text-ink-300 tabular-nums"
> >
+{xp} +{xp}
</dd> </dd>

View File

@ -43,7 +43,7 @@ export function Sheet({
}) { }) {
return createPortal( return createPortal(
<div <div
className={`fixed inset-x-0 top-14 bottom-0 ${layer} flex items-end justify-center bg-space-950/70 sm:items-center sm:p-4 ${side ? "xl:pointer-events-none xl:top-4 xl:items-start xl:justify-end xl:bg-transparent xl:pr-[calc(45rem+2rem)]" : ""}`} className={`fixed inset-x-0 top-14 bottom-0 ${layer} flex items-end justify-center bg-space-950/70 sm:items-center sm:p-4 ${side ? "xl:pointer-events-none xl:top-20 xl:items-start xl:justify-end xl:bg-transparent xl:pr-[calc(45rem+2rem)]" : ""}`}
> >
<div <div
role="dialog" role="dialog"

View File

@ -77,8 +77,7 @@ describe("app layout", () => {
await screen.findByRole("heading", { level: 1, name: "More" }); await screen.findByRole("heading", { level: 1, name: "More" });
for (const [name, href] of [ for (const [name, href] of [
[/^Shipyard/, "/market"], [/^Shipyard/, "/market"],
[/^Places/, "/locations"], [/^Places & HQs/, "/locations"],
[/^HQs/, "/hqs"],
[/^Logbook/, "/logbook"], [/^Logbook/, "/logbook"],
] as const) { ] as const) {
expect(screen.getByRole("link", { name })).toHaveAttribute("href", href); expect(screen.getByRole("link", { name })).toHaveAttribute("href", href);

View File

@ -159,10 +159,8 @@ describe("location contract boards", () => {
const plain = screen.getByRole("row", { name: "Consumer Goods" }); const plain = screen.getByRole("row", { name: "Consumer Goods" });
const special = screen.getByRole("row", { name: "Machine Parts" }); const special = screen.getByRole("row", { name: "Machine Parts" });
// CR-007: good, class chip, shipper, mass and distance. // CR-007: good, class chip, shipper, mass and distance.
expect(plain).toHaveTextContent("Standard");
expect(plain).toHaveTextContent("Pacific Orbital Logistics"); expect(plain).toHaveTextContent("Pacific Orbital Logistics");
expect(plain).toHaveTextContent("50 t"); expect(plain).toHaveTextContent("50 t");
expect(plain).toHaveTextContent("No skills needed");
expect(plain).toHaveTextContent("expires in 30m 00s"); expect(plain).toHaveTextContent("expires in 30m 00s");
expect(within(plain).queryByText("First contact")).not.toBeInTheDocument(); expect(within(plain).queryByText("First contact")).not.toBeInTheDocument();