157 lines
5.0 KiB
TypeScript
157 lines
5.0 KiB
TypeScript
import { useState } from "react";
|
|
import type { ReactNode } from "react";
|
|
import { WelcomeCards } from "../components/Welcome";
|
|
import { SpendingFloor } from "../components/IndependentPilots";
|
|
import { resetMapView } from "../map/camera";
|
|
import { About } from "./AboutSettings";
|
|
import { CompanyName, NewCompany } from "./CompanySettings";
|
|
import { DevTools } from "./DevTools";
|
|
import { ExportSave, ImportSave } from "./SaveSettings";
|
|
import { useLoadedPlayer } from "../state/actions";
|
|
import { usePreferences, type ClockFormat, type CountdownFormat } from "../state/preferences";
|
|
import { Button, Panel, Screen, ScreenHeader } from "../ui";
|
|
|
|
/** Developer tools only exist in non-production builds (CR-011). */
|
|
export function isDevBuild(): boolean {
|
|
return !import.meta.env.PROD;
|
|
}
|
|
|
|
/** Settings (CR-011): company, display, help, about and, in development, developer tools. */
|
|
export function Settings() {
|
|
const state = useLoadedPlayer();
|
|
const prefs = usePreferences();
|
|
const [howToPlay, setHowToPlay] = useState(false);
|
|
|
|
return (
|
|
<Screen className="max-w-2xl">
|
|
<ScreenHeader title="Settings" eyebrow={state.company.name} icon="gear" />
|
|
|
|
<Section title="Company">
|
|
<CompanyName />
|
|
<SpendingFloor />
|
|
<div className="flex flex-wrap gap-2">
|
|
<ExportSave />
|
|
<ImportSave hasCompany />
|
|
<NewCompany />
|
|
</div>
|
|
</Section>
|
|
|
|
<Section title="Display">
|
|
<Choice<ClockFormat>
|
|
label="Clock"
|
|
value={prefs.clock}
|
|
options={[
|
|
["locale", "Device"],
|
|
["12h", "12-hour"],
|
|
["24h", "24-hour"],
|
|
]}
|
|
onChange={(clock) => prefs.set({ clock })}
|
|
/>
|
|
<Choice<CountdownFormat>
|
|
label="Countdowns"
|
|
value={prefs.countdown}
|
|
options={[
|
|
["real", "Real time"],
|
|
["game", "Game time"],
|
|
["both", "Both"],
|
|
]}
|
|
onChange={(countdown) => prefs.set({ countdown })}
|
|
/>
|
|
<Choice<"device" | "on" | "off">
|
|
label="Reduced motion"
|
|
value={prefs.reducedMotion === null ? "device" : prefs.reducedMotion ? "on" : "off"}
|
|
options={[
|
|
["device", "Device"],
|
|
["on", "On"],
|
|
["off", "Off"],
|
|
]}
|
|
onChange={(v) => prefs.set({ reducedMotion: v === "device" ? null : v === "on" })}
|
|
/>
|
|
</Section>
|
|
|
|
<Section title="Notifications">
|
|
<Choice<"on" | "off">
|
|
label="Arrivals, rest and upgrades"
|
|
value={prefs.notifications ? "on" : "off"}
|
|
options={[
|
|
["on", "On"],
|
|
["off", "Off"],
|
|
]}
|
|
onChange={(v) => prefs.set({ notifications: v === "on" })}
|
|
/>
|
|
<p className="text-xs text-ink-400">On the iOS and Android apps only.</p>
|
|
</Section>
|
|
|
|
<Section title="Help">
|
|
<div className="flex flex-wrap gap-2">
|
|
<Button variant="secondary" size="sm" onClick={resetMapView}>
|
|
Reset map view
|
|
</Button>
|
|
<Button variant="secondary" size="sm" onClick={() => prefs.showTipsAgain()}>
|
|
Show tips again
|
|
</Button>
|
|
<Button variant="secondary" size="sm" onClick={() => setHowToPlay(true)}>
|
|
How to play
|
|
</Button>
|
|
</div>
|
|
{/* The welcome cards again, ending in Close (decision 0.5.132). */}
|
|
{howToPlay && <WelcomeCards final="close" onClose={() => setHowToPlay(false)} />}
|
|
</Section>
|
|
|
|
<Section title="About">
|
|
<About />
|
|
</Section>
|
|
|
|
{isDevBuild() && (
|
|
<Section title="Developer">
|
|
<DevTools />
|
|
</Section>
|
|
)}
|
|
</Screen>
|
|
);
|
|
}
|
|
|
|
function Section({ title, children }: { title: string; children: ReactNode }) {
|
|
const id = `settings-${title.toLowerCase()}`;
|
|
return (
|
|
<Panel as="section" aria-labelledby={id} className="space-y-3">
|
|
<h2 id={id} className="font-display text-sm font-bold tracking-wider text-ink-300 uppercase">
|
|
{title}
|
|
</h2>
|
|
{children}
|
|
</Panel>
|
|
);
|
|
}
|
|
|
|
/** A labelled radio group of a few options, styled as a segmented control. */
|
|
function Choice<T extends string>(props: {
|
|
label: string;
|
|
value: T;
|
|
options: [T, string][];
|
|
onChange: (value: T) => void;
|
|
}) {
|
|
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">
|
|
{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"
|
|
>
|
|
<input
|
|
type="radio"
|
|
name={props.label}
|
|
value={value}
|
|
checked={props.value === value}
|
|
onChange={() => props.onChange(value)}
|
|
className="sr-only"
|
|
/>
|
|
{text}
|
|
</label>
|
|
))}
|
|
</div>
|
|
</fieldset>
|
|
);
|
|
}
|