Step 29.7: Dispatch, Fleet and Crew
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
parent
d279ac4ec0
commit
4a33aed970
|
|
@ -291,7 +291,7 @@ export function AwaySummary() {
|
|||
<ul aria-label="What happened" className="space-y-1.5">
|
||||
{summary.direct.map((entry) => (
|
||||
<li key={entry.id} className="flex items-start gap-2 text-ink-100">
|
||||
<Icon name="send" className="mt-0.5 size-4 shrink-0 text-cyan-300" />
|
||||
<Icon name="send" className="mt-0.5 size-4 shrink-0 text-ink-300" />
|
||||
{directText(entry as LogEntry, state)}
|
||||
</li>
|
||||
))}
|
||||
|
|
@ -302,7 +302,7 @@ export function AwaySummary() {
|
|||
{highlights.length > 0 && (
|
||||
<ul aria-label="Highlights" className="space-y-1.5">
|
||||
{highlights.map((h, i) => (
|
||||
<li key={i} className="flex items-start gap-2 text-amber-100">
|
||||
<li key={i} className="flex items-start gap-2 text-ink-100">
|
||||
<Icon
|
||||
name={h.kind === "problem" ? "alert" : "star"}
|
||||
className="mt-0.5 size-4 shrink-0 text-amber-300"
|
||||
|
|
@ -346,7 +346,7 @@ function Totals({ summary }: { summary: Summary }) {
|
|||
[words.totals.deliveries, String(deliveries), "text-ink-50"],
|
||||
];
|
||||
if (discoveries > 0)
|
||||
figures.push([words.totals.discoveries, String(discoveries), "text-cyan-200"]);
|
||||
figures.push([words.totals.discoveries, String(discoveries), "text-ink-100"]);
|
||||
if (loanRepaid > 0)
|
||||
figures.push([words.totals.loanRepaid, formatCredits(loanRepaid), "text-ink-50"]);
|
||||
return (
|
||||
|
|
@ -380,7 +380,7 @@ function PilotLines({ summary }: { summary: Summary }) {
|
|||
})}
|
||||
</span>
|
||||
{line.leveledUp && (
|
||||
<span className="shrink-0 rounded-full bg-amber-500/20 px-1.5 text-xs font-semibold text-amber-200">
|
||||
<span className="shrink-0 rounded-full bg-amber-500/20 px-1.5 text-xs font-semibold text-ink-100">
|
||||
{words.leveledUp}
|
||||
</span>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ export function DealModal() {
|
|||
of every job.
|
||||
</p>
|
||||
</div>
|
||||
<dl className="mt-4 grid grid-cols-[auto_1fr] gap-x-4 gap-y-2 rounded-xl bg-space-950/60 p-3 text-sm ring-1 ring-amber-400/25">
|
||||
<dl className="mt-4 grid grid-cols-[auto_1fr] gap-x-4 gap-y-2 rounded-xl bg-black/25 p-3 text-sm ring-1 ring-amber-400/25">
|
||||
<dt className="font-display text-xs text-ink-400">You get</dt>
|
||||
<dd aria-label="Ships and bay">
|
||||
A {courier.name} for you, a {clipper.name} for {veteran.name}, and a bay at {bay.name}
|
||||
|
|
@ -68,7 +68,7 @@ export function DealModal() {
|
|||
<dt className="font-display text-xs text-ink-400">Loan</dt>
|
||||
<dd
|
||||
aria-label="Loan amount"
|
||||
className="font-display font-semibold text-amber-200 tabular-nums"
|
||||
className="font-display font-semibold text-ink-100 tabular-nums"
|
||||
>
|
||||
{formatCredits(dealLoanAmount)}
|
||||
</dd>
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ export function SleepBanner() {
|
|||
const { sleepsAt, afterHours } = state.fleetSleep;
|
||||
return (
|
||||
<Panel tone="amber" className="text-sm" aria-label="Fleet asleep">
|
||||
<p className="flex items-start gap-2 text-amber-100">
|
||||
<p className="flex items-start gap-2 text-ink-100">
|
||||
<Icon name="rest" className="mt-0.5 size-4 shrink-0" />
|
||||
Your fleet went quiet on {formatSleepTime(sleepsAt)} after {sleepLabel(afterHours)} without
|
||||
orders.
|
||||
|
|
@ -63,7 +63,7 @@ export function FleetAsleep() {
|
|||
if (!fleetQuiet(state)) return null;
|
||||
return (
|
||||
<Panel tone="amber" className="text-sm" aria-label="Fleet asleep">
|
||||
<p className="flex items-center gap-2 font-display font-semibold text-amber-100">
|
||||
<p className="flex items-center gap-2 font-display font-semibold text-ink-100">
|
||||
<Icon name="rest" className="size-4 shrink-0" />
|
||||
Fleet asleep — waiting for your orders
|
||||
</p>
|
||||
|
|
|
|||
|
|
@ -54,7 +54,7 @@ export function FuelGauge({ ship }: { ship: Ship }) {
|
|||
label={`${ship.name} fuel`}
|
||||
value={ship.fuelFu}
|
||||
max={capacity}
|
||||
tone={percent < 25 ? "rose" : "amber"}
|
||||
tone={percent < 25 ? "rose" : "default"}
|
||||
size="sm"
|
||||
/>
|
||||
<div className="mt-1 flex justify-between font-display text-xs text-ink-400">
|
||||
|
|
@ -183,7 +183,7 @@ export function ShipFuel({ shipId, onClose }: { shipId: string; onClose: () => v
|
|||
placeholder="Amount (fu)"
|
||||
value={amount}
|
||||
onChange={(e) => setAmount(e.target.value)}
|
||||
className="w-full rounded-lg border border-space-600 bg-space-950/70 px-3 py-2 text-ink-50 focus:border-amber-400 focus:outline-none"
|
||||
className="w-full rounded-lg border border-glass-line bg-black/25 px-3 py-2 text-ink-50 focus:border-action focus:outline-none"
|
||||
/>
|
||||
</label>
|
||||
<Button
|
||||
|
|
@ -202,11 +202,11 @@ export function ShipFuel({ shipId, onClose }: { shipId: string; onClose: () => v
|
|||
|
||||
<fieldset className="mt-4 text-sm">
|
||||
<legend className="text-ink-400">Refuel policy</legend>
|
||||
<div className="mt-1 flex overflow-hidden rounded-lg border border-space-600">
|
||||
<div className="mt-1 flex overflow-hidden rounded-lg border border-glass-line">
|
||||
{(["manual", "auto"] as const).map((value) => (
|
||||
<label
|
||||
key={value}
|
||||
className="flex-1 cursor-pointer px-3 py-1.5 text-center has-checked:bg-amber-500/20 has-checked:text-amber-100 has-focus-visible:ring-2"
|
||||
className="flex-1 cursor-pointer px-3 py-1.5 text-center has-checked:bg-white/14 has-checked:text-ink-50 has-focus-visible:ring-2"
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import { goodName } from "./Goods";
|
|||
import { serviceStatus } from "./Service";
|
||||
|
||||
const inputClass =
|
||||
"rounded-lg border border-space-600 bg-space-950/70 px-3 py-2 text-sm text-ink-50 focus:border-amber-400 focus:outline-none";
|
||||
"rounded-lg border border-glass-line bg-black/25 px-3 py-2 text-sm text-ink-50 focus:border-action focus:outline-none";
|
||||
|
||||
/** The one-time Dispatch Office on the Bridge (decision 0.5.123). */
|
||||
export function DispatchOfficePanel() {
|
||||
|
|
@ -22,7 +22,7 @@ export function DispatchOfficePanel() {
|
|||
if (state.dispatchOffice.owned) {
|
||||
return (
|
||||
<Panel tone="cyan" className="text-sm" aria-label="Dispatch Office">
|
||||
<p className="flex items-center gap-2 font-display font-semibold text-cyan-100">
|
||||
<p className="flex items-center gap-2 font-display font-semibold text-ink-100">
|
||||
<Icon name="send" className="size-4" />
|
||||
Dispatch Office
|
||||
</p>
|
||||
|
|
@ -100,14 +100,14 @@ export function PilotIndependence({ pilot }: { pilot: Pilot }) {
|
|||
const status = independentStatus(state, pilot);
|
||||
|
||||
return (
|
||||
<div className="mt-3 rounded-lg bg-space-950/50 p-2.5 ring-1 ring-space-700">
|
||||
<div className="mt-3 rounded-lg bg-black/25 p-2.5 ring-1 ring-glass-line">
|
||||
<fieldset>
|
||||
<legend className="font-display text-xs text-ink-400">Flies</legend>
|
||||
<div className="mt-1 flex overflow-hidden rounded-lg border border-space-600">
|
||||
<div className="mt-1 flex overflow-hidden rounded-lg border border-glass-line">
|
||||
{(["manual", "independent"] as const).map((mode) => (
|
||||
<label
|
||||
key={mode}
|
||||
className={`flex-1 px-3 py-1.5 text-center has-checked:bg-amber-500/20 has-checked:text-amber-100 has-focus-visible:ring-2 ${owned ? "cursor-pointer" : "cursor-not-allowed opacity-60"}`}
|
||||
className={`flex-1 px-3 py-1.5 text-center has-checked:bg-white/14 has-checked:text-ink-50 has-focus-visible:ring-2 ${owned ? "cursor-pointer" : "cursor-not-allowed opacity-60"}`}
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
|
|
@ -129,7 +129,7 @@ export function PilotIndependence({ pilot }: { pilot: Pilot }) {
|
|||
)}
|
||||
</fieldset>
|
||||
{status && (
|
||||
<p aria-label={`${pilot.name} status`} className="mt-2 text-cyan-100">
|
||||
<p aria-label={`${pilot.name} status`} className="mt-2 text-ink-100">
|
||||
{status}
|
||||
</p>
|
||||
)}
|
||||
|
|
@ -155,7 +155,7 @@ function OrdersEditor({ pilot }: { pilot: Pilot }) {
|
|||
|
||||
return (
|
||||
<details className="mt-2">
|
||||
<summary className="cursor-pointer font-display text-xs text-amber-200">Orders</summary>
|
||||
<summary className="cursor-pointer font-display text-xs text-ink-100">Orders</summary>
|
||||
<form
|
||||
aria-label={`Orders for ${pilot.name}`}
|
||||
className="mt-2 space-y-3 text-sm"
|
||||
|
|
|
|||
|
|
@ -100,14 +100,12 @@ function EntryLine({ entry, state }: { entry: LogEntry; state: PlayerState }) {
|
|||
{locationName(from)} → {locationName(to)}
|
||||
</span>
|
||||
);
|
||||
const fuel = (cost: number) => (
|
||||
<span className="text-amber-200/90">fuel {formatCredits(cost)}</span>
|
||||
);
|
||||
const fuel = (cost: number) => <span className="text-ink-100">fuel {formatCredits(cost)}</span>;
|
||||
switch (entry.kind) {
|
||||
case "flight":
|
||||
return entry.deadhead ? (
|
||||
<>
|
||||
<Icon name="send" className="size-4 text-cyan-300" />
|
||||
<Icon name="send" className="size-4 text-ink-300" />
|
||||
<span>Deadhead</span>
|
||||
{route(entry.originId, entry.destinationId)}
|
||||
{fuel(entry.fuelCost)}
|
||||
|
|
@ -131,56 +129,56 @@ function EntryLine({ entry, state }: { entry: LogEntry; state: PlayerState }) {
|
|||
);
|
||||
case "refuel":
|
||||
return (
|
||||
<span className="text-amber-200/90">
|
||||
<span className="text-ink-100">
|
||||
{Math.round(entry.boughtFu)} fu at {locationName(entry.locationId)},{" "}
|
||||
{formatCredits(entry.cost)} ({REFUEL_SOURCE[entry.source]})
|
||||
</span>
|
||||
);
|
||||
case "rest":
|
||||
return (
|
||||
<span className="flex items-center gap-1 text-sky-300">
|
||||
<span className="flex items-center gap-1 text-ink-300">
|
||||
<Icon name="rest" className="size-4" />
|
||||
Rested until {formatLocalTime(entry.until)}
|
||||
</span>
|
||||
);
|
||||
case "upgrade":
|
||||
return (
|
||||
<span className="text-cyan-100">
|
||||
<span className="text-ink-100">
|
||||
{shipName(state, entry.shipId)}:{" "}
|
||||
{content.upgrade(entry.upgradeId)?.name ?? entry.upgradeId} level {entry.level} finished
|
||||
</span>
|
||||
);
|
||||
case "discovery":
|
||||
return <span className="text-cyan-200">Discovered {locationName(entry.locationId)}</span>;
|
||||
return <span className="text-ink-100">Discovered {locationName(entry.locationId)}</span>;
|
||||
case "level-up":
|
||||
return (
|
||||
<span className="text-amber-200">
|
||||
<span className="text-ink-100">
|
||||
Reached level {entry.level} · {entry.skillPoints}{" "}
|
||||
{entry.skillPoints === 1 ? "skill point" : "skill points"}
|
||||
</span>
|
||||
);
|
||||
case "event":
|
||||
return (
|
||||
<span className={entry.good ? "text-emerald-200" : "text-amber-200"}>
|
||||
<span className={entry.good ? "text-emerald-200" : "text-ink-100"}>
|
||||
{eventResultText(entry, state)}
|
||||
</span>
|
||||
);
|
||||
case "repair":
|
||||
return (
|
||||
<span className="text-cyan-100">
|
||||
<span className="text-ink-100">
|
||||
{shipName(state, entry.shipId)}: hull repaired from {entry.fromPct}% to 100%,{" "}
|
||||
{formatCredits(entry.cost)}
|
||||
</span>
|
||||
);
|
||||
case "service":
|
||||
return (
|
||||
<span className={entry.phase === "waiting-funds" ? "text-amber-200" : "text-cyan-100"}>
|
||||
<span className={entry.phase === "waiting-funds" ? "text-ink-100" : "text-ink-100"}>
|
||||
{serviceEntryText(entry, shipName(state, entry.shipId))}
|
||||
</span>
|
||||
);
|
||||
case "goal-ready":
|
||||
return (
|
||||
<span className="text-amber-200">
|
||||
<span className="text-ink-100">
|
||||
Goal ready: {content.onboardingGoal(entry.goalId)?.title ?? entry.goalId}
|
||||
</span>
|
||||
);
|
||||
|
|
@ -201,13 +199,13 @@ function PilotSection({ log, state, open }: { log: PilotLog; state: PlayerState;
|
|||
const [shown, setShown] = useState(PAGE);
|
||||
const now = useClock((c) => c.now);
|
||||
return (
|
||||
<details open={open} className="rounded-xl border border-space-600 bg-space-950/50 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">
|
||||
<span className="font-display font-bold">{log.name}</span>
|
||||
{log.xp > 0 && <span className="text-xs text-violet-300">+{log.xp} XP</span>}
|
||||
</summary>
|
||||
{log.now && (
|
||||
<p aria-label={`${log.name} now`} className="mt-1 text-cyan-100">
|
||||
<p aria-label={`${log.name} now`} className="mt-1 text-ink-100">
|
||||
{log.now}
|
||||
</p>
|
||||
)}
|
||||
|
|
@ -246,7 +244,7 @@ export function PilotLogbook({ logbook, state }: { logbook: Logbook; state: Play
|
|||
<span>{totals.deadheads} deadheads</span>
|
||||
<span className="text-emerald-300">{formatCredits(totals.payout)} paid</span>
|
||||
<span className="text-ink-300">{formatCredits(totals.pilotShare)} pilot shares</span>
|
||||
<span className="text-amber-200">{formatCredits(totals.fuelCost)} fuel</span>
|
||||
<span className="text-ink-100">{formatCredits(totals.fuelCost)} fuel</span>
|
||||
<span className="text-ink-300">{formatCredits(totals.loanRepaid)} to the lender</span>
|
||||
<span className={totals.net < 0 ? "text-rose-300" : "text-emerald-300"}>
|
||||
{formatCredits(totals.net)} net
|
||||
|
|
@ -287,7 +285,7 @@ export function CatchingUp({ busy, delayMs = 500 }: { busy: boolean; delayMs?: n
|
|||
return (
|
||||
<p
|
||||
role="status"
|
||||
className="fixed top-3 left-1/2 z-40 -translate-x-1/2 rounded-full border border-cyan-400/50 bg-space-900/95 px-4 py-1.5 font-display text-sm text-cyan-100 shadow-panel"
|
||||
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"
|
||||
>
|
||||
Catching up on your fleet…
|
||||
</p>
|
||||
|
|
|
|||
|
|
@ -70,7 +70,7 @@ function CallBody({ call, state }: { call: RadioCall; state: PlayerState }) {
|
|||
<div className="space-y-3 text-sm">
|
||||
<p className="text-ink-100">{fill(def.text, { pilot })}</p>
|
||||
{ship && <p className="text-ink-400">{ship}</p>}
|
||||
<p aria-label="Time to answer" className="text-amber-200">
|
||||
<p aria-label="Time to answer" className="text-ink-100">
|
||||
<Icon name="clock" className="mr-1.5 inline size-4 align-[-3px]" />
|
||||
Answer within <Countdown to={call.windowEndsAt} format={formatClock} done="now" /> — then{" "}
|
||||
{pilot} will {fallback?.label.toLowerCase()}.
|
||||
|
|
@ -151,7 +151,7 @@ export function RadioCallsPanel() {
|
|||
if (state.radioCalls.length === 0) return null;
|
||||
return (
|
||||
<Panel tone="amber" className="text-sm" aria-label="Radio calls">
|
||||
<p className="flex items-center gap-2 font-display font-semibold text-amber-100">
|
||||
<p className="flex items-center gap-2 font-display font-semibold text-ink-100">
|
||||
<Icon name="alert" className="size-4" />
|
||||
Radio calls ({state.radioCalls.length})
|
||||
</p>
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ export function RouteStrip({
|
|||
{percent === null ? (
|
||||
<Icon
|
||||
name="arrow"
|
||||
className="absolute top-1/2 left-1/2 size-4 -translate-1/2 rounded-full bg-space-900 text-cyan-300"
|
||||
className="absolute top-1/2 left-1/2 size-4 -translate-1/2 rounded-full bg-space-900 text-ink-300"
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
|
|
|
|||
|
|
@ -80,7 +80,7 @@ export function ServiceTag({ ship }: { ship: Ship }) {
|
|||
<p
|
||||
role="status"
|
||||
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-amber-100" : "border-cyan-400/40 bg-cyan-950/30 text-cyan-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-cyan-400/40 bg-cyan-950/30 text-ink-100"}`}
|
||||
>
|
||||
{tag}
|
||||
</p>
|
||||
|
|
@ -149,7 +149,7 @@ function BookingForm({ ship, onDone }: { ship: Ship; onDone: () => void }) {
|
|||
checked={repair && canRepair}
|
||||
disabled={!canRepair}
|
||||
onChange={(e) => setRepair(e.target.checked)}
|
||||
className="size-4 accent-amber-400"
|
||||
className="size-4 accent-action"
|
||||
/>
|
||||
Repair to 100%
|
||||
{!canRepair && <span className="text-ink-400">(hull is whole)</span>}
|
||||
|
|
@ -160,7 +160,7 @@ function BookingForm({ ship, onDone }: { ship: Ship; onDone: () => void }) {
|
|||
aria-label="Upgrade"
|
||||
value={upgradeId ?? ""}
|
||||
onChange={(e) => setUpgradeId(e.target.value || null)}
|
||||
className="mt-1 block w-full rounded-lg border border-space-600 bg-space-950 px-2 py-1.5"
|
||||
className="mt-1 block w-full rounded-lg border border-glass-line bg-space-950 px-2 py-1.5"
|
||||
>
|
||||
<option value="">None</option>
|
||||
{upgrades.map((u) => (
|
||||
|
|
@ -176,7 +176,7 @@ function BookingForm({ ship, onDone }: { ship: Ship; onDone: () => void }) {
|
|||
aria-label="HQ"
|
||||
value={hqId}
|
||||
onChange={(e) => setHqId(e.target.value)}
|
||||
className="mt-1 block w-full rounded-lg border border-space-600 bg-space-950 px-2 py-1.5"
|
||||
className="mt-1 block w-full rounded-lg border border-glass-line bg-space-950 px-2 py-1.5"
|
||||
>
|
||||
{hqs.map((id) => (
|
||||
<option key={id} value={id}>
|
||||
|
|
@ -221,14 +221,14 @@ function AutoRepair({ ship }: { ship: Ship }) {
|
|||
const { minPct, maxPct, stepPct } = content.config.autoRepair;
|
||||
const { on, thresholdPct } = ship.autoRepair;
|
||||
return (
|
||||
<div className="mt-3 border-t border-space-700 pt-3">
|
||||
<div className="mt-3 border-t border-glass-line pt-3">
|
||||
<label className="flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={on}
|
||||
disabled={save.isPending}
|
||||
onChange={(e) => save.mutate({ shipId: ship.id, on: e.target.checked, thresholdPct })}
|
||||
className="size-4 accent-amber-400"
|
||||
className="size-4 accent-action"
|
||||
/>
|
||||
Auto-repair below {thresholdPct}%
|
||||
</label>
|
||||
|
|
@ -241,7 +241,7 @@ function AutoRepair({ ship }: { ship: Ship }) {
|
|||
value={thresholdPct}
|
||||
disabled={!on}
|
||||
onChange={(e) => save.mutate({ shipId: ship.id, on, thresholdPct: Number(e.target.value) })}
|
||||
className="mt-2 w-full accent-amber-400"
|
||||
className="mt-2 w-full accent-action"
|
||||
/>
|
||||
<p className="mt-1 text-xs text-ink-400">
|
||||
With an Independent pilot, a repair is booked at the nearest HQ when the hull drops below
|
||||
|
|
@ -272,7 +272,7 @@ export function ServiceSection({ shipId }: { shipId: string }) {
|
|||
return (
|
||||
<section
|
||||
aria-label="Service"
|
||||
className="mt-4 rounded-xl border border-space-600 bg-space-900/60 p-3 text-sm"
|
||||
className="mt-4 rounded-xl border border-glass-line bg-white/5 p-3 text-sm"
|
||||
>
|
||||
<div className="flex items-baseline justify-between gap-2">
|
||||
<span className="font-display font-bold">Service</span>
|
||||
|
|
|
|||
|
|
@ -74,7 +74,7 @@ export function SkillPointPrompt() {
|
|||
{ready.map((pilot, i) => (
|
||||
<span key={pilot.id}>
|
||||
{i > 0 && " · "}
|
||||
<strong className="font-semibold text-cyan-200">{callName(pilot)}:</strong>{" "}
|
||||
<strong className="font-semibold text-ink-100">{callName(pilot)}:</strong>{" "}
|
||||
{pilot.skillPointsAvailable} {pilot.skillPointsAvailable === 1 ? "point" : "points"}
|
||||
</span>
|
||||
))}
|
||||
|
|
|
|||
|
|
@ -80,7 +80,7 @@ function ToastItem({
|
|||
onTouchEnd={() => setPaused(false)}
|
||||
onPointerDown={onPointerDown}
|
||||
onPointerUp={onPointerUp}
|
||||
className={`pointer-events-auto flex items-start gap-2 rounded-xl border bg-space-900/95 py-2 pr-1.5 pl-3 text-sm shadow-panel backdrop-blur ${TONES[toast.tone]} ${motion ? (desktop ? "animate-slide" : "animate-rise") : ""}`}
|
||||
className={`pointer-events-auto flex items-start gap-2 rounded-xl border bg-white/5 py-2 pr-1.5 pl-3 text-sm shadow-panel backdrop-blur ${TONES[toast.tone]} ${motion ? (desktop ? "animate-slide" : "animate-rise") : ""}`}
|
||||
>
|
||||
<button type="button" onClick={open} className="min-w-0 flex-1 py-0.5 text-left">
|
||||
{toast.text}
|
||||
|
|
@ -136,7 +136,7 @@ export function Toasts() {
|
|||
dropOverflow();
|
||||
void navigate("/logbook");
|
||||
}}
|
||||
className="pointer-events-auto self-end rounded-full border border-space-600 bg-space-900/95 px-3 py-1 font-display text-xs text-ink-200 shadow-panel"
|
||||
className="pointer-events-auto self-end rounded-full border border-glass-line bg-white/5 px-3 py-1 font-display text-xs text-ink-200 shadow-panel"
|
||||
>
|
||||
+{more} more
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@ export function WelcomeCards({
|
|||
};
|
||||
|
||||
return createPortal(
|
||||
<div className="fixed inset-0 z-40 flex items-end justify-center bg-space-950/80 backdrop-blur-sm sm:items-center sm:p-4">
|
||||
<div className="fixed inset-0 z-40 flex items-end justify-center bg-black/25 backdrop-blur-sm sm:items-center sm:p-4">
|
||||
<div
|
||||
ref={dialog}
|
||||
role="dialog"
|
||||
|
|
@ -85,9 +85,9 @@ export function WelcomeCards({
|
|||
if (from - to > 50) advance();
|
||||
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-space-900/95 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-amber-400/50 bg-white/5 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-cyan-300/80">
|
||||
<p aria-live="polite" className="font-display text-xs font-semibold text-ink-300">
|
||||
{index + 1} of {cards.length}
|
||||
</p>
|
||||
<div key={index} data-testid="welcome-card" className={reduced ? "" : "animate-slide"}>
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import { usePreferences } from "../state/preferences";
|
|||
import { Button, Sheet } from "../ui";
|
||||
|
||||
const inputClass =
|
||||
"w-full rounded-lg border border-space-600 bg-space-950/70 px-3 py-2 text-sm text-ink-50 focus:border-amber-400 focus:outline-none";
|
||||
"w-full rounded-lg border border-glass-line bg-black/25 px-3 py-2 text-sm text-ink-50 focus:border-action focus:outline-none";
|
||||
|
||||
/** The company name, editable in place with the server's own rules (decision 0.5.75). */
|
||||
export function CompanyName() {
|
||||
|
|
|
|||
|
|
@ -19,7 +19,9 @@ export function Contracts() {
|
|||
icon="jobs"
|
||||
/>
|
||||
{state.tutorial ? <TutorialBoard /> : <LocationBoards onSelect={setSelected} />}
|
||||
{selected && <DispatchSheet contract={selected} onClose={() => setSelected(null)} />}
|
||||
{selected && (
|
||||
<DispatchSheet key={selected.id} contract={selected} onClose={() => setSelected(null)} />
|
||||
)}
|
||||
</Screen>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -299,7 +299,7 @@ function YourShip({ className = "" }: { className?: string }) {
|
|||
)}
|
||||
|
||||
{inShop && (
|
||||
<p className="font-display text-sm text-amber-200">
|
||||
<p className="font-display text-sm text-ink-100">
|
||||
In the shop — ready in <Countdown to={ship!.inShopUntil!} done="now" />
|
||||
</p>
|
||||
)}
|
||||
|
|
@ -355,12 +355,12 @@ function PilotDuty({ pilot }: { pilot: Pilot }) {
|
|||
{pilot.name} · Level {pilot.level}
|
||||
</Eyebrow>
|
||||
{pilot.restingInShopUntil !== null ? (
|
||||
<p className="mt-1 flex items-center gap-1.5 text-sm text-sky-200">
|
||||
<p className="mt-1 flex items-center gap-1.5 text-sm text-ink-100">
|
||||
<Icon name="rest" className="size-4 shrink-0" />
|
||||
<ShopRest until={pilot.restingInShopUntil} />
|
||||
</p>
|
||||
) : pilot.resting && pilot.restEndsAt !== null ? (
|
||||
<p className="mt-1 flex items-center gap-1.5 text-sm text-sky-200">
|
||||
<p className="mt-1 flex items-center gap-1.5 text-sm text-ink-100">
|
||||
<Icon name="rest" className="size-4 shrink-0" />
|
||||
Resting · wakes in <Countdown to={pilot.restEndsAt} done="any moment" />
|
||||
</p>
|
||||
|
|
@ -370,7 +370,7 @@ function PilotDuty({ pilot }: { pilot: Pilot }) {
|
|||
label="Your duty"
|
||||
value={Math.min(pilot.dutyHours, pilot.dutyLimit)}
|
||||
max={pilot.dutyLimit}
|
||||
tone={pilot.dutyHours / pilot.dutyLimit >= 0.85 ? "rose" : "amber"}
|
||||
tone={pilot.dutyHours / pilot.dutyLimit >= 0.85 ? "rose" : "default"}
|
||||
size="sm"
|
||||
className="mt-2"
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import { Button } from "../ui";
|
|||
|
||||
const MINUTE = 60_000;
|
||||
const selectClass =
|
||||
"min-w-0 flex-1 rounded-lg border border-space-600 bg-space-900 px-2 py-1.5 text-sm text-ink-100";
|
||||
"min-w-0 flex-1 rounded-lg border border-glass-line bg-space-900 px-2 py-1.5 text-sm text-ink-100";
|
||||
|
||||
/**
|
||||
* Settings → Developer (non-production builds only, CR-011): fast-forward the
|
||||
|
|
|
|||
|
|
@ -31,7 +31,6 @@ import {
|
|||
import { requirementsText } from "./LocationBoards";
|
||||
import { jobMoney, location, tripRealMs, shipStatsOf, simShip } from "../game";
|
||||
import { errorMessage, useGameAction, useLoadedPlayer } from "../state/actions";
|
||||
import { RouteStrip } from "../components/RouteStrip";
|
||||
import { Art, Button, Sheet } from "../ui";
|
||||
import { goodName, PriorityChip } from "../components/Goods";
|
||||
import {
|
||||
|
|
@ -364,6 +363,7 @@ export function DispatchSheet({ contract, onClose }: { contract: Contract; onClo
|
|||
<Sheet
|
||||
titleId="dispatch-title"
|
||||
eyebrow="Dispatch"
|
||||
side
|
||||
title={`${locationName(contract.originId)} → ${locationName(contract.destinationId)}`}
|
||||
>
|
||||
<p aria-label="Job details" className="mt-1 text-sm text-ink-300">
|
||||
|
|
@ -378,9 +378,6 @@ export function DispatchSheet({ contract, onClose }: { contract: Contract; onClo
|
|||
<PriorityChip windowHours={contract.deliveryWindowGameHours} />
|
||||
</div>
|
||||
)}
|
||||
<div className="mt-3">
|
||||
<RouteStrip originId={contract.originId} destinationId={contract.destinationId} />
|
||||
</div>
|
||||
|
||||
{options.length === 0 ? (
|
||||
<p className="mt-3 text-sm text-ink-400">
|
||||
|
|
@ -397,10 +394,10 @@ export function DispatchSheet({ contract, onClose }: { contract: Contract; onClo
|
|||
aria-disabled={blocked}
|
||||
className={`block rounded-xl border p-3 text-sm transition ${
|
||||
blocked
|
||||
? "border-space-700 bg-space-950/40 opacity-70"
|
||||
? "border-glass-line bg-black/25 opacity-70"
|
||||
: picked
|
||||
? "cursor-pointer border-amber-400 bg-amber-500/10 shadow-glow-amber"
|
||||
: "cursor-pointer border-space-600 bg-space-900/60 hover:border-space-500"
|
||||
? "cursor-pointer border-action bg-action/8"
|
||||
: "cursor-pointer border-glass-line bg-white/5 hover:border-glass-line"
|
||||
}`}
|
||||
>
|
||||
<div className="flex items-center gap-3">
|
||||
|
|
@ -411,16 +408,16 @@ export function DispatchSheet({ contract, onClose }: { contract: Contract; onClo
|
|||
disabled={blocked}
|
||||
checked={picked}
|
||||
onChange={() => setChosen(option.shipId)}
|
||||
className="size-4 shrink-0 accent-amber-400"
|
||||
className="size-4 shrink-0 accent-action"
|
||||
/>
|
||||
<Art sprite={option.modelSprite} className="size-10 shrink-0" />
|
||||
<span className="min-w-0">
|
||||
<span className="block font-display font-bold">{option.shipName}</span>
|
||||
<span className="block font-semibold">{option.shipName}</span>
|
||||
<span className="block text-xs text-ink-400">
|
||||
{option.modelName} · {option.pilotName ?? "no pilot"}
|
||||
</span>
|
||||
{option.service && (
|
||||
<span aria-label="Service" className="block text-xs text-cyan-200">
|
||||
<span aria-label="Service" className="block text-xs text-ink-100">
|
||||
{option.service}
|
||||
</span>
|
||||
)}
|
||||
|
|
@ -438,8 +435,13 @@ export function DispatchSheet({ contract, onClose }: { contract: Contract; onClo
|
|||
))}
|
||||
</ul>
|
||||
) : (
|
||||
<dl className="mt-3 grid grid-cols-2 gap-2 sm:grid-cols-3">
|
||||
<Cell label="Trip" value={formatDuration(option.tripMs)} />
|
||||
<dl className="mt-3 grid grid-cols-2 gap-x-3 gap-y-2">
|
||||
<div className="col-span-2">
|
||||
<dt className="text-xs font-semibold text-ink-300">Net profit</dt>
|
||||
<dd className="text-3xl leading-tight font-semibold text-earned tabular-nums">
|
||||
{formatCredits(option.netProfit)}
|
||||
</dd>
|
||||
</div>
|
||||
<Cell
|
||||
label="Fuel used (at your average price)"
|
||||
value={formatCredits(option.fuelCost)}
|
||||
|
|
@ -448,7 +450,7 @@ export function DispatchSheet({ contract, onClose }: { contract: Contract; onClo
|
|||
!state.pilots.find((p) => p.id === option.pilotId)?.isAvatar && (
|
||||
<Cell label="Pilot share" value={formatCredits(option.pilotPay)} />
|
||||
)}
|
||||
<Cell label="Net profit" value={formatCredits(option.netProfit)} good />
|
||||
<Cell label="Trip" value={formatDuration(option.tripMs)} />
|
||||
<Cell
|
||||
label="ETA (game)"
|
||||
value={formatGameTime(
|
||||
|
|
@ -481,7 +483,7 @@ export function DispatchSheet({ contract, onClose }: { contract: Contract; onClo
|
|||
/>
|
||||
)}
|
||||
{!blocked && option.priority && (
|
||||
<p aria-label="Priority margin" className="mt-2 text-amber-200">
|
||||
<p aria-label="Priority margin" className="mt-2 text-ink-100">
|
||||
Priority: {formatGameHoursSpan(option.priority.tripHours, prefsCountdown)}{" "}
|
||||
trip,{" "}
|
||||
{formatGameHoursSpan(
|
||||
|
|
@ -520,15 +522,11 @@ export function DispatchSheet({ contract, onClose }: { contract: Contract; onClo
|
|||
}
|
||||
|
||||
/** One number in a pair's outlook; `dt` directly before `dd` ("Trip2h 05m"). */
|
||||
function Cell({ label, value, good }: { label: string; value: string; good?: boolean }) {
|
||||
function Cell({ label, value }: { label: string; value: string }) {
|
||||
return (
|
||||
<div className="rounded-lg bg-space-950/60 px-2 py-1.5 ring-1 ring-space-700">
|
||||
<dt className="font-display text-xs text-ink-400">{label}</dt>
|
||||
<dd
|
||||
className={`font-display text-sm font-semibold tabular-nums ${good ? "text-emerald-300" : "text-ink-50"}`}
|
||||
>
|
||||
{value}
|
||||
</dd>
|
||||
<div>
|
||||
<dt className="text-xs text-ink-300">{label}</dt>
|
||||
<dd className="text-sm font-semibold tabular-nums">{value}</dd>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ export function Fleet() {
|
|||
<Art sprite="ship-courier" className="size-20 opacity-60" />
|
||||
<p className="text-sm text-ink-300">
|
||||
No ships yet —{" "}
|
||||
<Link to="/market" className="font-semibold text-amber-300 underline">
|
||||
<Link to="/market" className="font-semibold underline">
|
||||
visit the Shipyard
|
||||
</Link>
|
||||
.
|
||||
|
|
@ -110,17 +110,17 @@ function ShipRow(props: {
|
|||
const blocked = findWorkBlock(ship, props.pilot, now);
|
||||
|
||||
return (
|
||||
<Panel as="li" tone={dispatch ? "cyan" : "default"} className="flex flex-col gap-3">
|
||||
<Panel as="li" className="flex flex-col gap-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="relative grid size-20 shrink-0 place-items-center rounded-xl bg-radial from-cyan-400/15 to-transparent to-70%">
|
||||
<div className="relative grid size-14 shrink-0 place-items-center">
|
||||
<Art
|
||||
sprite={model.sprite}
|
||||
className={`size-18 drop-shadow-[0_6px_10px_rgb(0_0_0/0.7)] ${dispatch ? "rotate-45" : ""}`}
|
||||
className={`size-14 drop-shadow-[0_6px_10px_rgb(0_0_0/0.7)] ${dispatch ? "rotate-45" : ""}`}
|
||||
/>
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<span className="truncate font-display text-lg font-bold">{ship.name}</span>
|
||||
<span className="truncate text-base font-semibold">{ship.name}</span>
|
||||
{dispatch ? (
|
||||
<Pill tone="cyan" icon="send">
|
||||
In flight
|
||||
|
|
@ -131,7 +131,7 @@ function ShipRow(props: {
|
|||
<Pill tone="emerald">Idle</Pill>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-xs text-ink-400">{shipModelName(ship.modelId)}</div>
|
||||
<div className="text-xs text-ink-300">{shipModelName(ship.modelId)}</div>
|
||||
<div className="mt-1 flex flex-wrap items-center gap-x-1 gap-y-1 text-sm text-ink-200">
|
||||
<span>
|
||||
{dispatch
|
||||
|
|
@ -154,15 +154,17 @@ function ShipRow(props: {
|
|||
<span>{progress}%</span>
|
||||
<span>
|
||||
Arrives in{" "}
|
||||
<span className="text-cyan-200">
|
||||
<span className="text-ink-100">
|
||||
<Countdown to={dispatch.arrivalAt} />
|
||||
</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="grid grid-cols-2 gap-3">
|
||||
<FuelGauge ship={ship} />
|
||||
<HullGauge ship={ship} />
|
||||
</div>
|
||||
<ServiceTag ship={ship} />
|
||||
{low && (
|
||||
<p
|
||||
|
|
@ -177,7 +179,7 @@ function ShipRow(props: {
|
|||
</p>
|
||||
)}
|
||||
{inShop && (
|
||||
<p aria-label={`${ship.name} shop`} className="font-display text-sm text-amber-200">
|
||||
<p aria-label={`${ship.name} shop`} className="font-display text-sm text-ink-100">
|
||||
{ship.repairing ? "Under repair" : "In the shop"} —{" "}
|
||||
<Countdown to={ship.inShopUntil!} format={formatClock} done="done" />
|
||||
</p>
|
||||
|
|
@ -217,17 +219,18 @@ function ShipRow(props: {
|
|||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={props.onDeadhead}
|
||||
className="flex-1 basis-40"
|
||||
aria-label="Deadhead (fly empty)"
|
||||
className="flex-1 basis-32"
|
||||
>
|
||||
<Icon name="send" className="size-4" />
|
||||
Deadhead (fly empty)
|
||||
Fly empty
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={props.onFuel}
|
||||
aria-label={`Fuel for ${ship.name}`}
|
||||
|
|
@ -236,7 +239,7 @@ function ShipRow(props: {
|
|||
Fuel
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
onClick={props.onUpgrades}
|
||||
aria-label={`Upgrades for ${ship.name}`}
|
||||
|
|
@ -378,7 +381,7 @@ export function DeadheadPicker({ ship, onClose }: { ship: Ship; onClose: () => v
|
|||
{ onSuccess: onClose },
|
||||
)
|
||||
}
|
||||
className="flex w-full items-center gap-3 rounded-xl border border-space-600 bg-space-900/60 p-3 text-left transition hover:border-amber-400/70 hover:bg-amber-500/10 active:scale-[0.99] disabled:pointer-events-none disabled:opacity-55"
|
||||
className="flex w-full items-center gap-3 rounded-xl border border-glass-line bg-white/5 p-3 text-left transition hover:border-amber-400/70 hover:bg-amber-500/10 active:scale-[0.99] disabled:pointer-events-none disabled:opacity-55"
|
||||
>
|
||||
{sprite && <Art sprite={sprite} className="size-10 shrink-0" />}
|
||||
<span className="min-w-0 flex-1">
|
||||
|
|
|
|||
|
|
@ -110,7 +110,7 @@ function HqCard({ bay }: { bay: Bay }) {
|
|||
{state.ships.find((s) => s.id === d.shipId)?.name ?? "A ship"} from{" "}
|
||||
{locationName(d.originId)}
|
||||
</span>
|
||||
<span className="font-display text-cyan-200">
|
||||
<span className="font-display text-ink-100">
|
||||
<Countdown to={d.arrivalAt} />
|
||||
</span>
|
||||
</li>
|
||||
|
|
|
|||
|
|
@ -175,7 +175,7 @@ function LocationBoard({
|
|||
{fresh.size > 0 && (
|
||||
<span
|
||||
role="status"
|
||||
className="ml-2 animate-rise rounded-full border border-cyan-400/50 bg-cyan-500/15 px-2 py-0.5 text-cyan-100 normal-case"
|
||||
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"
|
||||
>
|
||||
New contracts posted
|
||||
</span>
|
||||
|
|
@ -189,7 +189,7 @@ function LocationBoard({
|
|||
<div className="space-y-3">
|
||||
<Panel tone="muted" role="status" className="text-center text-sm text-ink-300">
|
||||
No jobs here your fleet can take right now. New contracts in{" "}
|
||||
<span className="font-display text-cyan-200 tabular-nums">
|
||||
<span className="font-display text-ink-100 tabular-nums">
|
||||
<Countdown to={board.data.periodEndsAt} format={formatClock} done="a moment" />
|
||||
</span>
|
||||
.
|
||||
|
|
@ -281,8 +281,8 @@ function NothingHere({
|
|||
tone="cyan"
|
||||
className="text-sm"
|
||||
>
|
||||
<p className="flex items-center gap-2 text-cyan-100">
|
||||
<Icon name="alert" className="size-4 shrink-0 text-cyan-300" />
|
||||
<p className="flex items-center gap-2 text-ink-100">
|
||||
<Icon name="alert" className="size-4 shrink-0 text-ink-300" />
|
||||
No job here that {ship.name} and {pilot.name} can take.
|
||||
</p>
|
||||
<div className="mt-3 flex flex-wrap gap-2">
|
||||
|
|
|
|||
|
|
@ -81,14 +81,14 @@ export function Locations() {
|
|||
</Pill>
|
||||
)}
|
||||
</div>
|
||||
<span className="font-display text-xs text-cyan-300/80">
|
||||
<span className="font-display text-xs text-ink-300">
|
||||
{location.id === "earth"
|
||||
? "Home"
|
||||
: `${formatMillionKm(distanceAt("earth", location.id))} from Earth`}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<dl className="mt-3 grid grid-cols-[auto_1fr] gap-x-4 gap-y-1 rounded-lg bg-space-950/50 p-2.5 text-ink-200 ring-1 ring-space-700">
|
||||
<dl className="mt-3 grid grid-cols-[auto_1fr] gap-x-4 gap-y-1 rounded-lg bg-black/25 p-2.5 text-ink-200 ring-1 ring-glass-line">
|
||||
<dt className="font-display text-xs text-ink-400">Type</dt>
|
||||
<dd>{TYPE_LABELS[location.type]}</dd>
|
||||
<dt className="font-display text-xs text-ink-400">Landing</dt>
|
||||
|
|
|
|||
|
|
@ -65,7 +65,7 @@ export function Logbook() {
|
|||
<select
|
||||
value={pilotId}
|
||||
onChange={(e) => set("pilot", e.target.value)}
|
||||
className="rounded-lg border border-space-600 bg-space-900 px-2 py-1.5"
|
||||
className="rounded-lg border border-glass-line bg-space-900 px-2 py-1.5"
|
||||
>
|
||||
<option value="">All pilots</option>
|
||||
{state.pilots.map((p) => (
|
||||
|
|
@ -77,11 +77,11 @@ export function Logbook() {
|
|||
</label>
|
||||
<fieldset className="flex flex-col gap-1">
|
||||
<legend className="text-ink-400">Time</legend>
|
||||
<div className="flex overflow-hidden rounded-lg border border-space-600">
|
||||
<div className="flex overflow-hidden rounded-lg border border-glass-line">
|
||||
{RANGES.map(([value, label]) => (
|
||||
<label
|
||||
key={value}
|
||||
className="cursor-pointer px-3 py-1.5 has-checked:bg-amber-500/20 has-checked:text-amber-100 has-focus-visible:ring-2"
|
||||
className="cursor-pointer px-3 py-1.5 has-checked:bg-white/14 has-checked:text-ink-50 has-focus-visible:ring-2"
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
|
|
|
|||
|
|
@ -171,7 +171,7 @@ function FlightPanel({ dispatchId, onClose }: { dispatchId: string; onClose: ()
|
|||
<p className="text-ink-300">Pilot: {pilotName}</p>
|
||||
<p className="text-ink-300">
|
||||
Arrives in{" "}
|
||||
<span className="font-display text-cyan-200">
|
||||
<span className="font-display text-ink-100">
|
||||
<Countdown to={dispatch.arrivalAt} />
|
||||
</span>{" "}
|
||||
({formatGameTime(realToGame(dispatch.arrivalAt, content.config), clockFormat)})
|
||||
|
|
|
|||
|
|
@ -131,7 +131,7 @@ function Spec(props: { icon: IconName; label: string; value: string; share?: num
|
|||
<dd className="font-display font-semibold tabular-nums">{props.value}</dd>
|
||||
</div>
|
||||
{props.share !== undefined && (
|
||||
<div aria-hidden="true" className="mt-1 h-1 overflow-hidden rounded-full bg-space-950/80">
|
||||
<div aria-hidden="true" className="mt-1 h-1 overflow-hidden rounded-full bg-black/25">
|
||||
<div
|
||||
className="h-full rounded-full bg-linear-to-r from-cyan-500 to-sky-300"
|
||||
style={{ width: `${Math.max(4, props.share * 100)}%` }}
|
||||
|
|
@ -192,14 +192,14 @@ function DeliveryPicker(props: {
|
|||
return (
|
||||
<label
|
||||
key={bay.id}
|
||||
className="flex cursor-pointer items-center gap-3 rounded-xl border border-space-600 bg-space-900/60 p-3 text-sm transition has-checked:border-amber-400 has-checked:bg-amber-500/10 has-checked:shadow-glow-amber"
|
||||
className="flex cursor-pointer items-center gap-3 rounded-xl border border-glass-line bg-white/5 p-3 text-sm transition has-checked:border-amber-400 has-checked:bg-amber-500/10 has-checked:shadow-glow-amber"
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
name="hq"
|
||||
checked={bayId === bay.id}
|
||||
onChange={() => setBayId(bay.id)}
|
||||
className="size-4 accent-amber-400"
|
||||
className="size-4 accent-action"
|
||||
/>
|
||||
{sprite && <Art sprite={sprite} className="size-8" />}
|
||||
<span className="font-display font-semibold">{locationName(bay.locationId)}</span>
|
||||
|
|
@ -230,7 +230,7 @@ function OwnedUpgraded({ modelId }: { modelId: string }) {
|
|||
);
|
||||
if (upgraded.length === 0) return null;
|
||||
return (
|
||||
<ul aria-label="Your upgraded ships" className="mt-3 space-y-1 text-xs text-cyan-200">
|
||||
<ul aria-label="Your upgraded ships" className="mt-3 space-y-1 text-xs text-ink-100">
|
||||
{upgraded.map((ship) => {
|
||||
const s = shipStatsOf(ship);
|
||||
return (
|
||||
|
|
|
|||
|
|
@ -55,21 +55,18 @@ function HirePilot() {
|
|||
const hasBay = state.bays.some((b) => b.locationId === bay);
|
||||
|
||||
return (
|
||||
<Panel tone="amber" className="text-sm">
|
||||
<Panel className="text-sm">
|
||||
{state.bays.length === 0 ? (
|
||||
<p className="text-ink-300">Lease a bay to hire pilots there.</p>
|
||||
) : (
|
||||
<div className="flex flex-wrap items-center gap-3">
|
||||
<span className="grid size-10 shrink-0 place-items-center rounded-full bg-amber-500/15 text-amber-300 ring-1 ring-amber-400/40">
|
||||
<Icon name="hire" className="size-5" />
|
||||
</span>
|
||||
<label className="flex items-center gap-2">
|
||||
<span className="font-display text-xs text-ink-300">Hire at</span>
|
||||
<select
|
||||
aria-label="Hire at"
|
||||
value={bay}
|
||||
onChange={(e) => setBay(e.target.value)}
|
||||
className="min-h-11 rounded-lg border border-space-600 bg-space-950/70 px-3 font-display text-sm focus:border-amber-400 focus:outline-none"
|
||||
className="min-h-11 rounded-lg border border-glass-line bg-black/25 px-3 font-display text-sm focus:border-action focus:outline-none"
|
||||
>
|
||||
{state.bays.map((b) => (
|
||||
<option key={b.id} value={b.locationId}>
|
||||
|
|
@ -79,6 +76,8 @@ function HirePilot() {
|
|||
</select>
|
||||
</label>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
disabled={!hasBay || state.company.credits < cost || hire.isPending}
|
||||
onClick={() => hire.mutate({ locationId: bay })}
|
||||
className="ml-auto"
|
||||
|
|
@ -114,29 +113,29 @@ function PilotCard({ pilot }: { pilot: Pilot }) {
|
|||
<div className="flex items-center gap-3">
|
||||
<span
|
||||
aria-hidden="true"
|
||||
className={`relative grid size-14 shrink-0 place-items-center rounded-full font-display text-lg font-bold ring-2 ${
|
||||
pilot.isAvatar
|
||||
? "bg-amber-500/15 text-amber-200 ring-amber-400/70 shadow-glow-amber"
|
||||
: "bg-cyan-500/10 text-cyan-200 ring-cyan-400/50"
|
||||
className={`relative grid size-12 shrink-0 place-items-center rounded-full text-base font-semibold ring-1 ${
|
||||
pilot.isAvatar ? "text-action ring-action/60" : "text-ink-100 ring-glass-line"
|
||||
}`}
|
||||
>
|
||||
{initials}
|
||||
<span className="absolute -right-1 -bottom-1 grid size-6 place-items-center rounded-full bg-space-950 font-display text-xs text-ink-50 ring-2 ring-space-600">
|
||||
<span className="absolute -right-1 -bottom-1 grid size-5 place-items-center rounded-full bg-space-900 text-xs font-semibold text-ink-50 ring-1 ring-glass-line">
|
||||
{pilot.level}
|
||||
</span>
|
||||
</span>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-baseline justify-between gap-2">
|
||||
<span className="truncate font-display text-lg font-bold">
|
||||
<span className="truncate text-base font-semibold">
|
||||
{pilot.name}
|
||||
{pilot.isAvatar && <span className="ml-1 text-xs text-amber-300">(you)</span>}
|
||||
{pilot.isAvatar && (
|
||||
<span className="ml-1 text-xs font-normal text-ink-300">(you)</span>
|
||||
)}
|
||||
</span>
|
||||
<span className="font-display text-xs whitespace-nowrap text-ink-300">
|
||||
Level {pilot.level}
|
||||
</span>
|
||||
</div>
|
||||
{pilot.isVeteran && (
|
||||
<p aria-label={`${pilot.name} bio`} className="mt-0.5 text-xs text-amber-200/90 italic">
|
||||
<p aria-label={`${pilot.name} bio`} className="mt-0.5 text-xs text-ink-300 italic">
|
||||
{content.veteranPilot.bio}
|
||||
</p>
|
||||
)}
|
||||
|
|
@ -145,9 +144,9 @@ function PilotCard({ pilot }: { pilot: Pilot }) {
|
|||
? `${progress.xp} / ${progress.needed} XP to next level`
|
||||
: `${pilot.xp} XP · max level`}
|
||||
</div>
|
||||
<div aria-hidden="true" className="mt-1 h-1 overflow-hidden rounded-full bg-space-950/80">
|
||||
<div aria-hidden="true" className="mt-1 h-1 overflow-hidden rounded-full bg-black/25">
|
||||
<div
|
||||
className="h-full rounded-full bg-linear-to-r from-violet-500 to-fuchsia-300"
|
||||
className="h-full rounded-full bg-ink-300"
|
||||
style={{ width: `${progress ? (progress.xp / progress.needed) * 100 : 100}%` }}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -170,28 +169,28 @@ function PilotCard({ pilot }: { pilot: Pilot }) {
|
|||
: "No ship"}
|
||||
</div>
|
||||
|
||||
<div className="mt-3 rounded-lg bg-space-950/50 p-2.5 ring-1 ring-space-700">
|
||||
<div className="mt-3">
|
||||
<div
|
||||
className={`font-display text-xs ${pilot.skillPointsAvailable > 0 ? "text-amber-300" : "text-ink-400"}`}
|
||||
className={`text-xs font-semibold ${pilot.skillPointsAvailable > 0 ? "text-action" : "text-ink-300"}`}
|
||||
>
|
||||
Skill points:{" "}
|
||||
<span aria-label={`${pilot.name} skill points`}>{pilot.skillPointsAvailable}</span>
|
||||
</div>
|
||||
<ul className="mt-2 grid grid-cols-2 gap-x-3 gap-y-2">
|
||||
<ul className="mt-1 divide-y divide-glass-line">
|
||||
{content.skills.map((skill) => {
|
||||
const level = pilot.skills[skill.id] ?? 0;
|
||||
const canRaise = pilot.skillPointsAvailable > 0 && level < skill.maxLevel;
|
||||
return (
|
||||
<li key={skill.id} className="flex items-center justify-between gap-1">
|
||||
<li key={skill.id} className="flex min-h-11 items-center justify-between gap-2 py-1">
|
||||
<span className="min-w-0">
|
||||
<span className="block truncate text-xs text-ink-200">
|
||||
<span className="block text-[13px] text-ink-100">
|
||||
{skill.name} {level}/{skill.maxLevel}
|
||||
</span>
|
||||
<span aria-hidden="true" className="mt-0.5 flex gap-0.5">
|
||||
{Array.from({ length: skill.maxLevel }, (_, i) => (
|
||||
<span
|
||||
key={i}
|
||||
className={`h-1.5 w-3 rounded-sm ${i < level ? "bg-cyan-300 shadow-[0_0_4px] shadow-cyan-300/70" : "bg-space-700"}`}
|
||||
className={`h-1 w-3 rounded-full ${i < level ? "bg-ink-200" : "bg-white/14"}`}
|
||||
/>
|
||||
))}
|
||||
</span>
|
||||
|
|
@ -202,7 +201,7 @@ function PilotCard({ pilot }: { pilot: Pilot }) {
|
|||
aria-label={`Raise ${skill.name} for ${pilot.name}`}
|
||||
disabled={spend.isPending}
|
||||
onClick={() => spend.mutate({ pilotId: pilot.id, skillId: skill.id })}
|
||||
className="grid size-8 shrink-0 animate-glow place-items-center rounded-lg border border-amber-400/70 bg-amber-500/15 text-amber-200 transition active:scale-95"
|
||||
className="grid size-11 shrink-0 place-items-center rounded-xl border border-action/60 text-action transition hover:bg-action/10 active:scale-95"
|
||||
>
|
||||
<Icon name="plus" className="size-4" />
|
||||
</button>
|
||||
|
|
@ -244,7 +243,7 @@ function Duty({ pilot, canRest }: { pilot: Pilot; canRest: boolean }) {
|
|||
if (pilot.restingInShopUntil !== null) {
|
||||
return (
|
||||
<div
|
||||
className="mt-3 flex items-center gap-2 rounded-lg border border-sky-400/30 bg-sky-950/40 px-3 py-2 text-sky-200"
|
||||
className="mt-3 flex items-center gap-2 rounded-lg border border-sky-400/30 bg-sky-950/40 px-3 py-2 text-ink-100"
|
||||
aria-label={`${pilot.name} rest`}
|
||||
>
|
||||
<Icon name="rest" className="size-4 shrink-0" />
|
||||
|
|
@ -255,7 +254,7 @@ function Duty({ pilot, canRest }: { pilot: Pilot; canRest: boolean }) {
|
|||
if (pilot.resting && pilot.restEndsAt !== null) {
|
||||
return (
|
||||
<div
|
||||
className="mt-3 flex items-center gap-2 rounded-lg border border-sky-400/30 bg-sky-950/40 px-3 py-2 text-sky-200"
|
||||
className="mt-3 flex items-center gap-2 rounded-lg border border-sky-400/30 bg-sky-950/40 px-3 py-2 text-ink-100"
|
||||
aria-label={`${pilot.name} rest`}
|
||||
>
|
||||
<Icon name="rest" className="size-4 shrink-0" />
|
||||
|
|
@ -278,7 +277,7 @@ function Duty({ pilot, canRest }: { pilot: Pilot; canRest: boolean }) {
|
|||
label={`${pilot.name} duty`}
|
||||
value={used}
|
||||
max={pilot.dutyLimit}
|
||||
tone={used / pilot.dutyLimit >= 0.85 ? "rose" : "amber"}
|
||||
tone={used / pilot.dutyLimit >= 0.85 ? "rose" : "default"}
|
||||
size="sm"
|
||||
className="mt-1"
|
||||
/>
|
||||
|
|
@ -345,7 +344,7 @@ function AssignShip({ pilot }: { pilot: Pilot }) {
|
|||
aria-label={`Assign ${pilot.name} to`}
|
||||
value={shipId}
|
||||
onChange={(e) => setShipId(e.target.value)}
|
||||
className="min-w-0 flex-1 min-h-11 rounded-lg border border-space-600 bg-space-950/70 px-3 font-display text-sm focus:border-amber-400 focus:outline-none"
|
||||
className="min-w-0 flex-1 min-h-11 rounded-lg border border-glass-line bg-black/25 px-3 font-display text-sm focus:border-action focus:outline-none"
|
||||
>
|
||||
<option value="">Choose a ship…</option>
|
||||
{ships.map((s) => (
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@ async function shareFile(file: File): Promise<void> {
|
|||
}
|
||||
|
||||
const codeBox =
|
||||
"mt-1 h-28 w-full resize-none rounded-lg border border-space-600 bg-space-950/70 px-3 py-2 font-mono text-xs break-all text-ink-100 focus:border-amber-400 focus:outline-none";
|
||||
"mt-1 h-28 w-full resize-none rounded-lg border border-glass-line bg-black/25 px-3 py-2 font-mono text-xs break-all text-ink-100 focus:border-action focus:outline-none";
|
||||
|
||||
/** "Export save" (decision 0.5.92): the whole company as one code, to copy or share as a file. */
|
||||
export function ExportSave() {
|
||||
|
|
@ -137,7 +137,7 @@ export function ImportSave({ hasCompany }: { hasCompany: boolean }) {
|
|||
{hasCompany && (
|
||||
<p
|
||||
role="note"
|
||||
className="mt-3 rounded-lg bg-amber-950/50 p-2 text-sm text-amber-200 ring-1 ring-amber-400/40"
|
||||
className="mt-3 rounded-lg bg-amber-950/50 p-2 text-sm text-ink-100 ring-1 ring-amber-400/40"
|
||||
>
|
||||
This replaces the company on this device. Export it first if you want to keep it.
|
||||
</p>
|
||||
|
|
@ -187,7 +187,7 @@ export function ImportSave({ hasCompany }: { hasCompany: boolean }) {
|
|||
</p>
|
||||
)}
|
||||
{confirming && (
|
||||
<p className="text-sm text-amber-200">
|
||||
<p className="text-sm text-ink-100">
|
||||
{hasCompany ? "Replace your company with this save?" : "Open this company?"}
|
||||
</p>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -161,11 +161,11 @@ function Choice<T extends string>(props: {
|
|||
return (
|
||||
<fieldset className="flex flex-wrap items-center justify-between gap-2 text-sm">
|
||||
<legend className="float-left text-ink-400">{props.label}</legend>
|
||||
<div className="flex overflow-hidden rounded-lg border border-space-600">
|
||||
<div className="flex overflow-hidden rounded-lg border border-glass-line">
|
||||
{props.options.map(([value, text]) => (
|
||||
<label
|
||||
key={value}
|
||||
className="cursor-pointer px-3 py-1.5 has-checked:bg-amber-500/20 has-checked:text-amber-100 has-focus-visible:ring-2"
|
||||
className="cursor-pointer px-3 py-1.5 has-checked:bg-white/14 has-checked:text-ink-50 has-focus-visible:ring-2"
|
||||
>
|
||||
<input
|
||||
type="radio"
|
||||
|
|
|
|||
|
|
@ -83,7 +83,7 @@ function HullRepair({ shipId, atHq }: { shipId: string; atHq: boolean }) {
|
|||
return (
|
||||
<section
|
||||
aria-label="Hull"
|
||||
className="mt-4 rounded-xl border border-space-600 bg-space-900/60 p-3 text-sm"
|
||||
className="mt-4 rounded-xl border border-glass-line bg-white/5 p-3 text-sm"
|
||||
>
|
||||
<div className="flex items-baseline justify-between gap-2">
|
||||
<span className="font-display font-bold">Hull</span>
|
||||
|
|
@ -171,7 +171,7 @@ export function ShipUpgrades({ shipId, onClose }: { shipId: string; onClose: ()
|
|||
<li
|
||||
key={upgrade.id}
|
||||
aria-label={upgrade.name}
|
||||
className="rounded-xl border border-space-600 bg-space-900/60 p-3 text-sm"
|
||||
className="rounded-xl border border-glass-line bg-white/5 p-3 text-sm"
|
||||
>
|
||||
<div className="flex items-baseline justify-between gap-2">
|
||||
<span className="font-display font-bold">{upgrade.name}</span>
|
||||
|
|
@ -203,7 +203,7 @@ export function ShipUpgrades({ shipId, onClose }: { shipId: string; onClose: ()
|
|||
<div
|
||||
role="group"
|
||||
aria-label={`Start ${upgrade.name} ${check.level}?`}
|
||||
className="mt-2 rounded-lg bg-space-950/70 p-2"
|
||||
className="mt-2 rounded-lg bg-black/25 p-2"
|
||||
>
|
||||
<p className="text-ink-200">
|
||||
{upgrade.name} {check.level} costs {formatCredits(check.cost)}. {ship.name}{" "}
|
||||
|
|
|
|||
|
|
@ -42,7 +42,7 @@ export function StartScreen() {
|
|||
<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">
|
||||
DEADHEAD
|
||||
</h1>
|
||||
<p className="mt-2 font-display text-sm text-cyan-300/80">Space Freight Tycoon</p>
|
||||
<p className="mt-2 font-display text-sm text-ink-300">Space Freight Tycoon</p>
|
||||
</div>
|
||||
<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
|
||||
|
|
|
|||
|
|
@ -49,7 +49,7 @@ export function TutorialBoard() {
|
|||
<Panel aria-label="Intro" tone="amber" className="flex items-center gap-3 text-sm">
|
||||
<Art sprite={loaner.sprite} className="size-16 shrink-0 -rotate-12" />
|
||||
<div>
|
||||
<p className="font-display text-base font-bold text-amber-200">
|
||||
<p className="font-display text-base font-bold text-ink-100">
|
||||
{content.tutorialCompanyName} needs a pilot.
|
||||
</p>
|
||||
<p className="text-ink-200">Their ship, your hands. Get the crates to the Moon.</p>
|
||||
|
|
@ -68,8 +68,8 @@ export function TutorialBoard() {
|
|||
Company runs in the loaner {loaner.name}. You keep a share of each payout.
|
||||
</p>
|
||||
{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-cyan-100">
|
||||
<Icon name="send" className="size-4 shrink-0 text-cyan-300" />
|
||||
<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">
|
||||
<Icon name="send" className="size-4 shrink-0 text-ink-300" />
|
||||
Your run is in flight. Wait for it to land.
|
||||
</p>
|
||||
)}
|
||||
|
|
@ -139,7 +139,7 @@ export function TutorialBoard() {
|
|||
destinationId={contract.destinationId}
|
||||
size="sm"
|
||||
/>
|
||||
<div className="mt-auto flex items-end justify-between gap-2 border-t border-space-700/70 pt-2">
|
||||
<div className="mt-auto flex items-end justify-between gap-2 border-t border-glass-line pt-2">
|
||||
<dl className="grid flex-1 grid-cols-3 gap-2 text-sm">
|
||||
<div>
|
||||
<dt className="font-display text-xs text-ink-400">Pay</dt>
|
||||
|
|
|
|||
|
|
@ -27,6 +27,7 @@ export function Sheet({
|
|||
subtitle,
|
||||
tone = "default",
|
||||
layer = "z-20",
|
||||
side = false,
|
||||
children,
|
||||
}: {
|
||||
titleId: string;
|
||||
|
|
@ -36,17 +37,19 @@ export function Sheet({
|
|||
tone?: Tone;
|
||||
/** Stacking layer, e.g. `z-30` for a sheet over other sheets. */
|
||||
layer?: string;
|
||||
/** From `xl`, opens beside the wide panel and leaves it usable (decision 0.5.169). */
|
||||
side?: boolean;
|
||||
children: ReactNode;
|
||||
}) {
|
||||
return createPortal(
|
||||
<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`}
|
||||
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)]" : ""}`}
|
||||
>
|
||||
<div
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby={titleId}
|
||||
className={`relative max-h-full w-full max-w-md animate-sheet overflow-y-auto rounded-t-3xl border bg-space-850 p-5 pb-[max(1.25rem,env(safe-area-inset-bottom))] shadow-panel sm:rounded-3xl ${BORDER_TONES[tone] ?? "border-glass-line"}`}
|
||||
className={`relative max-h-full w-full max-w-md animate-sheet ${side ? "xl:pointer-events-auto xl:max-w-[26rem]" : ""} overflow-y-auto rounded-t-3xl border bg-space-850 p-5 pb-[max(1.25rem,env(safe-area-inset-bottom))] shadow-panel sm:rounded-3xl ${BORDER_TONES[tone] ?? "border-glass-line"}`}
|
||||
>
|
||||
<div
|
||||
aria-hidden="true"
|
||||
|
|
|
|||
Loading…
Reference in New Issue