1
0
Fork 0
polaris-task-force/src/components/frontend/minigames/qualification/CrosshairSettings.tsx
2026-09-11 03:25:21 -04:00

201 lines
6.8 KiB
TypeScript

"use client";
import { useId, type ChangeEvent } from "react";
import { Crosshair } from "lucide-react";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Popover, PopoverContent, PopoverTrigger } from "@/components/ui/popover";
import {
AIM_TRAINER_CROSSHAIR_SCALE_MAX,
AIM_TRAINER_CROSSHAIR_SCALE_MIN,
AIM_TRAINER_CROSSHAIR_SCALE_STEP,
AIM_TRAINER_CROSSHAIR_STYLES,
normalizeAimTrainerCrosshairScale,
type AimTrainerCrosshairPrefs,
type AimTrainerCrosshairStyle,
} from "./crosshair";
const STYLE_LABELS: Record<AimTrainerCrosshairStyle, string> = {
classic: "Classic",
dot: "Dot",
cross: "Cross",
circle: "Circle",
chevron: "Chevron",
};
const COLOR_PRESETS = [
"#d8d3b6",
"#4ade80",
"#f87171",
"#22d3ee",
"#e879f9",
"#ffffff",
] as const;
function CrosshairPreview({ style }: { style: AimTrainerCrosshairStyle }) {
const lineProps = {
stroke: "currentColor",
strokeWidth: 1.75,
strokeLinecap: "butt" as const,
strokeLinejoin: "miter" as const,
};
switch (style) {
case "classic":
return (
<svg viewBox="-12 -12 24 24" className="size-5" aria-hidden="true">
<path d="M0 -11V-3 M0 3V11 M-11 0H-3 M3 0H11" fill="none" {...lineProps} />
<circle cx="0" cy="0" r="0.75" fill="currentColor" />
</svg>
);
case "dot":
return (
<svg viewBox="-12 -12 24 24" className="size-5" aria-hidden="true">
<circle cx="0" cy="0" r="1.5" fill="currentColor" />
</svg>
);
case "cross":
return (
<svg viewBox="-12 -12 24 24" className="size-5" aria-hidden="true">
<path d="M0 -11V11 M-11 0H11" fill="none" {...lineProps} />
</svg>
);
case "circle":
return (
<svg viewBox="-12 -12 24 24" className="size-5" aria-hidden="true">
<circle cx="0" cy="0" r="9" fill="none" {...lineProps} />
<circle cx="0" cy="0" r="0.75" fill="currentColor" />
</svg>
);
case "chevron":
return (
<svg viewBox="-12 -12 24 24" className="size-5" aria-hidden="true">
<path d="M-5 0L0 -6L5 0" fill="none" {...lineProps} />
<circle cx="0" cy="0" r="0.75" fill="currentColor" />
</svg>
);
}
}
export function CrosshairSettings({
value,
onPrefsChangeAction,
}: {
readonly value: AimTrainerCrosshairPrefs;
readonly onPrefsChangeAction: (prefs: AimTrainerCrosshairPrefs) => void;
}) {
const colorInputId = useId();
const handleCustomColorChange = (event: ChangeEvent<HTMLInputElement>) => {
onPrefsChangeAction({ ...value, color: event.target.value });
};
const handleScaleChange = (event: ChangeEvent<HTMLInputElement>) => {
onPrefsChangeAction({
...value,
scale: normalizeAimTrainerCrosshairScale(Number(event.target.value)),
});
};
return (
<Popover>
<PopoverTrigger asChild>
<Button type="button" variant="ghost" size="icon" aria-label="Crosshair settings">
<Crosshair aria-hidden="true" className="size-4" />
</Button>
</PopoverTrigger>
<PopoverContent align="end" collisionPadding={8} className="w-[300px] p-3">
<div className="flex flex-col gap-3">
<div className="flex flex-col gap-2">
<span className="text-xs font-medium uppercase tracking-wider text-muted-foreground">
Crosshair style
</span>
<div className="grid grid-cols-5 gap-1">
{AIM_TRAINER_CROSSHAIR_STYLES.map((style) => (
<Button
key={style}
type="button"
variant={value.style === style ? "default" : "outline"}
className="h-auto min-w-0 flex-col gap-1 whitespace-normal px-1 py-1.5 text-center text-xs"
aria-label={`${STYLE_LABELS[style]} crosshair`}
aria-pressed={value.style === style}
onClick={() => onPrefsChangeAction({ ...value, style })}
>
<CrosshairPreview style={style} />
<span>{STYLE_LABELS[style]}</span>
</Button>
))}
</div>
</div>
<div className="flex flex-col gap-2">
<span className="text-xs font-medium uppercase tracking-wider text-muted-foreground">
Crosshair color
</span>
<div className="flex flex-wrap items-center gap-1.5">
{COLOR_PRESETS.map((color) => {
const selected = value.color.toLowerCase() === color.toLowerCase();
return (
<Button
key={color}
type="button"
variant="ghost"
size="icon"
className={
selected ? "size-7 p-1 ring-2 ring-ring ring-offset-2" : "size-7 p-1"
}
aria-label={`Use ${color} crosshair color`}
aria-pressed={selected}
onClick={() => onPrefsChangeAction({ ...value, color })}
>
<span
aria-hidden="true"
className="size-4 rounded-sm border border-black/30"
style={{ backgroundColor: color }}
/>
</Button>
);
})}
</div>
<label
htmlFor={colorInputId}
className="flex w-full items-center justify-between gap-3 text-xs text-muted-foreground"
>
<span>Custom</span>
<Input
id={colorInputId}
type="color"
value={value.color}
aria-label="Custom crosshair color"
className="h-8 w-10 cursor-pointer p-1"
onChange={handleCustomColorChange}
/>
</label>
</div>
<div className="flex flex-col gap-2">
<div className="flex items-center justify-between gap-3">
<span className="text-xs font-medium uppercase tracking-wider text-muted-foreground">
Crosshair scale
</span>
<span className="font-mono text-xs tabular-nums text-muted-foreground">
{value.scale.toFixed(1)}x
</span>
</div>
<input
type="range"
min={AIM_TRAINER_CROSSHAIR_SCALE_MIN}
max={AIM_TRAINER_CROSSHAIR_SCALE_MAX}
step={AIM_TRAINER_CROSSHAIR_SCALE_STEP}
value={value.scale}
aria-label="Crosshair scale"
className="h-4 w-full cursor-pointer accent-primary"
onChange={handleScaleChange}
/>
</div>
</div>
</PopoverContent>
</Popover>
);
}