This commit is contained in:
1 parent
8efd032cc6
commit
df38dccbf1
735 files changed
+128321
-120870
No files matched your search
+147
-131
@@ -3,153 +3,169 @@
|
||||
import { useState } from "react";
|
||||
|
||||
interface Field {
|
||||
key: string;
|
||||
label: string;
|
||||
def: string;
|
||||
desc: string;
|
||||
bgKey?: string;
|
||||
key: string;
|
||||
label: string;
|
||||
def: string;
|
||||
desc: string;
|
||||
bgKey?: string;
|
||||
}
|
||||
|
||||
function hexToRgb(hex: string): readonly [number, number, number] | null {
|
||||
const m = /^#([0-9a-f]{6})$/i.exec(hex.trim());
|
||||
if (!m) return null;
|
||||
const n = Number.parseInt(m[1], 16);
|
||||
return [(n >> 16) & 255, (n >> 8) & 255, n & 255];
|
||||
const m = /^#([0-9a-f]{6})$/i.exec(hex.trim());
|
||||
if (!m) return null;
|
||||
const n = Number.parseInt(m[1], 16);
|
||||
return [(n >> 16) & 255, (n >> 8) & 255, n & 255];
|
||||
}
|
||||
|
||||
function luminance([r, g, b]: readonly [number, number, number]): number {
|
||||
const a = [r, g, b].map((v) => {
|
||||
const c = v / 255;
|
||||
return c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
|
||||
});
|
||||
return 0.2126 * a[0] + 0.7152 * a[1] + 0.0722 * a[2];
|
||||
const a = [r, g, b].map((v) => {
|
||||
const c = v / 255;
|
||||
return c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
|
||||
});
|
||||
return 0.2126 * a[0] + 0.7152 * a[1] + 0.0722 * a[2];
|
||||
}
|
||||
|
||||
function contrastRatio(a: string, b: string): number | null {
|
||||
const ra = hexToRgb(a);
|
||||
const rb = hexToRgb(b);
|
||||
if (!ra || !rb) return null;
|
||||
const l1 = luminance(ra);
|
||||
const l2 = luminance(rb);
|
||||
const hi = Math.max(l1, l2);
|
||||
const lo = Math.min(l1, l2);
|
||||
return (hi + 0.05) / (lo + 0.05);
|
||||
const ra = hexToRgb(a);
|
||||
const rb = hexToRgb(b);
|
||||
if (!ra || !rb) return null;
|
||||
const l1 = luminance(ra);
|
||||
const l2 = luminance(rb);
|
||||
const hi = Math.max(l1, l2);
|
||||
const lo = Math.min(l1, l2);
|
||||
return (hi + 0.05) / (lo + 0.05);
|
||||
}
|
||||
|
||||
function readableOn(bg: string): string {
|
||||
const black = contrastRatio("#000000", bg) ?? 0;
|
||||
const white = contrastRatio("#ffffff", bg) ?? 0;
|
||||
return white >= black ? "#ffffff" : "#000000";
|
||||
const black = contrastRatio("#000000", bg) ?? 0;
|
||||
const white = contrastRatio("#ffffff", bg) ?? 0;
|
||||
return white >= black ? "#ffffff" : "#000000";
|
||||
}
|
||||
|
||||
export function ColorField({
|
||||
field,
|
||||
value,
|
||||
bgValue,
|
||||
name,
|
||||
field,
|
||||
value,
|
||||
bgValue,
|
||||
name,
|
||||
}: {
|
||||
field: Field;
|
||||
value: string;
|
||||
bgValue?: string;
|
||||
name: string;
|
||||
field: Field;
|
||||
value: string;
|
||||
bgValue?: string;
|
||||
name: string;
|
||||
}) {
|
||||
const [val, setVal] = useState(value);
|
||||
const ratio = bgValue ? contrastRatio(val, bgValue) : null;
|
||||
const poor = ratio != null && ratio < 4.5;
|
||||
const ok = ratio != null && ratio >= 4.5;
|
||||
const [val, setVal] = useState(value);
|
||||
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) => setVal(e.target.value)}
|
||||
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 }}>{field.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",
|
||||
}}
|
||||
>
|
||||
{field.desc}
|
||||
</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(readableOn(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>
|
||||
);
|
||||
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) => setVal(e.target.value)}
|
||||
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 }}
|
||||
>
|
||||
{field.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",
|
||||
}}
|
||||
>
|
||||
{field.desc}
|
||||
</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(readableOn(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>
|
||||
);
|
||||
}
|
||||
+616
-478
File diff suppressed because it is too large.
Load diff
Reference in new issue
Block a user