Step 29.7: Dispatch, Fleet and Crew

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
fireshaper 2026-11-04 18:33:47 -05:00
parent d279ac4ec0
commit 4a33aed970
31 changed files with 156 additions and 153 deletions

View File

@ -291,7 +291,7 @@ export function AwaySummary() {
<ul aria-label="What happened" className="space-y-1.5"> <ul aria-label="What happened" className="space-y-1.5">
{summary.direct.map((entry) => ( {summary.direct.map((entry) => (
<li key={entry.id} className="flex items-start gap-2 text-ink-100"> <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)} {directText(entry as LogEntry, state)}
</li> </li>
))} ))}
@ -302,7 +302,7 @@ export function AwaySummary() {
{highlights.length > 0 && ( {highlights.length > 0 && (
<ul aria-label="Highlights" className="space-y-1.5"> <ul aria-label="Highlights" className="space-y-1.5">
{highlights.map((h, i) => ( {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 <Icon
name={h.kind === "problem" ? "alert" : "star"} name={h.kind === "problem" ? "alert" : "star"}
className="mt-0.5 size-4 shrink-0 text-amber-300" 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"], [words.totals.deliveries, String(deliveries), "text-ink-50"],
]; ];
if (discoveries > 0) 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) if (loanRepaid > 0)
figures.push([words.totals.loanRepaid, formatCredits(loanRepaid), "text-ink-50"]); figures.push([words.totals.loanRepaid, formatCredits(loanRepaid), "text-ink-50"]);
return ( return (
@ -380,7 +380,7 @@ function PilotLines({ summary }: { summary: Summary }) {
})} })}
</span> </span>
{line.leveledUp && ( {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} {words.leveledUp}
</span> </span>
)} )}

View File

@ -56,7 +56,7 @@ export function DealModal() {
of every job. of every job.
</p> </p>
</div> </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> <dt className="font-display text-xs text-ink-400">You get</dt>
<dd aria-label="Ships and bay"> <dd aria-label="Ships and bay">
A {courier.name} for you, a {clipper.name} for {veteran.name}, and a bay at {bay.name} 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> <dt className="font-display text-xs text-ink-400">Loan</dt>
<dd <dd
aria-label="Loan amount" 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)} {formatCredits(dealLoanAmount)}
</dd> </dd>

View File

