From a26e7c6312eafcefc5439a2799cc90b1806d2c34 Mon Sep 17 00:00:00 2001 From: SJCoetz Date: Sat, 11 Jul 2026 23:41:22 +0200 Subject: [PATCH] feat(leaflet-markers): add driverLetterPin + unassignedPoolPin --- src/components/leaflet-markers.test.ts | 65 ++++++++++++++++++++++++++ src/components/leaflet-markers.ts | 45 ++++++++++++++++++ 2 files changed, 110 insertions(+) create mode 100644 src/components/leaflet-markers.test.ts diff --git a/src/components/leaflet-markers.test.ts b/src/components/leaflet-markers.test.ts new file mode 100644 index 0000000..ab3ab22 --- /dev/null +++ b/src/components/leaflet-markers.test.ts @@ -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"); + }); +}); \ No newline at end of file diff --git a/src/components/leaflet-markers.ts b/src/components/leaflet-markers.ts index 1c7a227..62d89e6 100644 --- a/src/components/leaflet-markers.ts +++ b/src/components/leaflet-markers.ts @@ -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: `
${letter}
${count}
`, + 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: `
${symbol}
${count}
`, + iconSize: [size, size], + iconAnchor: [size / 2, size / 2], + }); } \ No newline at end of file