Add dark mode
This commit is contained in:
parent
5e210e14bd
commit
7235df0a97
14 changed files with 304 additions and 139 deletions
|
|
@ -21,6 +21,7 @@ interface MapProps {
|
|||
selectedHexagonId: string | null;
|
||||
onHexagonClick: (h3: string) => void;
|
||||
initialViewState?: ViewState;
|
||||
theme?: 'light' | 'dark';
|
||||
}
|
||||
|
||||
// Twemoji CDN base URL
|
||||
|
|
@ -42,7 +43,8 @@ const INITIAL_VIEW: ViewState = {
|
|||
pitch: 0,
|
||||
};
|
||||
|
||||
const MAP_STYLE = 'https://basemaps.cartocdn.com/gl/voyager-gl-style/style.json';
|
||||
const MAP_STYLE_LIGHT = 'https://basemaps.cartocdn.com/gl/voyager-gl-style/style.json';
|
||||
const MAP_STYLE_DARK = 'https://basemaps.cartocdn.com/gl/dark-matter-gl-style/style.json';
|
||||
|
||||
// Gradient stops for normalized [0,1] values
|
||||
const GRADIENT: { t: number; color: [number, number, number] }[] = [
|
||||
|
|
@ -204,7 +206,7 @@ function PostcodeSearch({
|
|||
setError(null);
|
||||
}}
|
||||
placeholder="Search postcode..."
|
||||
className="px-3 py-2 text-sm w-40 border-none outline-none bg-white"
|
||||
className="px-3 py-2 text-sm w-40 border-none outline-none bg-white dark:bg-warm-800 dark:text-warm-100 dark:placeholder-warm-500"
|
||||
/>
|
||||
<button
|
||||
type="submit"
|
||||
|
|
@ -215,7 +217,7 @@ function PostcodeSearch({
|
|||
</button>
|
||||
</div>
|
||||
{error && (
|
||||
<span className="text-xs text-red-600 bg-white/90 rounded px-2 py-0.5 shadow">{error}</span>
|
||||
<span className="text-xs text-red-600 dark:text-red-400 bg-white/90 dark:bg-warm-800/90 rounded px-2 py-0.5 shadow">{error}</span>
|
||||
)}
|
||||
</form>
|
||||
);
|
||||
|
|
@ -240,7 +242,7 @@ function MapLegend({
|
|||
};
|
||||
|
||||
return (
|
||||
<div className="absolute top-3 right-3 z-10 bg-white rounded shadow-lg p-3 text-xs min-w-[160px]">
|
||||
<div className="absolute top-3 right-3 z-10 bg-white dark:bg-warm-800 dark:text-warm-200 rounded shadow-lg p-3 text-xs min-w-[160px]">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<span className="font-semibold text-sm">{featureLabel}</span>
|
||||
{showCancel && (
|
||||
|
|
@ -268,7 +270,7 @@ function MapLegend({
|
|||
'linear-gradient(to right, rgb(46, 204, 113), rgb(241, 196, 15), rgb(231, 76, 60), rgb(142, 68, 173))',
|
||||
}}
|
||||
/>
|
||||
<div className="flex justify-between mt-1 text-warm-600">
|
||||
<div className="flex justify-between mt-1 text-warm-600 dark:text-warm-400">
|
||||
<span>{formatVal(range[0])}</span>
|
||||
<span>{formatVal(range[1])}</span>
|
||||
</div>
|
||||
|
|
@ -289,6 +291,7 @@ export default memo(function Map({
|
|||
selectedHexagonId,
|
||||
onHexagonClick,
|
||||
initialViewState,
|
||||
theme = 'light',
|
||||
}: MapProps) {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const [viewState, setViewState] = useState<ViewState>(initialViewState || INITIAL_VIEW);
|
||||
|
|
@ -343,28 +346,39 @@ export default memo(function Map({
|
|||
setViewState((prev) => ({ ...prev, latitude: lat, longitude: lng, zoom }));
|
||||
}, []);
|
||||
|
||||
const themeRef = useRef(theme);
|
||||
themeRef.current = theme;
|
||||
|
||||
// Make place labels more legible over the colored hexagons
|
||||
const handleMapLoad = useCallback(
|
||||
(evt: { target: MapRef['getMap'] extends () => infer M ? M : never }) => {
|
||||
const map = evt.target;
|
||||
for (const layer of map.getStyle().layers || []) {
|
||||
if (layer.type !== 'symbol') continue;
|
||||
map.setPaintProperty(layer.id, 'text-halo-color', 'rgba(255,255,255,1)');
|
||||
map.setPaintProperty(layer.id, 'text-halo-width', 2);
|
||||
map.setPaintProperty(layer.id, 'text-color', '#222');
|
||||
}
|
||||
// Make water more prominent
|
||||
for (const layer of map.getStyle().layers || []) {
|
||||
if (layer.id === 'water' || layer.id.startsWith('water')) {
|
||||
map.setPaintProperty(layer.id, 'fill-color', '#6baed6');
|
||||
if (themeRef.current === 'light') {
|
||||
for (const layer of map.getStyle().layers || []) {
|
||||
if (layer.type !== 'symbol') continue;
|
||||
map.setPaintProperty(layer.id, 'text-halo-color', 'rgba(255,255,255,1)');
|
||||
map.setPaintProperty(layer.id, 'text-halo-width', 2);
|
||||
map.setPaintProperty(layer.id, 'text-color', '#222');
|
||||
}
|
||||
// Make water more prominent
|
||||
for (const layer of map.getStyle().layers || []) {
|
||||
if (layer.id === 'water' || layer.id.startsWith('water')) {
|
||||
map.setPaintProperty(layer.id, 'fill-color', '#6baed6');
|
||||
}
|
||||
}
|
||||
}
|
||||
map.setLayoutProperty('building', 'visibility', 'none');
|
||||
map.setLayoutProperty('building-top', 'visibility', 'none');
|
||||
try {
|
||||
map.setLayoutProperty('building', 'visibility', 'none');
|
||||
map.setLayoutProperty('building-top', 'visibility', 'none');
|
||||
} catch {
|
||||
// layers may not exist in dark style
|
||||
}
|
||||
},
|
||||
[]
|
||||
);
|
||||
|
||||
const mapStyle = theme === 'dark' ? MAP_STYLE_DARK : MAP_STYLE_LIGHT;
|
||||
|
||||
// Popup state for POI hover
|
||||
const [popupInfo, setPopupInfo] = useState<{
|
||||
x: number;
|
||||
|
|
@ -541,20 +555,20 @@ export default memo(function Map({
|
|||
getPosition: (d) => [d.lon as number, d.lat as number],
|
||||
getText: (d) => d.postcode as string,
|
||||
getSize: 11,
|
||||
getColor: [30, 30, 30, 220],
|
||||
getColor: theme === 'dark' ? [220, 220, 220, 220] : [30, 30, 30, 220],
|
||||
getTextAnchor: 'middle',
|
||||
getAlignmentBaseline: 'center',
|
||||
fontFamily: 'Inter, system-ui, sans-serif',
|
||||
fontWeight: 600,
|
||||
outlineWidth: 2,
|
||||
outlineColor: [255, 255, 255, 200],
|
||||
outlineColor: theme === 'dark' ? [30, 30, 30, 200] : [255, 255, 255, 200],
|
||||
billboard: false,
|
||||
sizeUnits: 'pixels',
|
||||
sizeMinPixels: 10,
|
||||
sizeMaxPixels: 14,
|
||||
})
|
||||
: null,
|
||||
[postcodeData, showPostcodes]
|
||||
[postcodeData, showPostcodes, theme]
|
||||
);
|
||||
|
||||
const layers = useMemo(
|
||||
|
|
@ -593,12 +607,14 @@ export default memo(function Map({
|
|||
}
|
||||
}
|
||||
|
||||
const isDark = themeRef.current === 'dark';
|
||||
return {
|
||||
html: `<div style="padding: 8px; font-size: 12px;">${lines.join('')}</div>`,
|
||||
style: {
|
||||
backgroundColor: 'white',
|
||||
backgroundColor: isDark ? '#292524' : 'white',
|
||||
color: isDark ? '#e7e5e4' : 'inherit',
|
||||
borderRadius: '4px',
|
||||
boxShadow: '0 2px 4px rgba(0,0,0,0.2)',
|
||||
boxShadow: isDark ? '0 2px 4px rgba(0,0,0,0.5)' : '0 2px 4px rgba(0,0,0,0.2)',
|
||||
},
|
||||
};
|
||||
},
|
||||
|
|
@ -611,9 +627,14 @@ export default memo(function Map({
|
|||
{...viewState}
|
||||
onMove={handleMove}
|
||||
onLoad={handleMapLoad as never}
|
||||
mapStyle={MAP_STYLE}
|
||||
mapStyle={mapStyle}
|
||||
style={{ width: '100%', height: '100%' }}
|
||||
attributionControl={false}
|
||||
dragRotate={false}
|
||||
touchZoomRotate={true}
|
||||
touchPitch={false}
|
||||
keyboard={true}
|
||||
pitchWithRotate={false}
|
||||
>
|
||||
<DeckOverlay layers={layers} getTooltip={getTooltip as never} />
|
||||
</MapGL>
|
||||
|
|
@ -628,7 +649,7 @@ export default memo(function Map({
|
|||
)}
|
||||
{popupInfo && (
|
||||
<div
|
||||
className="absolute pointer-events-none bg-white rounded shadow-lg p-2 text-sm"
|
||||
className="absolute pointer-events-none bg-white dark:bg-warm-800 rounded shadow-lg p-2 text-sm dark:text-warm-200"
|
||||
style={{
|
||||
left: popupInfo.x,
|
||||
top: popupInfo.y - 40,
|
||||
|
|
@ -637,7 +658,7 @@ export default memo(function Map({
|
|||
}}
|
||||
>
|
||||
<strong>{popupInfo.name}</strong>
|
||||
<div className="text-gray-500 text-xs">{popupInfo.category}</div>
|
||||
<div className="text-gray-500 dark:text-warm-400 text-xs">{popupInfo.category}</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue