tfrere's picture
tfrere HF Staff
feat(frontend): editor refresh (embed studio, comment popover, shiki, top bar, hooks, styles)
76fc93a
Raw
History Blame Contribute Delete
6.13 kB
import { useRef, useState, useCallback } from "react";
import { OKLCH_L, OKLCH_C } from "#shared/theme";
const COLOR_NAMES = [
{ name: "Candy Apple Red", hue: 0 },
{ name: "Boiling Magma", hue: 15 },
{ name: "Aerospace Orange", hue: 25 },
{ name: "Burtuqali Orange", hue: 33 },
{ name: "American Orange", hue: 40 },
{ name: "Cheese", hue: 48 },
{ name: "Amber", hue: 55 },
{ name: "Demonic Yellow", hue: 65 },
{ name: "Bat-Signal", hue: 72 },
{ name: "Bright Yellow Green", hue: 85 },
{ name: "Lasting Lime", hue: 95 },
{ name: "Bright Green", hue: 110 },
{ name: "Chlorophyll Green", hue: 120 },
{ name: "Green Screen", hue: 130 },
{ name: "Cathode Green", hue: 148 },
{ name: "Booger Buster", hue: 160 },
{ name: "Enthusiasm", hue: 170 },
{ name: "Ice Ice Baby", hue: 180 },
{ name: "Vivid Sky Blue", hue: 195 },
{ name: "Azure", hue: 214 },
{ name: "Blue Ribbon", hue: 225 },
{ name: "Icelandic Water", hue: 238 },
{ name: "Blue Pencil", hue: 250 },
{ name: "Electric Ultramarine", hue: 260 },
{ name: "Purple Climax", hue: 275 },
{ name: "Amethyst Ganzstar", hue: 285 },
{ name: "Electric Purple", hue: 295 },
{ name: "Phlox", hue: 305 },
{ name: "Brusque Pink", hue: 315 },
{ name: "Bright Magenta", hue: 325 },
{ name: "Pink", hue: 335 },
{ name: "Hot Flamingoes", hue: 345 },
{ name: "Carmine Red", hue: 355 },
];
function getColorName(hue: number): string {
let best = COLOR_NAMES[0].name;
let bestDist = 361;
for (const c of COLOR_NAMES) {
const d = Math.abs(c.hue - hue);
const dist = Math.min(d, 360 - d);
if (dist < bestDist) {
bestDist = dist;
best = c.name;
}
}
return best;
}
function oklchToHex(l: number, c: number, h: number): string {
const hRad = (h * Math.PI) / 180;
const a = c * Math.cos(hRad);
const b = c * Math.sin(hRad);
const l_ = l + 0.3963377774 * a + 0.2158037573 * b;
const m_ = l - 0.1055613458 * a - 0.0638541728 * b;
const s_ = l - 0.0894841775 * a - 1.291485548 * b;
const ll = l_ * l_ * l_;
const mm = m_ * m_ * m_;
const ss = s_ * s_ * s_;
const toSrgb = (u: number) =>
u <= 0.0031308 ? 12.92 * u : 1.055 * Math.pow(Math.max(0, u), 1 / 2.4) - 0.055;
const r = toSrgb(+4.0767416621 * ll - 3.3077115913 * mm + 0.2309699292 * ss);
const g = toSrgb(-1.2684380046 * ll + 2.6097574011 * mm - 0.3413193965 * ss);
const bVal = toSrgb(-0.0041960863 * ll - 0.7034186147 * mm + 1.707614701 * ss);
const clamp = (v: number) => Math.max(0, Math.min(255, Math.round(v * 255)));
const hex = (v: number) => clamp(v).toString(16).padStart(2, "0");
return `#${hex(r)}${hex(g)}${hex(bVal)}`;
}
interface HueSliderProps {
hue: number;
onChange: (hue: number) => void;
}
export function HueSlider({ hue, onChange }: HueSliderProps) {
const sliderRef = useRef<HTMLDivElement>(null);
const [dragging, setDragging] = useState(false);
const hexColor = oklchToHex(OKLCH_L, OKLCH_C, hue);
const colorName = getColorName(hue);
const hueFromClientX = useCallback((clientX: number) => {
const rect = sliderRef.current?.getBoundingClientRect();
if (!rect) return hue;
const x = Math.max(0, Math.min(clientX - rect.left, rect.width));
return (x / rect.width) * 360;
}, [hue]);
const handlePointerDown = useCallback((e: React.PointerEvent) => {
e.preventDefault();
(e.target as HTMLElement).setPointerCapture(e.pointerId);
setDragging(true);
onChange(hueFromClientX(e.clientX));
}, [hueFromClientX, onChange]);
const handlePointerMove = useCallback((e: React.PointerEvent) => {
if (!dragging) return;
onChange(hueFromClientX(e.clientX));
}, [dragging, hueFromClientX, onChange]);
const handlePointerUp = useCallback(() => {
setDragging(false);
}, []);
const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
const step = e.shiftKey ? 10 : 2;
if (e.key === "ArrowLeft") { e.preventDefault(); onChange(((hue - step) % 360 + 360) % 360); }
if (e.key === "ArrowRight") { e.preventDefault(); onChange((hue + step) % 360); }
}, [hue, onChange]);
return (
<div style={{ display: "flex", flexDirection: "column", gap: 12 }}>
<div style={{ display: "flex", alignItems: "center", gap: 12 }}>
<div
style={{
width: 44,
height: 44,
borderRadius: 8,
backgroundColor: hexColor,
border: "1px solid var(--ed-border)",
flexShrink: 0,
}}
/>
<div style={{ minWidth: 0 }}>
<div style={{ fontWeight: 700, fontSize: "0.8rem", lineHeight: 1.3, color: "var(--ed-text)" }}>
{colorName}
</div>
<div style={{ color: "var(--ed-text-secondary)", fontSize: "0.7rem", fontFamily: "monospace" }}>
{hexColor.toUpperCase()} - {Math.round(hue)}&deg;
</div>
</div>
</div>
<div
ref={sliderRef}
role="slider"
tabIndex={0}
aria-label="Hue"
aria-valuemin={0}
aria-valuemax={360}
aria-valuenow={Math.round(hue)}
onPointerDown={handlePointerDown}
onPointerMove={handlePointerMove}
onPointerUp={handlePointerUp}
onKeyDown={handleKeyDown}
style={{
position: "relative",
height: 16,
borderRadius: 10,
border: "1px solid var(--ed-border)",
background: "linear-gradient(to right, #f00 0%, #ff0 17%, #0f0 33%, #0ff 50%, #00f 67%, #f0f 83%, #f00 100%)",
cursor: "ew-resize",
touchAction: "none",
}}
>
<div
style={{
position: "absolute",
top: "50%",
left: `${(hue / 360) * 100}%`,
width: 14,
height: 14,
borderRadius: "50%",
border: "2px solid #fff",
transform: "translate(-50%, -50%)",
backgroundColor: hexColor,
boxShadow: "0 0 0 1px rgba(0,0,0,0.15), 0 1px 3px rgba(0,0,0,0.3)",
pointerEvents: "none",
}}
/>
</div>
</div>
);
}
export { oklchToHex };