feat(leaflet-markers): add driverLetterPin + unassignedPoolPin

This commit is contained in:
SJCoetz 2026-07-11 23:41:22 +02:00
parent d3c68d0703
commit a26e7c6312
2 changed files with 110 additions and 0 deletions

View file

@ -0,0 +1,65 @@
import L from "leaflet";
import { describe, expect, it } from "vitest";
import { driverLetterPin, unassignedPoolPin } from "./leaflet-markers";
describe("driverLetterPin", () => {
it("returns an L.DivIcon sized 28x28", () => {
const icon = driverLetterPin("A", 3);
expect(icon).toBeInstanceOf(L.DivIcon);
expect(icon.options.iconSize).toEqual([28, 28]);
expect(icon.options.iconAnchor).toEqual([14, 14]);
});
it("renders the letter inside the HTML", () => {
const icon = driverLetterPin("B", 5);
const html = icon.options.html as string;
expect(html).toContain("B");
expect(html).toContain("5");
});
it("uses a custom color when provided", () => {
const icon = driverLetterPin("A", 2, "#336699");
const html = icon.options.html as string;
expect(html).toContain("#336699");
});
it("falls back to the default blue when no color is given", () => {
const icon = driverLetterPin("A", 2);
const html = icon.options.html as string;
// Default blue from DRIVER_PALETTE[1] (#1B76BB) — assert presence
// loosely via the substring "1B76BB" so a future palette tweak
// doesn't immediately break the test.
expect(html.toLowerCase()).toContain("1b76bb");
});
});
describe("unassignedPoolPin", () => {
it("returns an L.DivIcon sized 28x28", () => {
const icon = unassignedPoolPin("ready", 3);
expect(icon).toBeInstanceOf(L.DivIcon);
expect(icon.options.iconSize).toEqual([28, 28]);
expect(icon.options.iconAnchor).toEqual([14, 14]);
});
it("encodes ✓ when glyph is 'ready'", () => {
const icon = unassignedPoolPin("ready", 3);
const html = icon.options.html as string;
expect(html).toContain("✓");
expect(html).not.toContain("✕");
expect(html).toContain("3");
});
it("encodes ✕ when glyph is 'unready'", () => {
const icon = unassignedPoolPin("unready", 2);
const html = icon.options.html as string;
expect(html).toContain("✕");
expect(html).not.toContain("✓");
expect(html).toContain("2");
});
it("renders the amber fill colour", () => {
const icon = unassignedPoolPin("ready", 1);
const html = icon.options.html as string;
expect(html.toLowerCase()).toContain("f59e0b");
});
});

View file

@ -94,4 +94,49 @@ export function clusterBubble(color: string, count: number) {
iconSize: [size, size],
iconAnchor: [size / 2, size / 2],
});
}
/**
* Blue circle with the driver letter (A-Z, then 1-9) inside. Used by
* the new collection-map "one pin per driver per supplier" model. The
* count renders as a small round superscript in the top-right corner.
*
* Default colour is `DRIVER_PALETTE[1]` (#1B76BB) — the existing deep
* blue from src/lib/geo.ts — so the helper stays consistent with the
* rest of the app's driver signal even though the page no longer uses
* the palette for differentiating drivers (the letter does that now).
*/
export function driverLetterPin(
letter: string,
count: number,
color: string = "#1B76BB"
) {
const size = 28;
return L.divIcon({
className: "",
html: `<div style="position:relative;width:${size}px;height:${size}px;"><div style="position:absolute;inset:0;display:grid;place-items:center;background:${color};color:white;border:2.5px solid white;border-radius:999px;box-shadow:0 6px 14px rgba(15,23,42,.28);font-size:13px;font-weight:800;letter-spacing:0.3px;text-shadow:0 1px 2px rgba(0,0,0,.45);">${letter}</div><div style="position:absolute;top:-4px;right:-4px;min-width:16px;height:16px;padding:0 4px;display:grid;place-items:center;background:white;color:#1F2937;border-radius:999px;border:1.5px solid ${color};font-size:10px;font-weight:800;box-shadow:0 2px 6px rgba(15,23,42,.22);">${count}</div></div>`,
iconSize: [size, size],
iconAnchor: [size / 2, size / 2],
});
}
/**
* Amber circle with ✓ or ✕ inside, for unassigned buckets on the
* collection map. Same size as the driver letter pin so they read as
* sibling shapes. The count renders as a white-bordered superscript
* in the top-right, mirroring the driver pin.
*/
export function unassignedPoolPin(
glyph: "ready" | "unready",
count: number
) {
const size = 28;
const amber = "#F59E0B";
const symbol = glyph === "ready" ? "✓" : "✕";
return L.divIcon({
className: "",
html: `<div style="position:relative;width:${size}px;height:${size}px;"><div style="position:absolute;inset:0;display:grid;place-items:center;background:${amber};color:white;border:2.5px solid white;border-radius:999px;box-shadow:0 6px 14px rgba(217,119,6,.32);font-size:14px;font-weight:900;line-height:1;text-shadow:0 1px 2px rgba(0,0,0,.45);">${symbol}</div><div style="position:absolute;top:-4px;right:-4px;min-width:16px;height:16px;padding:0 4px;display:grid;place-items:center;background:white;color:#1F2937;border-radius:999px;border:1.5px solid ${amber};font-size:10px;font-weight:800;box-shadow:0 2px 6px rgba(15,23,42,.22);">${count}</div></div>`,
iconSize: [size, size],
iconAnchor: [size / 2, size / 2],
});
}