Changelog a34a404
Give the map of a federation's states or provinces the world map's hover box
Hovering a state, province or territory now shows the same box as on the home page's map with states shown: flag and name, the score in its tier's colour, the tier and its rank within the country, a bar and the short summary. The boxes are written into the page and shown by a small delegated script, so they also work in the home page's drawer. The summary shortening moves from map-notes.json.js to src/lib/short.js so both can use it. Claude-Session: https://claude.ai/code/session_015PFhJhHBpXsnUocs8vvYAd
6 files of code, design or docs +71 −22 lines
Code, design and documents 6 files
src/components/CountryArticle.astro +19 −4
@@ -5,4 +5,5 @@ import { useAges, SEGMENTS, REC_LABEL, REC_HELP, MED_LABEL, MED_HELP, countries, import { TIERS, tierOf, tierColor, tierIndex } from "../lib/tiers.js"; import { unitMap } from "../lib/unit-map.js"; +import { short } from "../lib/short.js"; import CountryChanges from "./CountryChanges.astro"; import CountryChart from "./CountryChart.astro"; @@ -228,14 +229,25 @@ const culture = cultureOf(c); <svg viewBox={map.viewBox} role="group" aria-label={`Map of ${c.name}: ${group.plural} coloured by score`}> {map.shapes.map(({ unit: x, d }) => ( - <a href={unitUrl(x)} data-subunit class={sub?.id === x.id ? "current" : undefined} aria-label={`${x.name}: ${x.score}`}> - <path d={d} style={`fill:${tierColor(x.raw)}`}><title>{x.name}: {x.score} · {tierOf(x.raw).name}</title></path> + <a href={unitUrl(x)} data-subunit data-tip={x.id} class={sub?.id === x.id ? "current" : undefined} aria-label={`${x.name}: ${x.score}, ${tierOf(x.raw).name}`}> + <path d={d} style={`fill:${tierColor(x.raw)}`} /> </a> ))} {map.dots.map(({ unit: x, x: cx, y: cy }) => ( - <a href={unitUrl(x)} data-subunit class={sub?.id === x.id ? "current" : undefined} aria-hidden="true" tabindex="-1"> - <circle cx={cx} cy={cy} r="5" style={`fill:${tierColor(x.raw)}`}><title>{x.name}: {x.score} · {tierOf(x.raw).name}</title></circle> + <a href={unitUrl(x)} data-subunit data-tip={x.id} class={sub?.id === x.id ? "current" : undefined} aria-hidden="true" tabindex="-1"> + <circle cx={cx} cy={cy} r="5" style={`fill:${tierColor(x.raw)}`} /> </a> ))} </svg> + {/* the hover box of each, as on the home page's map with its states shown (src/lib/unit-map-tip.js) */} + <div class="tooltip" hidden></div> + {group.units.map((x) => ( + <template data-tip-for={x.id}> + <div class="tip-head"><strong><span class="tip-flag">{c.flag}</span>{x.name}</strong><span class="tip-chip" style={`--chip:${tierColor(x.raw)}`}>{x.score}</span></div> + <div class="tip-meta">{tierOf(x.raw).name} · #{x.rank} of {group.units.length} in {c.name}</div> + <div class="tip-bar"><i style={`width:${Math.max(2, Math.min(100, x.raw))}%;background:${tierColor(x.raw)}`}></i></div> + {short(x.note) && <p class="tip-note">{short(x.note)}</p>} + <div class="tip-foot">Click for the full profile</div> + </template> + ))} <figcaption class="unit-legend"> {mapTiers.map((t) => <span><i style={`background:${tierColor(t.min)}`}></i>{t.name}</span>)} @@ -378,2 +390,5 @@ const culture = cultureOf(c); </div> </article> +<script> + import "../lib/unit-map-tip.js"; +</script>
src/components/HomePage.astro +1 −0
@@ -527,4 +527,5 @@ const columnGroups = [["Shown by default", BASE_COLUMNS], ...["Score segments", import territoryShapes from "../data/territories.geo.json"; import { tierOf, tierColor } from "../lib/tiers.js"; + import "../lib/unit-map-tip.js"; // the hover box of a federation's map, in the drawer const base = import.meta.env.BASE_URL;
src/lib/short.js +19 −0
@@ -0,0 +1,19 @@ +// A long text cut to what fits in a hover box: on the home page's map (map-notes.json.js) and on +// a federation's map of its states or provinces (CountryArticle.astro). +const LIMIT = 220; + +// The first sentences of a text, as many as fit in about LIMIT characters; a first sentence that +// is longer than that is cut at a word and ends in an ellipsis. +export function short(text) { + if (!text) return undefined; + const flat = text.replace(/\s+/g, " ").trim(); + // a sentence ends at . ! or ? followed by a space and a capital, digit or quote, so "7.5 g" stays whole + const sentences = flat.split(/(?<=[.!?])\s+(?=[A-Z0-9“"'(])/); + let out = ""; + for (const s of sentences) { + if (out && out.length + 1 + s.length > LIMIT) break; + out = out ? `${out} ${s}` : s; + } + if (out.length <= LIMIT + 40) return out; + return out.slice(0, LIMIT).replace(/\s+\S*$/, "") + "…"; +}
src/lib/unit-map-tip.js +30 −0
@@ -0,0 +1,30 @@ +// The hover box of a federation's map of its states or provinces (CountryArticle.astro): the same +// box as on the home page's map. Each one is written into the page beside the map, in a <template>, +// and shown under the pointer. Listened for on the document, so that it also works in a profile +// fetched into the home page's drawer. +let open = null; +const hide = () => { + if (open) open.hidden = true; + open = null; +}; +document.addEventListener("pointermove", (e) => { + const a = e.pointerType === "touch" ? null : e.target.closest?.(".unit-map a[data-tip]"); + const figure = a?.closest(".unit-map"); + const tip = figure?.querySelector(".tooltip"); + const content = figure?.querySelector(`template[data-tip-for="${a.dataset.tip}"]`); + if (!tip || !content) return hide(); + if (tip.dataset.for !== a.dataset.tip) { + tip.replaceChildren(content.content.cloneNode(true)); + tip.dataset.for = a.dataset.tip; + } + if (open !== tip) hide(); + tip.hidden = false; + open = tip; + // above the pointer where there is room, below it otherwise; never past the map's edges + const box = figure.getBoundingClientRect(); + const x = e.clientX - box.left, y = e.clientY - box.top; + const w = tip.offsetWidth, h = tip.offsetHeight; + tip.style.left = Math.max(8, Math.min(x + 16, box.width - w - 8)) + "px"; + tip.style.top = (y - h - 14 >= 8 ? y - h - 14 : y + 18) + "px"; +}, { passive: true }); +document.addEventListener("pointerout", (e) => { if (!e.relatedTarget) hide(); });
src/pages/map-notes.json.js +1 −17
@@ -6,23 +6,7 @@ // States and provinces are keyed like their map shapes ("us/ca") and carry n and s only. import { places, countries, subunitsOf } from "../lib/countries.js"; +import { short } from "../lib/short.js"; const MAP_CONTEXT = ["planting", "medical", "clubs", "driving", "visitors"]; -const LIMIT = 220; - -// The first sentences of a text, as many as fit in about LIMIT characters; a first sentence that -// is longer than that is cut at a word and ends in an ellipsis. -function short(text) { - if (!text) return undefined; - const flat = text.replace(/\s+/g, " ").trim(); - // a sentence ends at . ! or ? followed by a space and a capital, digit or quote, so "7.5 g" stays whole - const sentences = flat.split(/(?<=[.!?])\s+(?=[A-Z0-9“"'(])/); - let out = ""; - for (const s of sentences) { - if (out && out.length + 1 + s.length > LIMIT) break; - out = out ? `${out} ${s}` : s; - } - if (out.length <= LIMIT + 40) return out; - return out.slice(0, LIMIT).replace(/\s+\S*$/, "") + "…"; -} const notes = {};
src/styles/global.css +1 −1
@@ -508,5 +508,5 @@ a.chip:hover { border-color: var(--muted); color: var(--text); } .u-score { text-align: right; font-weight: 600; font-variant-numeric: tabular-nums; } @media (max-width: 640px) { .units { columns: 1; } } -.unit-map { margin: 8px 0 14px; } +.unit-map { position: relative; margin: 8px 0 14px; } .unit-map svg { display: block; width: 100%; height: auto; max-height: 420px; } .unit-map path, .unit-map circle { stroke: var(--bg); stroke-width: 0.8; stroke-linejoin: round; transition: opacity 0.15s; }