diff --git a/DECISIONS.md b/DECISIONS.md index 9f5f7e7..cefe652 100644 --- a/DECISIONS.md +++ b/DECISIONS.md @@ -846,3 +846,9 @@ Implementation decisions not spelled out in `memory-bank/implementation-plan.md` - **Jobs badge during the tutorial is 1 until a run is in flight**: the loaner isn't a ship in state, but "go fly a run" is the one thing the player should do next. - **The desktop rail is 96 px with labels under icons**, not a wide sidebar, so the map and screens keep the room. The wordmark hides below 420 px wide so the credits and clock chips fit on small phones. - **Screenshot stack:** a scratchpad Playwright harness (playwright-core driving local Chrome) seeds a tutorial save and a mid-game save through the API on a separate database (`deadhead_shots`, API 3125, web 5199), as in Step 10C. + +## Step 10E.4 + +- **"Fly a company run" on the tutorial HQ panel is the one copy addition so far**: a link to Jobs, shown only when no run is in flight and no deal is waiting. It adds a path; nothing existing changed (decision 0.5.51 is about not changing existing copy and names). +- **`ScreenHeader` is a `div`, not a `header`**: a second `
` was exposed as another `banner` landmark next to the app header. +- **Route strip labels are CSS-generated** (`after:content-[attr(data-label)]`) because tests and screen readers already get the route as text; DOM text in a decorative strip would duplicate it. diff --git a/apps/web/src/components/GoalsCard.tsx b/apps/web/src/components/GoalsCard.tsx index 7e07e6f..db4f315 100644 --- a/apps/web/src/components/GoalsCard.tsx +++ b/apps/web/src/components/GoalsCard.tsx @@ -1,6 +1,7 @@ import { content } from "@deadhead/content"; import { formatCredits } from "../format"; import { errorMessage, useGameAction, useLoadedPlayer } from "../state/actions"; +import { Button, Icon, Panel } from "../ui"; /** How many unclaimed goals HQ shows at once (Step 10A.15). */ export const GOALS_SHOWN = 3; @@ -13,46 +14,61 @@ export function GoalsCard() { if (next.length === 0) return null; return ( -
-

+ +

+ Goals

-
    +
      {next.map((goal) => { const details = content.onboardingGoal(goal.id); if (!details) return null; const ready = goal.status === "ready"; return ( -
    • - - {details.title} - - +{formatCredits(details.reward)} +
    • + + + + {details.title} + + +{formatCredits(details.reward)} + {ready && ( - + )}
    • ); })}
    {claim.isError && ( -

    +

    {errorMessage(claim.error)}

    )} -

+ ); } diff --git a/apps/web/src/components/LoanCard.tsx b/apps/web/src/components/LoanCard.tsx index bc11e5a..87f5d8b 100644 --- a/apps/web/src/components/LoanCard.tsx +++ b/apps/web/src/components/LoanCard.tsx @@ -2,6 +2,7 @@ import { content } from "@deadhead/content"; import { useState } from "react"; import { formatCredits } from "../format"; import { errorMessage, useGameAction, useLoadedPlayer } from "../state/actions"; +import { Button, Meter, Panel } from "../ui"; /** Why a pay-down amount cannot be sent, or null if it can. */ export function payDownProblem(amount: number, credits: number, balance: number): string | null { @@ -23,28 +24,24 @@ export function LoanCard() { const problem = input === "" ? null : payDownProblem(amount, credits, loanBalance); return ( -
+
-

Loan from Halvorsen

- +

Loan from Halvorsen

+ {formatCredits(loanBalance)} left
-

+

{Math.round(content.config.loanRepaymentShare * 100)}% of every payout goes to the lender until it's paid off.

-
-
-
-

{paidOff}% paid off

+ +

+ {paidOff}% paid off +

setInput(e.target.value)} - className="min-w-0 flex-1 rounded-md border border-slate-700 bg-slate-900 px-2 py-1" + className="min-h-11 min-w-0 flex-1 rounded-lg border border-space-600 bg-space-950/70 px-3 font-display tabular-nums placeholder:text-ink-500 focus:border-amber-400 focus:outline-none" /> - +
- {problem &&

{problem}

} + {problem &&

{problem}

} {repay.isError && ( -

+

{errorMessage(repay.error)}

)} -
+
); } diff --git a/apps/web/src/components/RouteStrip.tsx b/apps/web/src/components/RouteStrip.tsx new file mode 100644 index 0000000..25dea8d --- /dev/null +++ b/apps/web/src/components/RouteStrip.tsx @@ -0,0 +1,84 @@ +import { content } from "@deadhead/content"; +import { locationName } from "../format"; +import { Art, Icon } from "../ui"; + +/** + * Origin art → flight line → destination art (CR-005). With `progress` (0–1) + * the line fills and the ship's art rides along it. Decorative: the route is + * always in text nearby, so the strip is hidden from screen readers. + */ +export function RouteStrip({ + originId, + destinationId, + progress, + shipSprite, + size = "md", +}: { + originId: string; + destinationId: string; + progress?: number; + shipSprite?: string; + size?: "sm" | "md"; +}) { + const percent = progress === undefined ? null : Math.max(0, Math.min(1, progress)) * 100; + return ( +