perfect-postcode/frontend/src/components/map/MapLegend.tsx
2026-03-25 08:05:50 +00:00

154 lines
4.6 KiB
TypeScript

import { formatValue } from '../../lib/format';
import {
FEATURE_GRADIENT,
DENSITY_GRADIENT,
DENSITY_GRADIENT_DARK,
ENUM_PALETTE,
} from '../../lib/consts';
import { gradientToCss } from '../../lib/utils';
import { CloseIcon } from '../ui/icons/CloseIcon';
import { TickerValue } from '../ui/TickerValue';
function EnumSwatches({ values }: { values: string[] }) {
return (
<div className="flex flex-col gap-1">
{values.map((label, i) => {
const color = ENUM_PALETTE[i % ENUM_PALETTE.length];
return (
<div key={label} className="flex items-center gap-1.5">
<div
className="w-3 h-3 rounded-sm shrink-0"
style={{ backgroundColor: `rgb(${color[0]},${color[1]},${color[2]})` }}
/>
<span className="text-warm-600 dark:text-warm-300 truncate">{label}</span>
</div>
);
})}
</div>
);
}
function InlineEnumSwatches({ values }: { values: string[] }) {
return (
<div className="flex items-center gap-2 flex-1 min-w-[40%] flex-wrap">
{values.map((label, i) => {
const color = ENUM_PALETTE[i % ENUM_PALETTE.length];
return (
<div key={label} className="flex items-center gap-1">
<div
className="w-2.5 h-2.5 rounded-sm shrink-0"
style={{ backgroundColor: `rgb(${color[0]},${color[1]},${color[2]})` }}
/>
<span className="text-warm-500 dark:text-warm-400 whitespace-nowrap text-[11px]">
{label}
</span>
</div>
);
})}
</div>
);
}
export default function MapLegend({
featureLabel,
range,
showCancel,
onCancel,
mode,
enumValues,
theme = 'light',
inline = false,
suffix,
raw,
}: {
featureLabel: string;
range: [number, number];
showCancel: boolean;
onCancel: () => void;
mode: 'feature' | 'density';
enumValues?: string[];
theme?: 'light' | 'dark';
inline?: boolean;
suffix?: string;
raw?: boolean;
}) {
const isEnum = enumValues && enumValues.length > 0;
const densityGradient = theme === 'dark' ? DENSITY_GRADIENT_DARK : DENSITY_GRADIENT;
const gradientStyle =
mode === 'density' ? gradientToCss(densityGradient) : gradientToCss(FEATURE_GRADIENT);
const fmt = raw ? { raw: true } : undefined;
const rangeMin =
mode === 'density' ? (
<TickerValue text={formatValue(range[0])} />
) : isEnum ? null : (
<TickerValue text={formatValue(range[0], fmt) + (suffix || '')} />
);
const rangeMax =
mode === 'density' ? (
<TickerValue text={formatValue(range[1])} />
) : isEnum ? null : (
<TickerValue text={formatValue(range[1], fmt) + (suffix || '')} />
);
if (inline) {
return (
<div className="bg-warm-100 dark:bg-warm-800 text-warm-700 dark:text-warm-200 px-3 py-1.5 text-xs border-b border-warm-200 dark:border-warm-700 flex items-center gap-2">
<span className="font-semibold text-xs text-warm-800 dark:text-warm-200 truncate">
{featureLabel}
</span>
{showCancel && (
<button
onClick={onCancel}
className="text-warm-400 hover:text-warm-700 dark:hover:text-warm-300 shrink-0"
title="Clear colour view"
>
<CloseIcon className="w-3.5 h-3.5" />
</button>
)}
{isEnum ? (
<InlineEnumSwatches values={enumValues} />
) : (
<div className="flex items-center gap-1.5 flex-1 min-w-[40%] text-warm-500 dark:text-warm-400">
{rangeMin}
<div
className="h-2.5 rounded flex-1 min-w-[40px]"
style={{ background: gradientStyle }}
/>
{rangeMax}
</div>
)}
</div>
);
}
return (
<div className="bg-white dark:bg-navy-800 dark:text-white rounded shadow-lg p-3 text-xs min-w-[300px] pointer-events-auto">
<div className="flex items-center justify-between mb-2">
<span className="font-semibold text-sm dark:text-white">{featureLabel}</span>
{showCancel && (
<button
onClick={onCancel}
className="text-warm-400 hover:text-warm-700 dark:hover:text-warm-300 ml-2"
title="Clear colour view"
>
<CloseIcon className="w-4 h-4" />
</button>
)}
</div>
{isEnum ? (
<EnumSwatches values={enumValues} />
) : (
<>
<div className="h-3 rounded" style={{ background: gradientStyle }} />
<div className="flex justify-between mt-1 text-warm-600 dark:text-warm-200">
{rangeMin}
{rangeMax}
</div>
</>
)}
</div>
);
}