Files
EpicNext-Cms/src/components/admin/color-field.tsx
T
openhands 8c12fc6c60
CI / check (push) Failing after 1m35s
CI / deploy (push) Skipped
CI / e2e (push) Skipped
refactor(cms): deduplicate shared admin components and fix studio batch completion
- fix(studio): stop markBatchDone infinite recursion so batches complete
- refactor(api): merge api-response into api and drop the duplicate module
- refactor(media): extract shared media loader and URL validator
- refactor(ui): extract shared LoadingSpinner for site and admin groups
- refactor(dates): consolidate raw date formatting into formatDate util
- refactor(logs): share a single generic log-list loader across tables
- refactor(theme): merge both ColorField components and reuse contrast helpers
- refactor(import): extract shared ImportErrorBanner and SearchInput
- chore(): remove dead theme-editor-tabs after inlining tab components
2026-09-05 20:58:42 +02:00

148 lines
3.0 KiB
TypeScript

"use client";
import { useState } from "react";
import { contrastRatio, readableColor } from "@/lib/theme-contrast";
type ColorFieldProps = {
name: string;
label: string;
description: string;
defaultValue: string;
/** Background color to check the foreground contrast against. */
bgValue?: string;
};
export function ColorField({
name,
label,
description,
defaultValue,
bgValue,
}: ColorFieldProps) {
const [val, setVal] = useState(defaultValue);
const ratio = bgValue ? contrastRatio(val, bgValue) : null;
const poor = ratio != null && ratio < 4.5;
const ok = ratio != null && ratio >= 4.5;
return (
<div
style={{
display: "flex",
gap: "0.6rem",
padding: "0.5rem",
borderRadius: 8,
border: poor ? "1px solid #ef4444" : "1px solid transparent",
background: poor ? "rgba(239,68,68,0.08)" : "transparent",
}}
>
<input
type="color"
name={name}
value={val}
onChange={(e) => {
const next = e.target.value;
if (bgValue) {
const nextRatio = contrastRatio(next, bgValue);
if (nextRatio != null && nextRatio < 4.5) {
setVal(readableColor("#ffffff", [bgValue]));
return;
}
}
setVal(next);
}}
style={{
width: 42,
height: 36,
padding: 0,
border: "none",
background: "none",
cursor: "pointer",
flexShrink: 0,
}}
/>
<div
style={{
display: "flex",
flexDirection: "column",
minWidth: 0,
flex: 1,
}}
>
<div
style={{
display: "flex",
justifyContent: "space-between",
alignItems: "baseline",
gap: "0.5rem",
}}
>
<span
style={{ fontSize: "0.82rem", fontWeight: 600, lineHeight: 1.2 }}
>
{label}
</span>
{ratio != null ? (
<span
style={{
fontSize: "0.7rem",
fontWeight: 700,
color: ok ? "#22c55e" : "#ef4444",
whiteSpace: "nowrap",
}}
>
{ratio.toFixed(1)}:1 {ok ? "✓" : "⚠"}
</span>
) : null}
</div>
<span
style={{
fontSize: "0.7rem",
color: "var(--admin-text-muted)",
lineHeight: 1.3,
display: "-webkit-box",
WebkitLineClamp: 2,
WebkitBoxOrient: "vertical",
overflow: "hidden",
}}
>
{description}
</span>
{bgValue ? (
<div
style={{
marginTop: 4,
padding: "2px 8px",
borderRadius: 4,
background: bgValue,
color: val,
fontSize: "0.75rem",
fontWeight: 600,
}}
>
Aa Text preview
</div>
) : null}
{poor && bgValue ? (
<button
type="button"
onClick={() => setVal(readableColor("#ffffff", [bgValue]))}
style={{
marginTop: 4,
alignSelf: "flex-start",
fontSize: "0.7rem",
fontWeight: 600,
color: "var(--admin-accent)",
background: "none",
border: "none",
padding: 0,
cursor: "pointer",
}}
>
Use readable color →
</button>
) : null}
</div>
</div>
);
}