feat(stats): add world-map theme awareness and highlight color

- Add --highlight-rgb CSS custom property for light and dark themes
- Add MutationObserver to world-map for theme change detection
- Read choropleth colors from CSS custom properties instead of hardcoded values
- Simplify stats_map partial to use text-highlight class
This commit is contained in:
Jon Seager 2026-02-08 15:20:44 +00:00
parent 66c5cd0d6c
commit f144526288
No known key found for this signature in database
3 changed files with 29 additions and 10 deletions

View file

@ -30,6 +30,9 @@
--text: #1c1917; /* stone-900 */
--text-secondary: #57534e; /* stone-600 */
--text-muted: #78716c; /* stone-500 */
/* Data-viz highlight (RGB triplet for alpha usage in JS) */
--highlight-rgb: 194, 65, 12; /* orange-700, matches --accent */
}
[data-theme="dark"] {
@ -50,6 +53,8 @@
--text: #e7e5e4;
--text-secondary: #d6d3d1;
--text-muted: #a8a29e;
--highlight-rgb: 234, 88, 12; /* orange-600, matches --accent */
}
/* ── Theme: map raw vars to Tailwind utilities ─────────────────── */
@ -66,6 +71,7 @@
--color-text-secondary: var(--text-secondary);
--color-text-muted: var(--text-muted);
--color-border: var(--border);
--color-highlight: rgb(var(--highlight-rgb));
}
/* ── Base: default border color ────────────────────────────────── */

View file

@ -6,7 +6,16 @@ const WORLD_SVG = `
customElements.define("world-map", class extends HTMLElement {
static get observedAttributes() { return ["data-countries", "data-max", "data-selected"]; }
connectedCallback() { this._scheduleRender(); }
connectedCallback() {
this._scheduleRender();
this._themeObserver = new MutationObserver(() => requestAnimationFrame(() => this._recolor()));
this._themeObserver.observe(document.documentElement, { attributes: true, attributeFilter: ['data-theme'] });
}
disconnectedCallback() {
this._themeObserver?.disconnect();
this._themeObserver = null;
}
attributeChangedCallback(name) {
if (!this.isConnected) return;
@ -64,23 +73,29 @@ customElements.define("world-map", class extends HTMLElement {
const counts = this._counts;
const max = this._max;
const styles = getComputedStyle(document.documentElement);
const rgb = styles.getPropertyValue('--highlight-rgb').trim() || '185, 28, 28';
const borderColor = styles.getPropertyValue('--text-muted').trim() || '#9ca3af';
const surfaceAlt = styles.getPropertyValue('--surface-alt').trim() || '#f5f5f4';
const borderMuted = styles.getPropertyValue('--border').trim() || '#d6d3d1';
const applyStyle = (el, fill) => {
el.style.fill = fill;
el.style.stroke = "#9ca3af";
el.style.stroke = borderColor;
el.style.strokeWidth = "0.3";
};
const colorFor = (code) => {
const count = counts.get(code);
if (selected) {
if (code === selected && count) return "rgba(185, 28, 28, 1)";
return count ? "#d6d3d1" : "#f5f5f4";
if (code === selected && count) return `rgba(${rgb}, 1)`;
return count ? borderMuted : surfaceAlt;
}
if (count) {
const alpha = (0.15 + 0.85 * (count / max)).toFixed(2);
return `rgba(220, 38, 38, ${alpha})`;
return `rgba(${rgb}, ${alpha})`;
}
return "#f5f5f4";
return surfaceAlt;
};
svg.querySelectorAll("path[id], g[id]").forEach((el) => {

View file

@ -31,9 +31,9 @@
{% if !entry.flag_emoji.is_empty() %}
<span>{{ entry.flag_emoji }}</span>
{% endif %}
<span class="text-text whitespace-nowrap">{{ entry.country_name }}</span>
<span class="font-semibold text-highlight whitespace-nowrap">{{ entry.country_name }}</span>
</span>
<span class="border-l py-1.5 pl-2 pr-3 font-medium text-text">{{ entry.count }}</span>
<span class="border-l py-1.5 pl-2 pr-3 font-semibold text-highlight">{{ entry.count }}</span>
</button>
{% else %}
<div class="inline-flex shrink-0 snap-start items-center rounded-full border text-sm">
@ -52,7 +52,5 @@
{{ icons::chevron_right("h-4 w-4") }}
</button>
</chip-scroll>
<p class="text-xs text-text-muted">{{ geo_stats.total_countries }} {% if geo_stats.total_countries == 1 %}country{% else %}countries{% endif %}</p>
{% endif %}
</div>