Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
201 lines
6.8 KiB
TypeScript
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>
|
|
);
|
|
}
|