@ -47,7 +47,7 @@ export function SleepBanner() {
const { sleepsAt, afterHours } = state.fleetSleep; const { sleepsAt, afterHours } = state.fleetSleep;
return ( return (
<Panel tone="amber" className="text-sm" aria-label="Fleet asleep"> <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" /> <Icon name="rest" className="mt-0.5 size-4 shrink-0" />
Your fleet went quiet on {formatSleepTime(sleepsAt)} after {sleepLabel(afterHours)} without Your fleet went quiet on {formatSleepTime(sleepsAt)} after {sleepLabel(afterHours)} without
orders. orders.
@ -63,7 +63,7 @@ export function FleetAsleep() {
if (!fleetQuiet(state)) return null; if (!fleetQuiet(state)) return null;
return ( return (
<Panel tone="amber" className="text-sm" aria-label="Fleet asleep"> <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" /> <Icon name="rest" className="size-4 shrink-0" />
Fleet asleep — waiting for your orders Fleet asleep — waiting for your orders
</p> </p>

View File

@ -54,7 +54,7 @@ export function FuelGauge({ ship }: { ship: Ship }) {
label={`${ship.name} fuel`} label={`${ship.name} fuel`}
value={ship.fuelFu} value={ship.fuelFu}
max={capacity} max={capacity}
tone={percent < 25 ? "rose" : "amber"} tone={percent < 25 ? "rose" : "default"}
size="sm" size="sm"
/> />
<div className="mt-1 flex justify-between font-display text-xs text-ink-400"> <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)" placeholder="Amount (fu)"
value={amount} value={amount}
onChange={(e) => setAmount(e.target.value)} 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> </label>
<Button <Button
@ -202,11 +202,11 @@ export function ShipFuel({ shipId, onClose }: { shipId: string; onClose: () => v
<fieldset className="mt-4 text-sm"> <fieldset className="mt-4 text-sm">
<legend className="text-ink-400">Refuel policy</legend> <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) => ( {(["manual", "auto"] as const).map((value) => (
<label <label
key={value} 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 <input
type="radio" type="radio"

View File

@ -10,7 +10,7 @@ import { goodName } from "./Goods";
import { serviceStatus } from "./Service"; import { serviceStatus } from "./Service";
const inputClass = 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). */ /** The one-time Dispatch Office on the Bridge (decision 0.5.123). */
export function DispatchOfficePanel() { export function DispatchOfficePanel() {
@ -22,7 +22,7 @@ export function DispatchOfficePanel() {
if (state.dispatchOffice.owned) { if (state.dispatchOffice.owned) {
return ( return (
<Panel tone="cyan" className="text-sm" aria-label="Dispatch Office"> <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" /> <Icon name="send" className="size-4" />
Dispatch Office Dispatch Office
</p> </p>
@ -100,14 +100,14 @@ export function PilotIndependence({ pilot }: { pilot: Pilot }) {
const status = independentStatus(state, pilot); const status = independentStatus(state, pilot);
return ( 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> <fieldset>
<legend className="font-display text-xs text-ink-400">Flies</legend> <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) => ( {(["manual", "independent"] as const).map((mode) => (
<label <label
key={mode} 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 <input
type="radio" type="radio"
@ -129,7 +129,7 @@ export function PilotIndependence({ pilot }: { pilot: Pilot }) {
)} )}
</fieldset> </fieldset>
{status && ( {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} {status}
</p> </p>
)} )}
@ -155,7 +155,7 @@ function OrdersEditor({ pilot }: { pilot: Pilot }) {
return ( return (
<details className="mt-2"> <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 <form
aria-label={`Orders for ${pilot.name}`} aria-label={`Orders for ${pilot.name}`}
className="mt-2 space-y-3 text-sm" className="mt-2 space-y-3 text-sm"

View File

@ -100,14 +100,12 @@ function EntryLine({ entry, state }: { entry: LogEntry; state: PlayerState }) {
{locationName(from)} → {locationName(to)} {locationName(from)} → {locationName(to)}
</span> </span>
); );
const fuel = (cost: number) => ( const fuel = (cost: number) => <span className="text-ink-100">fuel {formatCredits(cost)}</span>;
<span className="text-amber-200/90">fuel {formatCredits(cost)}</span>
);
switch (entry.kind) { switch (entry.kind) {
case "flight": case "flight":
return entry.deadhead ? ( return entry.deadhead ? (
<> <>
<Icon name="send" className="size-4 text-cyan-300" /> <Icon name="send" className="size-4 text-ink-300" />
<span>Deadhead</span> <span>Deadhead</span>
{route(entry.originId, entry.destinationId)} {route(entry.originId, entry.destinationId)}
{fuel(entry.fuelCost)} {fuel(entry.fuelCost)}
@ -131,56 +129,56 @@ function EntryLine({ entry, state }: { entry: LogEntry; state: PlayerState }) {
); );
case "refuel": case "refuel":
return ( return (
<span className="text-amber-200/90"> <span className="text-ink-100">
{Math.round(entry.boughtFu)} fu at {locationName(entry.locationId)},{" "} {Math.round(entry.boughtFu)} fu at {locationName(entry.locationId)},{" "}
{formatCredits(entry.cost)} ({REFUEL_SOURCE[entry.source]}) {formatCredits(entry.cost)} ({REFUEL_SOURCE[entry.source]})
</span> </span>
); );
case "rest": case "rest":
return ( 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" /> <Icon name="rest" className="size-4" />
Rested until {formatLocalTime(entry.until)} Rested until {formatLocalTime(entry.until)}
</span> </span>
); );
case "upgrade": case "upgrade":
return ( return (
<span className="text-cyan-100"> <span className="text-ink-100">
{shipName(state, entry.shipId)}:{" "} {shipName(state, entry.shipId)}:{" "}
{content.upgrade(entry.upgradeId)?.name ?? entry.upgradeId} level {entry.level} finished {content.upgrade(entry.upgradeId)?.name ?? entry.upgradeId} level {entry.level} finished
</span> </span>
); );
case "discovery": 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": case "level-up":
return ( return (
<span className="text-amber-200"> <span className="text-ink-100">
Reached level {entry.level} · {entry.skillPoints}{" "} Reached level {entry.level} · {entry.skillPoints}{" "}
{entry.skillPoints === 1 ? "skill point" : "skill points"} {entry.skillPoints === 1 ? "skill point" : "skill points"}
</span> </span>
); );
case "event": case "event":
return ( return (
<span className={entry.good ? "text-emerald-200" : "text-amber-200"}> <span className={entry.good ? "text-emerald-200" : "text-ink-100"}>
{eventResultText(entry, state)} {eventResultText(entry, state)}
</span> </span>
); );
case "repair": case "repair":
return ( return (
<span className="text-cyan-100"> <span className="text-ink-100">
{shipName(state, entry.shipId)}: hull repaired from {entry.fromPct}% to 100%,{" "} {shipName(state, entry.shipId)}: hull repaired from {entry.fromPct}% to 100%,{" "}
{formatCredits(entry.cost)} {formatCredits(entry.cost)}
</span> </span>
); );
case "service": case "service":
return ( 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))} {serviceEntryText(entry, shipName(state, entry.shipId))}
</span> </span>
); );
case "goal-ready": case "goal-ready":
return ( return (
<span className="text-amber-200"> <span className="text-ink-100">
Goal ready: {content.onboardingGoal(entry.goalId)?.title ?? entry.goalId} Goal ready: {content.onboardingGoal(entry.goalId)?.title ?? entry.goalId}
</span> </span>
); );
@ -201,13 +199,13 @@ function PilotSection({ log, state, open }: { log: PilotLog; state: PlayerState;
const [shown, setShown] = useState(PAGE); const [shown, setShown] = useState(PAGE);
const now = useClock((c) => c.now); const now = useClock((c) => c.now);
return ( 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"> <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-violet-300">+{log.xp} XP</span>}
</summary> </summary>
{log.now && ( {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} {log.now}
</p> </p>
)} )}
@ -246,7 +244,7 @@ export function PilotLogbook({ logbook, state }: { logbook: Logbook; state: Play
<span>{totals.deadheads} deadheads</span> <span>{totals.deadheads} deadheads</span>
<span className="text-emerald-300">{formatCredits(totals.payout)} paid</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-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="text-ink-300">{formatCredits(totals.loanRepaid)} to the lender</span>
<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
@ -287,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-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… Catching up on your fleet…
</p> </p>

View File

@ -70,7 +70,7 @@ function CallBody({ call, state }: { call: RadioCall; state: PlayerState }) {
<div className="space-y-3 text-sm"> <div className="space-y-3 text-sm">
<p className="text-ink-100">{fill(def.text, { pilot })}</p> <p className="text-ink-100">{fill(def.text, { pilot })}</p>
{ship && <p className="text-ink-400">{ship}</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]" /> <Icon name="clock" className="mr-1.5 inline size-4 align-[-3px]" />
Answer within <Countdown to={call.windowEndsAt} format={formatClock} done="now" /> — then{" "} Answer within <Countdown to={call.windowEndsAt} format={formatClock} done="now" /> — then{" "}
{pilot} will {fallback?.label.toLowerCase()}. {pilot} will {fallback?.label.toLowerCase()}.
@ -151,7 +151,7 @@ export function RadioCallsPanel() {
if (state.radioCalls.length === 0) return null; if (state.radioCalls.length === 0) return null;
return ( return (
<Panel tone="amber" className="text-sm" aria-label="Radio calls"> <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" /> <Icon name="alert" className="size-4" />
Radio calls ({state.radioCalls.length}) Radio calls ({state.radioCalls.length})
</p> </p>

View File

@ -29,7 +29,7 @@ export function RouteStrip({
{percent === null ? ( {percent === null ? (
<Icon <Icon
name="arrow" 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"
/> />
) : ( ) : (
<> <>

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-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} {tag}
</p> </p>
@ -149,7 +149,7 @@ function BookingForm({ ship, onDone }: { ship: Ship; onDone: () => void }) {
checked={repair && canRepair} checked={repair && canRepair}
disabled={!canRepair} disabled={!canRepair}
onChange={(e) => setRepair(e.target.checked)} onChange={(e) => setRepair(e.target.checked)}
className="size-4 accent-amber-400" className="size-4 accent-action"
/> />
Repair to 100% Repair to 100%
{!canRepair && <span className="text-ink-400">(hull is whole)</span>} {!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" aria-label="Upgrade"
value={upgradeId ?? ""} value={upgradeId ?? ""}
onChange={(e) => setUpgradeId(e.target.value || null)} 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> <option value="">None</option>
{upgrades.map((u) => ( {upgrades.map((u) => (
@ -176,7 +176,7 @@ function BookingForm({ ship, onDone }: { ship: Ship; onDone: () => void }) {
aria-label="HQ" aria-label="HQ"
value={hqId} value={hqId}
onChange={(e) => setHqId(e.target.value)} 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) => ( {hqs.map((id) => (
<option key={id} value={id}> <option key={id} value={id}>
@ -221,14 +221,14 @@ function AutoRepair({ ship }: { ship: Ship }) {
const { minPct, maxPct, stepPct } = content.config.autoRepair; const { minPct, maxPct, stepPct } = content.config.autoRepair;
const { on, thresholdPct } = ship.autoRepair; const { on, thresholdPct } = ship.autoRepair;
return ( 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"> <label className="flex items-center gap-2">
<input <input
type="checkbox" type="checkbox"
checked={on} checked={on}
disabled={save.isPending} disabled={save.isPending}
onChange={(e) => save.mutate({ shipId: ship.id, on: e.target.checked, thresholdPct })} 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}% Auto-repair below {thresholdPct}%
</label> </label>
@ -241,7 +241,7 @@ function AutoRepair({ ship }: { ship: Ship }) {
value={thresholdPct} value={thresholdPct}
disabled={!on} disabled={!on}
onChange={(e) => save.mutate({ shipId: ship.id, on, thresholdPct: Number(e.target.value) })} 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"> <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 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 ( return (
<section <section
aria-label="Service" 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"> <div className="flex items-baseline justify-between gap-2">
<span className="font-display font-bold">Service</span> <span className="font-display font-bold">Service</span>

View File

@ -74,7 +74,7 @@ export function SkillPointPrompt() {
{ready.map((pilot, i) => ( {ready.map((pilot, i) => (
<span key={pilot.id}> <span key={pilot.id}>
{i > 0 && " · "} {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"} {pilot.skillPointsAvailable} {pilot.skillPointsAvailable === 1 ? "point" : "points"}
</span> </span>
))} ))}

View File

@ -80,7 +80,7 @@ function ToastItem({
onTouchEnd={() => setPaused(false)} onTouchEnd={() => setPaused(false)}
onPointerDown={onPointerDown} onPointerDown={onPointerDown}
onPointerUp={onPointerUp} 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"> <button type="button" onClick={open} className="min-w-0 flex-1 py-0.5 text-left">
{toast.text} {toast.text}
@ -136,7 +136,7 @@ export function Toasts() {
dropOverflow(); dropOverflow();
void navigate("/logbook"); 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 +{more} more
</button> </button>

View File

@ -66,7 +66,7 @@ export function WelcomeCards({
}; };
return createPortal( 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 <div
ref={dialog} ref={dialog}
role="dialog" role="dialog"
@ -85,9 +85,9 @@ 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-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} {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"}>

View File

@ -6,7 +6,7 @@ import { usePreferences } from "../state/preferences";
import { Button, Sheet } from "../ui"; import { Button, Sheet } from "../ui";
const inputClass = 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). */ /** The company name, editable in place with the server's own rules (decision 0.5.75). */
export function CompanyName() { export function CompanyName() {

View File

@ -19,7 +19,9 @@ export function Contracts() {
icon="jobs" icon="jobs"
/> />
{state.tutorial ? <TutorialBoard /> : <LocationBoards onSelect={setSelected} />} {state.tutorial ? <TutorialBoard /> : <LocationBoards onSelect={setSelected} />}
{selected && <DispatchSheet contract={selected} onClose={() => setSelected(null)} />} {selected && (
<DispatchSheet key={selected.id} contract={selected} onClose={() => setSelected(null)} />
)}
</Screen> </Screen>
); );
} }

View File

@ -299,7 +299,7 @@ function YourShip({ className = "" }: { className?: string }) {
)} )}
{inShop && ( {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" /> In the shop — ready in <Countdown to={ship!.inShopUntil!} done="now" />
</p> </p>
)} )}
@ -355,12 +355,12 @@ function PilotDuty({ pilot }: { pilot: Pilot }) {
{pilot.name} · Level {pilot.level} {pilot.name} · Level {pilot.level}
</Eyebrow> </Eyebrow>
{pilot.restingInShopUntil !== null ? ( {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" /> <Icon name="rest" className="size-4 shrink-0" />
<ShopRest until={pilot.restingInShopUntil} /> <ShopRest until={pilot.restingInShopUntil} />
</p> </p>
) : pilot.resting && pilot.restEndsAt !== null ? ( ) : 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" /> <Icon name="rest" className="size-4 shrink-0" />
Resting · wakes in <Countdown to={pilot.restEndsAt} done="any moment" /> Resting · wakes in <Countdown to={pilot.restEndsAt} done="any moment" />
</p> </p>
@ -370,7 +370,7 @@ function PilotDuty({ pilot }: { pilot: Pilot }) {
label="Your duty" label="Your duty"
value={Math.min(pilot.dutyHours, pilot.dutyLimit)} value={Math.min(pilot.dutyHours, pilot.dutyLimit)}
max={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" size="sm"
className="mt-2" className="mt-2"
/> />

View File

@ -10,7 +10,7 @@ import { Button } from "../ui";
const MINUTE = 60_000; const MINUTE = 60_000;
const selectClass = 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 * Settings → Developer (non-production builds only, CR-011): fast-forward the

View File

@ -31,7 +31,6 @@ import {
import { requirementsText } from "./LocationBoards"; import { requirementsText } from "./LocationBoards";
import { jobMoney, location, tripRealMs, shipStatsOf, simShip } from "../game"; import { jobMoney, location, tripRealMs, shipStatsOf, simShip } from "../game";
import { errorMessage, useGameAction, useLoadedPlayer } from "../state/actions"; import { errorMessage, useGameAction, useLoadedPlayer } from "../state/actions";
import { RouteStrip } from "../components/RouteStrip";
import { Art, Button, Sheet } from "../ui"; import { Art, Button, Sheet } from "../ui";
import { goodName, PriorityChip } from "../components/Goods"; import { goodName, PriorityChip } from "../components/Goods";
import { import {
@ -364,6 +363,7 @@ export function DispatchSheet({ contract, onClose }: { contract: Contract; onClo
<Sheet <Sheet
titleId="dispatch-title" titleId="dispatch-title"
eyebrow="Dispatch" eyebrow="Dispatch"
side
title={`${locationName(contract.originId)} → ${locationName(contract.destinationId)}`} title={`${locationName(contract.originId)} → ${locationName(contract.destinationId)}`}
> >
<p aria-label="Job details" className="mt-1 text-sm text-ink-300"> <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} /> <PriorityChip windowHours={contract.deliveryWindowGameHours} />
</div> </div>
)} )}
<div className="mt-3">
<RouteStrip originId={contract.originId} destinationId={contract.destinationId} />
</div>
{options.length === 0 ? ( {options.length === 0 ? (
<p className="mt-3 text-sm text-ink-400"> <p className="mt-3 text-sm text-ink-400">
@ -397,10 +394,10 @@ export function DispatchSheet({ contract, onClose }: { contract: Contract; onClo
aria-disabled={blocked} aria-disabled={blocked}
className={`block rounded-xl border p-3 text-sm transition ${ className={`block rounded-xl border p-3 text-sm transition ${
blocked blocked
? "border-space-700 bg-space-950/40 opacity-70" ? "border-glass-line bg-black/25 opacity-70"
: picked : picked
? "cursor-pointer border-amber-400 bg-amber-500/10 shadow-glow-amber" ? "cursor-pointer border-action bg-action/8"
: "cursor-pointer border-space-600 bg-space-900/60 hover:border-space-500" : "cursor-pointer border-glass-line bg-white/5 hover:border-glass-line"
}`} }`}
> >
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
@ -411,16 +408,16 @@ export function DispatchSheet({ contract, onClose }: { contract: Contract; onClo
disabled={blocked} disabled={blocked}
checked={picked} checked={picked}
onChange={() => setChosen(option.shipId)} 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" /> <Art sprite={option.modelSprite} className="size-10 shrink-0" />
<span className="min-w-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"> <span className="block text-xs text-ink-400">
{option.modelName} · {option.pilotName ?? "no pilot"} {option.modelName} · {option.pilotName ?? "no pilot"}
</span> </span>
{option.service && ( {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} {option.service}
</span> </span>
)} )}
@ -438,8 +435,13 @@ export function DispatchSheet({ contract, onClose }: { contract: Contract; onClo
))} ))}
</ul> </ul>
) : ( ) : (
<dl className="mt-3 grid grid-cols-2 gap-2 sm:grid-cols-3"> <dl className="mt-3 grid grid-cols-2 gap-x-3 gap-y-2">
<Cell label="Trip" value={formatDuration(option.tripMs)} /> <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 <Cell
label="Fuel used (at your average price)" label="Fuel used (at your average price)"
value={formatCredits(option.fuelCost)} 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 && ( !state.pilots.find((p) => p.id === option.pilotId)?.isAvatar && (
<Cell label="Pilot share" value={formatCredits(option.pilotPay)} /> <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 <Cell
label="ETA (game)" label="ETA (game)"
value={formatGameTime( value={formatGameTime(
@ -481,7 +483,7 @@ export function DispatchSheet({ contract, onClose }: { contract: Contract; onClo
/> />
)} )}
{!blocked && option.priority && ( {!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)}{" "} Priority: {formatGameHoursSpan(option.priority.tripHours, prefsCountdown)}{" "}
trip,{" "} trip,{" "}
{formatGameHoursSpan( {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"). */ /** 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 ( return (
<div className="rounded-lg bg-space-950/60 px-2 py-1.5 ring-1 ring-space-700"> <div>
<dt className="font-display text-xs text-ink-400">{label}</dt> <dt className="text-xs text-ink-300">{label}</dt>
<dd <dd className="text-sm font-semibold tabular-nums">{value}</dd>
className={`font-display text-sm font-semibold tabular-nums ${good ? "text-emerald-300" : "text-ink-50"}`}
>
{value}
</dd>
</div> </div>
); );
} }

View File

@ -59,7 +59,7 @@ export function Fleet() {
<Art sprite="ship-courier" className="size-20 opacity-60" /> <Art sprite="ship-courier" className="size-20 opacity-60" />
<p className="text-sm text-ink-300"> <p className="text-sm text-ink-300">
No ships yet —{" "} No ships yet —{" "}
<Link to="/market" className="font-semibold text-amber-300 underline"> <Link to="/market" className="font-semibold underline">
visit the Shipyard visit the Shipyard
</Link> </Link>
. .
@ -110,17 +110,17 @@ function ShipRow(props: {
const blocked = findWorkBlock(ship, props.pilot, now); const blocked = findWorkBlock(ship, props.pilot, now);
return ( 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="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 <Art
sprite={model.sprite} 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>
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
<div className="flex items-start justify-between gap-2"> <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 ? ( {dispatch ? (
<Pill tone="cyan" icon="send"> <Pill tone="cyan" icon="send">
In flight In flight
@ -131,7 +131,7 @@ function ShipRow(props: {
<Pill tone="emerald">Idle</Pill> <Pill tone="emerald">Idle</Pill>
)} )}
</div> </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"> <div className="mt-1 flex flex-wrap items-center gap-x-1 gap-y-1 text-sm text-ink-200">
<span> <span>
{dispatch {dispatch
@ -154,15 +154,17 @@ function ShipRow(props: {
<span>{progress}%</span> <span>{progress}%</span>
<span> <span>
Arrives in{" "} Arrives in{" "}
<span className="text-cyan-200"> <span className="text-ink-100">
<Countdown to={dispatch.arrivalAt} /> <Countdown to={dispatch.arrivalAt} />
</span> </span>
</span> </span>
</div> </div>
</div> </div>
)} )}
<div className="grid grid-cols-2 gap-3">
<FuelGauge ship={ship} /> <FuelGauge ship={ship} />
<HullGauge ship={ship} /> <HullGauge ship={ship} />
</div>
<ServiceTag ship={ship} /> <ServiceTag ship={ship} />
{low && ( {low && (
<p <p
@ -177,7 +179,7 @@ function ShipRow(props: {
</p> </p>
)} )}
{inShop && ( {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"} —{" "} {ship.repairing ? "Under repair" : "In the shop"} —{" "}
<Countdown to={ship.inShopUntil!} format={formatClock} done="done" /> <Countdown to={ship.inShopUntil!} format={formatClock} done="done" />
</p> </p>
@ -217,17 +219,18 @@ function ShipRow(props: {
variant="secondary" variant="secondary"
size="sm" size="sm"
onClick={props.onDeadhead} onClick={props.onDeadhead}
className="flex-1 basis-40" aria-label="Deadhead (fly empty)"
className="flex-1 basis-32"
> >
<Icon name="send" className="size-4" /> <Icon name="send" className="size-4" />
Deadhead (fly empty) Fly empty
</Button> </Button>
)} )}
</div> </div>
)} )}
<div className="flex gap-2"> <div className="flex gap-2">
<Button <Button
variant="ghost" variant="secondary"
size="sm" size="sm"
onClick={props.onFuel} onClick={props.onFuel}
aria-label={`Fuel for ${ship.name}`} aria-label={`Fuel for ${ship.name}`}
@ -236,7 +239,7 @@ function ShipRow(props: {
Fuel Fuel
</Button> </Button>
<Button <Button
variant="ghost" variant="secondary"
size="sm" size="sm"
onClick={props.onUpgrades} onClick={props.onUpgrades}
aria-label={`Upgrades for ${ship.name}`} aria-label={`Upgrades for ${ship.name}`}
@ -378,7 +381,7 @@ export function DeadheadPicker({ ship, onClose }: { ship: Ship; onClose: () => v
{ onSuccess: onClose }, { 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" />} {sprite && <Art sprite={sprite} className="size-10 shrink-0" />}
<span className="min-w-0 flex-1"> <span className="min-w-0 flex-1">

View File

@ -110,7 +110,7 @@ function HqCard({ bay }: { bay: Bay }) {
{state.ships.find((s) => s.id === d.shipId)?.name ?? "A ship"} from{" "} {state.ships.find((s) => s.id === d.shipId)?.name ?? "A ship"} from{" "}
{locationName(d.originId)} {locationName(d.originId)}
</span> </span>
<span className="font-display text-cyan-200"> <span className="font-display text-ink-100">
<Countdown to={d.arrivalAt} /> <Countdown to={d.arrivalAt} />
</span> </span>
</li> </li>

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-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 New contracts posted
</span> </span>
@ -189,7 +189,7 @@ function LocationBoard({
<div className="space-y-3"> <div className="space-y-3">
<Panel tone="muted" role="status" className="text-center text-sm text-ink-300"> <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{" "} 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" /> <Countdown to={board.data.periodEndsAt} format={formatClock} done="a moment" />
</span> </span>
. .
@ -281,8 +281,8 @@ function NothingHere({
tone="cyan" tone="cyan"
className="text-sm" className="text-sm"
> >
<p className="flex items-center gap-2 text-cyan-100"> <p className="flex items-center gap-2 text-ink-100">
<Icon name="alert" className="size-4 shrink-0 text-cyan-300" /> <Icon name="alert" className="size-4 shrink-0 text-ink-300" />
No job here that {ship.name} and {pilot.name} can take. No job here that {ship.name} and {pilot.name} can take.
</p> </p>
<div className="mt-3 flex flex-wrap gap-2"> <div className="mt-3 flex flex-wrap gap-2">

View File

@ -81,14 +81,14 @@ export function Locations() {
</Pill> </Pill>
)} )}
</div> </div>
<span className="font-display text-xs text-cyan-300/80"> <span className="font-display text-xs text-ink-300">
{location.id === "earth" {location.id === "earth"
? "Home" ? "Home"
: `${formatMillionKm(distanceAt("earth", location.id))} from Earth`} : `${formatMillionKm(distanceAt("earth", location.id))} from Earth`}
</span> </span>
</div> </div>
</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> <dt className="font-display text-xs text-ink-400">Type</dt>
<dd>{TYPE_LABELS[location.type]}</dd> <dd>{TYPE_LABELS[location.type]}</dd>
<dt className="font-display text-xs text-ink-400">Landing</dt> <dt className="font-display text-xs text-ink-400">Landing</dt>

View File

@ -65,7 +65,7 @@ export function Logbook() {
<select <select
value={pilotId} value={pilotId}
onChange={(e) => set("pilot", e.target.value)} 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> <option value="">All pilots</option>
{state.pilots.map((p) => ( {state.pilots.map((p) => (
@ -77,11 +77,11 @@ export function Logbook() {
</label> </label>
<fieldset className="flex flex-col gap-1"> <fieldset className="flex flex-col gap-1">
<legend className="text-ink-400">Time</legend> <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]) => ( {RANGES.map(([value, label]) => (
<label <label
key={value} 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 <input
type="radio" type="radio"

View File

@ -171,7 +171,7 @@ function FlightPanel({ dispatchId, onClose }: { dispatchId: string; onClose: ()
<p className="text-ink-300">Pilot: {pilotName}</p> <p className="text-ink-300">Pilot: {pilotName}</p>
<p className="text-ink-300"> <p className="text-ink-300">
Arrives in{" "} Arrives in{" "}
<span className="font-display text-cyan-200"> <span className="font-display text-ink-100">
<Countdown to={dispatch.arrivalAt} /> <Countdown to={dispatch.arrivalAt} />
</span>{" "} </span>{" "}
({formatGameTime(realToGame(dispatch.arrivalAt, content.config), clockFormat)}) ({formatGameTime(realToGame(dispatch.arrivalAt, content.config), clockFormat)})

View File

@ -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> <dd className="font-display font-semibold tabular-nums">{props.value}</dd>
</div> </div>
{props.share !== undefined && ( {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 <div
className="h-full rounded-full bg-linear-to-r from-cyan-500 to-sky-300" className="h-full rounded-full bg-linear-to-r from-cyan-500 to-sky-300"
style={{ width: `${Math.max(4, props.share * 100)}%` }} style={{ width: `${Math.max(4, props.share * 100)}%` }}
@ -192,14 +192,14 @@ function DeliveryPicker(props: {
return ( return (
<label <label
key={bay.id} 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 <input
type="radio" type="radio"
name="hq" name="hq"
checked={bayId === bay.id} checked={bayId === bay.id}
onChange={() => setBayId(bay.id)} onChange={() => setBayId(bay.id)}
className="size-4 accent-amber-400" className="size-4 accent-action"
/> />
{sprite && <Art sprite={sprite} className="size-8" />} {sprite && <Art sprite={sprite} className="size-8" />}
<span className="font-display font-semibold">{locationName(bay.locationId)}</span> <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; if (upgraded.length === 0) return null;
return ( 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) => { {upgraded.map((ship) => {
const s = shipStatsOf(ship); const s = shipStatsOf(ship);
return ( return (

View File

@ -55,21 +55,18 @@ function HirePilot() {
const hasBay = state.bays.some((b) => b.locationId === bay); const hasBay = state.bays.some((b) => b.locationId === bay);
return ( return (
<Panel tone="amber" className="text-sm"> <Panel className="text-sm">
{state.bays.length === 0 ? ( {state.bays.length === 0 ? (
<p className="text-ink-300">Lease a bay to hire pilots there.</p> <p className="text-ink-300">Lease a bay to hire pilots there.</p>
) : ( ) : (
<div className="flex flex-wrap items-center gap-3"> <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"> <label className="flex items-center gap-2">
<span className="font-display text-xs text-ink-300">Hire at</span> <span className="font-display text-xs text-ink-300">Hire at</span>
<select <select
aria-label="Hire at" aria-label="Hire at"
value={bay} value={bay}
onChange={(e) => setBay(e.target.value)} 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) => ( {state.bays.map((b) => (
<option key={b.id} value={b.locationId}> <option key={b.id} value={b.locationId}>
@ -79,6 +76,8 @@ function HirePilot() {
</select> </select>
</label> </label>
<Button <Button
variant="secondary"
size="sm"
disabled={!hasBay || state.company.credits < cost || hire.isPending} disabled={!hasBay || state.company.credits < cost || hire.isPending}
onClick={() => hire.mutate({ locationId: bay })} onClick={() => hire.mutate({ locationId: bay })}
className="ml-auto" className="ml-auto"
@ -114,29 +113,29 @@ function PilotCard({ pilot }: { pilot: Pilot }) {
<div className="flex items-center gap-3"> <div className="flex items-center gap-3">
<span <span
aria-hidden="true" aria-hidden="true"
className={`relative grid size-14 shrink-0 place-items-center rounded-full font-display text-lg font-bold ring-2 ${ className={`relative grid size-12 shrink-0 place-items-center rounded-full text-base font-semibold ring-1 ${
pilot.isAvatar pilot.isAvatar ? "text-action ring-action/60" : "text-ink-100 ring-glass-line"
? "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"
}`} }`}
> >
{initials} {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} {pilot.level}
</span> </span>
</span> </span>
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
<div className="flex items-baseline justify-between gap-2"> <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.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>
<span className="font-display text-xs whitespace-nowrap text-ink-300"> <span className="font-display text-xs whitespace-nowrap text-ink-300">
Level {pilot.level} Level {pilot.level}
</span> </span>
</div> </div>
{pilot.isVeteran && ( {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} {content.veteranPilot.bio}
</p> </p>
)} )}
@ -145,9 +144,9 @@ function PilotCard({ pilot }: { pilot: Pilot }) {
? `${progress.xp} / ${progress.needed} XP to next level` ? `${progress.xp} / ${progress.needed} XP to next level`
: `${pilot.xp} XP · max level`} : `${pilot.xp} XP · max level`}
</div> </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 <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}%` }} style={{ width: `${progress ? (progress.xp / progress.needed) * 100 : 100}%` }}
/> />
</div> </div>
@ -170,28 +169,28 @@ function PilotCard({ pilot }: { pilot: Pilot }) {
: "No ship"} : "No ship"}
</div> </div>
<div className="mt-3 rounded-lg bg-space-950/50 p-2.5 ring-1 ring-space-700"> <div className="mt-3">
<div <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:{" "} Skill points:{" "}
<span aria-label={`${pilot.name} skill points`}>{pilot.skillPointsAvailable}</span> <span aria-label={`${pilot.name} skill points`}>{pilot.skillPointsAvailable}</span>
</div> </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) => { {content.skills.map((skill) => {
const level = pilot.skills[skill.id] ?? 0; const level = pilot.skills[skill.id] ?? 0;
const canRaise = pilot.skillPointsAvailable > 0 && level < skill.maxLevel; const canRaise = pilot.skillPointsAvailable > 0 && level < skill.maxLevel;
return ( 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="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} {skill.name} {level}/{skill.maxLevel}
</span> </span>
<span aria-hidden="true" className="mt-0.5 flex gap-0.5"> <span aria-hidden="true" className="mt-0.5 flex gap-0.5">
{Array.from({ length: skill.maxLevel }, (_, i) => ( {Array.from({ length: skill.maxLevel }, (_, i) => (
<span <span
key={i} 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> </span>
@ -202,7 +201,7 @@ function PilotCard({ pilot }: { pilot: Pilot }) {
aria-label={`Raise ${skill.name} for ${pilot.name}`} aria-label={`Raise ${skill.name} for ${pilot.name}`}
disabled={spend.isPending} disabled={spend.isPending}
onClick={() => spend.mutate({ pilotId: pilot.id, skillId: skill.id })} 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" /> <Icon name="plus" className="size-4" />
</button> </button>
@ -244,7 +243,7 @@ function Duty({ pilot, canRest }: { pilot: Pilot; canRest: boolean }) {
if (pilot.restingInShopUntil !== null) { if (pilot.restingInShopUntil !== null) {
return ( return (
<div <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`} aria-label={`${pilot.name} rest`}
> >
<Icon name="rest" className="size-4 shrink-0" /> <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) { if (pilot.resting && pilot.restEndsAt !== null) {
return ( return (
<div <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`} aria-label={`${pilot.name} rest`}
> >
<Icon name="rest" className="size-4 shrink-0" /> <Icon name="rest" className="size-4 shrink-0" />
@ -278,7 +277,7 @@ function Duty({ pilot, canRest }: { pilot: Pilot; canRest: boolean }) {
label={`${pilot.name} duty`} label={`${pilot.name} duty`}
value={used} value={used}
max={pilot.dutyLimit} max={pilot.dutyLimit}
tone={used / pilot.dutyLimit >= 0.85 ? "rose" : "amber"} tone={used / pilot.dutyLimit >= 0.85 ? "rose" : "default"}
size="sm" size="sm"
className="mt-1" className="mt-1"
/> />
@ -345,7 +344,7 @@ function AssignShip({ pilot }: { pilot: Pilot }) {
aria-label={`Assign ${pilot.name} to`} aria-label={`Assign ${pilot.name} to`}
value={shipId} value={shipId}
onChange={(e) => setShipId(e.target.value)} 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> <option value="">Choose a ship…</option>
{ships.map((s) => ( {ships.map((s) => (

View File

@ -27,7 +27,7 @@ async function shareFile(file: File): Promise<void> {
} }
const codeBox = 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 save" (decision 0.5.92): the whole company as one code, to copy or share as a file. */
export function ExportSave() { export function ExportSave() {
@ -137,7 +137,7 @@ export function ImportSave({ hasCompany }: { hasCompany: boolean }) {
{hasCompany && ( {hasCompany && (
<p <p
role="note" 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. This replaces the company on this device. Export it first if you want to keep it.
</p> </p>
@ -187,7 +187,7 @@ export function ImportSave({ hasCompany }: { hasCompany: boolean }) {
</p> </p>
)} )}
{confirming && ( {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?"} {hasCompany ? "Replace your company with this save?" : "Open this company?"}
</p> </p>
)} )}

View File

@ -161,11 +161,11 @@ function Choice<T extends string>(props: {
return ( return (
<fieldset className="flex flex-wrap items-center justify-between gap-2 text-sm"> <fieldset className="flex flex-wrap items-center justify-between gap-2 text-sm">
<legend className="float-left text-ink-400">{props.label}</legend> <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]) => ( {props.options.map(([value, text]) => (
<label <label
key={value} 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 <input
type="radio" type="radio"

View File

@ -83,7 +83,7 @@ function HullRepair({ shipId, atHq }: { shipId: string; atHq: boolean }) {
return ( return (
<section <section
aria-label="Hull" 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"> <div className="flex items-baseline justify-between gap-2">
<span className="font-display font-bold">Hull</span> <span className="font-display font-bold">Hull</span>
@ -171,7 +171,7 @@ export function ShipUpgrades({ shipId, onClose }: { shipId: string; onClose: ()
<li <li
key={upgrade.id} key={upgrade.id}
aria-label={upgrade.name} 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"> <div className="flex items-baseline justify-between gap-2">
<span className="font-display font-bold">{upgrade.name}</span> <span className="font-display font-bold">{upgrade.name}</span>
@ -203,7 +203,7 @@ export function ShipUpgrades({ shipId, onClose }: { shipId: string; onClose: ()
<div <div
role="group" role="group"
aria-label={`Start ${upgrade.name} ${check.level}?`} 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"> <p className="text-ink-200">
{upgrade.name} {check.level} costs {formatCredits(check.cost)}. {ship.name}{" "} {upgrade.name} {check.level} costs {formatCredits(check.cost)}. {ship.name}{" "}

View File

@ -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"> <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 DEADHEAD
</h1> </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> </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

@ -49,7 +49,7 @@ export function TutorialBoard() {
<Panel aria-label="Intro" tone="amber" className="flex items-center gap-3 text-sm"> <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" /> <Art sprite={loaner.sprite} className="size-16 shrink-0 -rotate-12" />
<div> <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. {content.tutorialCompanyName} needs a pilot.
</p> </p>
<p className="text-ink-200">Their ship, your hands. Get the crates to the Moon.</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. 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-cyan-100"> <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-cyan-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>
)} )}
@ -139,7 +139,7 @@ export function TutorialBoard() {
destinationId={contract.destinationId} destinationId={contract.destinationId}
size="sm" 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"> <dl className="grid flex-1 grid-cols-3 gap-2 text-sm">
<div> <div>
<dt className="font-display text-xs text-ink-400">Pay</dt> <dt className="font-display text-xs text-ink-400">Pay</dt>

View File

@ -27,6 +27,7 @@ export function Sheet({
subtitle, subtitle,
tone = "default", tone = "default",
layer = "z-20", layer = "z-20",
side = false,
children, children,
}: { }: {
titleId: string; titleId: string;
@ -36,17 +37,19 @@ export function Sheet({
tone?: Tone; tone?: Tone;
/** Stacking layer, e.g. `z-30` for a sheet over other sheets. */ /** Stacking layer, e.g. `z-30` for a sheet over other sheets. */
layer?: string; layer?: string;
/** From `xl`, opens beside the wide panel and leaves it usable (decision 0.5.169). */
side?: boolean;
children: ReactNode; children: ReactNode;
}) { }) {
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`} 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 <div
role="dialog" role="dialog"
aria-modal="true" aria-modal="true"
aria-labelledby={titleId} 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 <div
aria-hidden="true" aria-hidden="true"