From e79d72161528ebaac6f5ca8207509488d3d8e198 Mon Sep 17 00:00:00 2001 From: richarjiang Date: Fri, 13 Mar 2026 14:46:23 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E7=94=A8=20react-map-gl=20+=20MapLibre?= =?UTF-8?q?=20=E6=9B=BF=E6=8D=A2=20react-simple-maps=20=E5=AE=9E=E7=8E=B0?= =?UTF-8?q?=202D=20=E5=9C=B0=E5=9B=BE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 替换 react-simple-maps/d3-geo/topojson-client 为 react-map-gl + maplibre-gl - 使用 CARTO dark-matter 免费暗色瓦片,自带国家/城市名标注 - 基于 locale 动态切换地图标注语言(name:zh / name_en) - MapLibre 原生 heatmap + circle 双层渲染替代 SVG 热力图 - 提取 MapPopup 组件,配合 react-map-gl Popup 实现点击弹窗 - continent page 改为 dynamic import (ssr: false) - dev 模式去掉 Turbopack 以兼容 maplibre-gl - 删除 heatmap-layer.tsx 和 react-simple-maps 类型声明 --- app/[locale]/continent/[slug]/page.tsx | 10 +- app/[locale]/layout.tsx | 16 + app/[locale]/page.tsx | 1 + app/api/v1/claws/route.ts | 4 +- app/api/v1/heatmap/route.ts | 58 +- app/api/v1/register/route.ts | 2 +- app/globals.css | 18 + components/dashboard/claw-feed.tsx | 66 ++- components/globe/globe-view.tsx | 87 ++- components/layout/install-banner.tsx | 138 +++-- components/map/continent-map.tsx | 278 +++++---- components/map/heatmap-layer.tsx | 66 --- components/map/map-popup.tsx | 60 ++ hooks/use-heatmap-data.ts | 8 + hooks/use-sse.ts | 2 +- messages/en.json | 45 +- messages/zh.json | 45 +- package.json | 9 +- pnpm-lock.yaml | 545 +++++++++++++----- skill/openclaw-reporter/SKILL.md | 154 ++++- skill/openclaw-reporter/scripts/heartbeat.sh | 26 + .../openclaw-reporter/scripts/task-report.sh | 14 + types/react-simple-maps.d.ts | 57 -- 23 files changed, 1215 insertions(+), 494 deletions(-) delete mode 100644 components/map/heatmap-layer.tsx create mode 100644 components/map/map-popup.tsx create mode 100755 skill/openclaw-reporter/scripts/heartbeat.sh create mode 100755 skill/openclaw-reporter/scripts/task-report.sh delete mode 100644 types/react-simple-maps.d.ts diff --git a/app/[locale]/continent/[slug]/page.tsx b/app/[locale]/continent/[slug]/page.tsx index 8fc13dd..0ccefd8 100644 --- a/app/[locale]/continent/[slug]/page.tsx +++ b/app/[locale]/continent/[slug]/page.tsx @@ -1,14 +1,22 @@ "use client"; import { use } from "react"; +import dynamic from "next/dynamic"; import { useTranslations } from "next-intl"; import { Navbar } from "@/components/layout/navbar"; import { ParticleBg } from "@/components/layout/particle-bg"; import { ViewSwitcher } from "@/components/layout/view-switcher"; -import { ContinentMap } from "@/components/map/continent-map"; import { StatsPanel } from "@/components/dashboard/stats-panel"; import { ClawFeed } from "@/components/dashboard/claw-feed"; +const ContinentMap = dynamic( + () => + import("@/components/map/continent-map").then((m) => ({ + default: m.ContinentMap, + })), + { ssr: false } +); + const continentSlugs = ["asia", "europe", "americas", "africa", "oceania"] as const; interface PageProps { diff --git a/app/[locale]/layout.tsx b/app/[locale]/layout.tsx index 905a675..05297e2 100644 --- a/app/[locale]/layout.tsx +++ b/app/[locale]/layout.tsx @@ -37,6 +37,22 @@ export async function generateMetadata({ routing.locales.map((l) => [l, `/${l}`]) ), }, + other: { + "application/ld+json": JSON.stringify({ + "@context": "https://schema.org", + "@type": "SoftwareApplication", + name: "openclaw-reporter", + applicationCategory: "DeveloperApplication", + operatingSystem: "macOS, Linux, Windows", + description: + "A Claude Code skill that lets you join the OpenClaw global heatmap. Sends anonymous heartbeats (platform + model only) and generic task summaries. Install via: clawhub install openclaw-reporter", + url: "https://kymr.top/", + installUrl: "https://clawhub.com/skills/openclaw-reporter", + offers: { "@type": "Offer", price: "0", priceCurrency: "USD" }, + permissions: + "Network access to https://kymr.top/, write to ~/.openclaw/, binaries: curl, python3, uname", + }), + }, }; } diff --git a/app/[locale]/page.tsx b/app/[locale]/page.tsx index c0b182e..329b417 100644 --- a/app/[locale]/page.tsx +++ b/app/[locale]/page.tsx @@ -39,6 +39,7 @@ export default function HomePage() { + ); diff --git a/app/api/v1/claws/route.ts b/app/api/v1/claws/route.ts index d21fa56..6f6dd51 100644 --- a/app/api/v1/claws/route.ts +++ b/app/api/v1/claws/route.ts @@ -10,6 +10,7 @@ export async function GET(req: NextRequest) { const limitParam = parseInt(searchParams.get("limit") ?? "50", 10); const limit = Math.min(Math.max(1, limitParam), 200); const region = searchParams.get("region"); + const sort = searchParams.get("sort") ?? "activity"; const conditions = []; if (region) { @@ -22,7 +23,7 @@ export async function GET(req: NextRequest) { .select() .from(claws) .where(whereClause) - .orderBy(desc(claws.lastHeartbeat)) + .orderBy(sort === "newest" ? desc(claws.createdAt) : desc(claws.lastHeartbeat)) .limit(limit); const totalResult = await db @@ -57,6 +58,7 @@ export async function GET(req: NextRequest) { city: claw.city, country: claw.country, isOnline: activeSet.has(claw.id), + createdAt: claw.createdAt, lastTask, }; }) diff --git a/app/api/v1/heatmap/route.ts b/app/api/v1/heatmap/route.ts index 47c7bbc..4cbd23b 100644 --- a/app/api/v1/heatmap/route.ts +++ b/app/api/v1/heatmap/route.ts @@ -1,5 +1,5 @@ import { NextResponse } from "next/server"; -import { gte, and, isNotNull, sql } from "drizzle-orm"; +import { and, isNotNull, sql } from "drizzle-orm"; import { db } from "@/lib/db"; import { claws } from "@/lib/db/schema"; import { getCacheHeatmap, setCacheHeatmap } from "@/lib/redis"; @@ -13,18 +13,19 @@ export async function GET() { const fiveMinutesAgo = new Date(Date.now() - 5 * 60 * 1000); - const activeClaws = await db + // Query all claws with coordinates, grouped by location + const locationRows = await db .select({ city: claws.city, country: claws.country, latitude: claws.latitude, longitude: claws.longitude, count: sql`count(*)`, + onlineCount: sql`sum(case when ${claws.lastHeartbeat} >= ${fiveMinutesAgo} then 1 else 0 end)`, }) .from(claws) .where( and( - gte(claws.lastHeartbeat, fiveMinutesAgo), isNotNull(claws.latitude), isNotNull(claws.longitude) ) @@ -36,14 +37,49 @@ export async function GET() { claws.longitude ); - const points = activeClaws.map((row) => ({ - lat: Number(row.latitude), - lng: Number(row.longitude), - weight: row.count, - clawCount: row.count, - city: row.city, - country: row.country, - })); + // Fetch individual claw details for all claws with coordinates + const clawDetails = await db + .select({ + id: claws.id, + name: claws.name, + city: claws.city, + country: claws.country, + latitude: claws.latitude, + longitude: claws.longitude, + lastHeartbeat: claws.lastHeartbeat, + }) + .from(claws) + .where( + and( + isNotNull(claws.latitude), + isNotNull(claws.longitude) + ) + ); + + // Group claw details by location key + const clawsByLocation = new Map>(); + for (const claw of clawDetails) { + const key = `${claw.latitude}:${claw.longitude}`; + const isOnline = claw.lastHeartbeat ? claw.lastHeartbeat >= fiveMinutesAgo : false; + const list = clawsByLocation.get(key) ?? []; + list.push({ id: claw.id, name: claw.name, isOnline }); + clawsByLocation.set(key, list); + } + + const points = locationRows.map((row) => { + const key = `${row.latitude}:${row.longitude}`; + const clawList = (clawsByLocation.get(key) ?? []).slice(0, 20); + return { + lat: Number(row.latitude), + lng: Number(row.longitude), + weight: row.count, + clawCount: row.count, + onlineCount: Number(row.onlineCount ?? 0), + city: row.city, + country: row.country, + claws: clawList, + }; + }); const lastUpdated = new Date().toISOString(); const responseData = { points, lastUpdated }; diff --git a/app/api/v1/register/route.ts b/app/api/v1/register/route.ts index 081391c..e9ddbce 100644 --- a/app/api/v1/register/route.ts +++ b/app/api/v1/register/route.ts @@ -67,7 +67,7 @@ export async function POST(req: NextRequest) { } await publishEvent({ - type: "online", + type: "registered", clawId, clawName: name, city: geo?.city ?? null, diff --git a/app/globals.css b/app/globals.css index 2ee05fd..b2de9f5 100644 --- a/app/globals.css +++ b/app/globals.css @@ -153,3 +153,21 @@ body { *::-webkit-scrollbar-thumb:hover { background-color: rgba(0, 240, 255, 0.4); } + +/* === MapLibre Dark Popup === */ +.maplibre-dark-popup .maplibregl-popup-content { + background: var(--bg-card); + color: var(--text-primary); + border: 1px solid rgba(0, 240, 255, 0.15); + border-radius: 0.75rem; + box-shadow: 0 0 20px rgba(0, 240, 255, 0.1); + padding: 0.75rem; +} + +.maplibre-dark-popup .maplibregl-popup-tip { + border-top-color: var(--bg-card); +} + +.maplibre-dark-popup .maplibregl-popup-close-button { + display: none; +} diff --git a/components/dashboard/claw-feed.tsx b/components/dashboard/claw-feed.tsx index 406c096..b232f33 100644 --- a/components/dashboard/claw-feed.tsx +++ b/components/dashboard/claw-feed.tsx @@ -9,7 +9,7 @@ import { useSSE } from "@/hooks/use-sse"; interface FeedItem { id: string; - type: "task" | "online" | "offline"; + type: "task" | "online" | "offline" | "registered"; clawName: string; city?: string; country?: string; @@ -24,7 +24,7 @@ export function ClawFeed() { const [items, setItems] = useState([]); const handleEvent = useCallback((event: { type: string; data: Record }) => { - if (event.type === "task" || event.type === "online" || event.type === "offline") { + if (event.type === "task" || event.type === "online" || event.type === "offline" || event.type === "registered") { const newItem: FeedItem = { id: `${Date.now()}-${Math.random().toString(36).slice(2)}`, type: event.type as FeedItem["type"], @@ -45,19 +45,25 @@ export function ClawFeed() { onEvent: handleEvent, }); - // Load initial recent tasks + // Load initial recent tasks + newest registrations useEffect(() => { const fetchRecent = async () => { try { - const res = await fetch("/api/v1/claws?limit=10"); - if (res.ok) { - const data = await res.json(); - const feedItems: FeedItem[] = (data.claws ?? []) + const [taskRes, newestRes] = await Promise.all([ + fetch("/api/v1/claws?limit=10"), + fetch("/api/v1/claws?sort=newest&limit=5"), + ]); + + const feedItems: FeedItem[] = []; + + if (taskRes.ok) { + const data = await taskRes.json(); + const taskItems: FeedItem[] = (data.claws ?? []) .filter((l: Record) => l.lastTask) .map((l: Record) => { const task = l.lastTask as Record; return { - id: `init-${l.id}`, + id: `init-task-${l.id}`, type: "task" as const, clawName: l.name as string, city: l.city as string, @@ -67,8 +73,35 @@ export function ClawFeed() { timestamp: new Date(task.timestamp as string).getTime(), }; }); - setItems(feedItems); + feedItems.push(...taskItems); } + + if (newestRes.ok) { + const data = await newestRes.json(); + const regItems: FeedItem[] = (data.claws ?? []) + .filter((l: Record) => l.createdAt) + .map((l: Record) => ({ + id: `init-reg-${l.id}`, + type: "registered" as const, + clawName: l.name as string, + city: l.city as string, + country: l.country as string, + timestamp: new Date(l.createdAt as string).getTime(), + })); + feedItems.push(...regItems); + } + + // Deduplicate by clawName+type, sort by timestamp desc + const seen = new Set(); + const unique = feedItems.filter((item) => { + const key = `${item.clawName}-${item.type}`; + if (seen.has(key)) return false; + seen.add(key); + return true; + }); + unique.sort((a, b) => b.timestamp - a.timestamp); + + setItems(unique); } catch { // will populate via SSE } @@ -90,11 +123,20 @@ export function ClawFeed() { return "🟢"; case "offline": return "⭕"; + case "registered": + return "🦞"; default: return "🦞"; } }; + const getDescription = (item: FeedItem) => { + if (item.type === "registered" && item.city && item.country) { + return t("joinedFrom", { city: item.city, country: item.country }); + } + return item.summary; + }; + return ( @@ -123,15 +165,15 @@ export function ClawFeed() { {item.clawName} - {item.city && ( + {item.city && item.type !== "registered" && ( {item.city}, {item.country} )} - {item.summary && ( + {getDescription(item) && (

- {item.summary} + {getDescription(item)}

)}
diff --git a/components/globe/globe-view.tsx b/components/globe/globe-view.tsx index d41d980..716af10 100644 --- a/components/globe/globe-view.tsx +++ b/components/globe/globe-view.tsx @@ -33,21 +33,24 @@ interface ArcData { export function GlobeView() { const t = useTranslations("globe"); + const tPopup = useTranslations("clawPopup"); // eslint-disable-next-line @typescript-eslint/no-explicit-any const globeRef = useRef(undefined); const containerRef = useRef(null); const [dimensions, setDimensions] = useState({ width: 0, height: 0 }); const [hoveredPoint, setHoveredPoint] = useState(null); + const [selectedPoint, setSelectedPoint] = useState(null); const { points } = useHeatmapData(30000); - // Generate arcs from recent activity (connecting pairs of active points) + // Generate arcs only between online points const arcs = useMemo((): ArcData[] => { - if (points.length < 2) return []; + const onlinePoints = points.filter((p) => p.onlineCount > 0); + if (onlinePoints.length < 2) return []; const result: ArcData[] = []; - const maxArcs = Math.min(points.length - 1, 8); + const maxArcs = Math.min(onlinePoints.length - 1, 8); for (let i = 0; i < maxArcs; i++) { - const from = points[i]; - const to = points[(i + 1) % points.length]; + const from = onlinePoints[i]; + const to = onlinePoints[(i + 1) % onlinePoints.length]; result.push({ startLat: from.lat, startLng: from.lng, @@ -99,6 +102,10 @@ export function GlobeView() { } }, []); + const handlePointClick = useCallback((point: object) => { + setSelectedPoint(point as HeatmapPoint); + }, []); + return (
{dimensions.width > 0 && ( @@ -114,11 +121,23 @@ export function GlobeView() { pointsData={points} pointLat={(d: object) => (d as HeatmapPoint).lat} pointLng={(d: object) => (d as HeatmapPoint).lng} - pointAltitude={(d: object) => Math.min((d as HeatmapPoint).weight * 0.02, 0.15)} - pointRadius={(d: object) => Math.max((d as HeatmapPoint).weight * 0.3, 0.4)} - pointColor={() => "#00f0ff"} + pointAltitude={(d: object) => { + const p = d as HeatmapPoint; + const base = Math.min(p.weight * 0.02, 0.15); + return p.onlineCount > 0 ? base : base * 0.5; + }} + pointRadius={(d: object) => { + const p = d as HeatmapPoint; + const base = Math.max(p.weight * 0.3, 0.4); + return p.onlineCount > 0 ? base : base * 0.6; + }} + pointColor={(d: object) => { + const p = d as HeatmapPoint; + return p.onlineCount > 0 ? "#00f0ff" : "#1a5c63"; + }} pointsMerge={false} onPointHover={(point: object | null) => setHoveredPoint(point as HeatmapPoint | null)} + onPointClick={handlePointClick} // Arcs arcsData={arcs} arcStartLat={(d: object) => (d as ArcData).startLat} @@ -136,8 +155,8 @@ export function GlobeView() { /> )} - {/* Tooltip */} - {hoveredPoint && ( + {/* Hover Tooltip */} + {hoveredPoint && !selectedPoint && (
@@ -150,12 +169,58 @@ export function GlobeView() {

- {t("activeClaws", { count: hoveredPoint.clawCount })} + {t("totalClaws", { total: hoveredPoint.clawCount, online: hoveredPoint.onlineCount })}

)} + {/* Click Popup */} + {selectedPoint && ( +
+
+
+
+

+ {selectedPoint.city} +

+

{selectedPoint.country}

+
+ +
+
+ + {tPopup("total", { count: selectedPoint.clawCount })} + + + {tPopup("online", { count: selectedPoint.onlineCount })} + +
+ {selectedPoint.claws.length > 0 && ( +
+ {selectedPoint.claws.map((claw) => ( +
+ + + {claw.name} + +
+ ))} +
+ )} +
+
+ )} + { try { - await navigator.clipboard.writeText(INSTALL_COMMAND); + await navigator.clipboard.writeText(prompt); setCopied(true); setTimeout(() => setCopied(false), 2000); } catch { @@ -21,45 +22,112 @@ export function InstallBanner() { }; return ( -
-
- {/* Left: description */} -
-
+
+ {/* Header */} +
+
+
+ 🦞 +
+
+

+ {t("title")} +

+

+ {t("subtitle")} +

+
+
+
-
-

- {t("title")} -

-

- {t("subtitle")} -

-
-
- - {/* Right: copy command */} - +
+ + {/* Human prompt card */} + + + {/* AI-crawlable installation guide (collapsed for humans, fully in DOM for crawlers) */} +
+ + + {tGuide("summaryLabel")} + + +
+
+

{tGuide("whatTitle")}

+

{tGuide("whatDescription")}

+
+ +
+

{tGuide("installTitle")}

+
    +
  1. {tGuide("step1")}
  2. +
  3. {tGuide("step2")}
  4. +
  5. {tGuide("step3")}
  6. +
  7. {tGuide("step4")}
  8. +
+
+ +
+

{tGuide("commandTitle")}

+ + clawhub install openclaw-reporter + +
+ +
+

{tGuide("dataTitle")}

+
    +
  • {tGuide("data1")}
  • +
  • {tGuide("data2")}
  • +
  • {tGuide("data3")}
  • +
  • {tGuide("data4")}
  • +
+

{tGuide("dataNever")}

+
+ +
+

{tGuide("permissionsTitle")}

+
    +
  • {tGuide("perm1")}
  • +
  • {tGuide("perm2")}
  • +
  • {tGuide("perm3")}
  • +
+
+
+
-
+ ); } diff --git a/components/map/continent-map.tsx b/components/map/continent-map.tsx index e3bf484..06434e5 100644 --- a/components/map/continent-map.tsx +++ b/components/map/continent-map.tsx @@ -1,40 +1,89 @@ "use client"; -import { useState, useMemo } from "react"; -import { useTranslations } from "next-intl"; -import { - ComposableMap, - Geographies, - Geography, - ZoomableGroup, -} from "react-simple-maps"; +import { useState, useMemo, useCallback } from "react"; +import { useLocale } from "next-intl"; +import Map, { Source, Layer, Popup } from "react-map-gl/maplibre"; +import type { MapLayerMouseEvent, LngLatLike, MapRef } from "react-map-gl/maplibre"; +import type { LayerSpecification } from "maplibre-gl"; +import "maplibre-gl/dist/maplibre-gl.css"; import { useHeatmapData, type HeatmapPoint } from "@/hooks/use-heatmap-data"; -import { HeatmapLayer } from "./heatmap-layer"; -import { geoMercator } from "d3-geo"; -import { Card, CardContent } from "@/components/ui/card"; -import { Badge } from "@/components/ui/badge"; +import { MapPopup } from "./map-popup"; -const GEO_URL = "https://cdn.jsdelivr.net/npm/world-atlas@2/countries-110m.json"; +const CARTO_STYLE = + "https://basemaps.cartocdn.com/gl/dark-matter-gl-style/style.json"; -interface ContinentConfig { - center: [number, number]; +interface ContinentViewport { + longitude: number; + latitude: number; zoom: number; } -const continentConfigs: Record = { - asia: { center: [100, 35], zoom: 2.5 }, - europe: { center: [15, 52], zoom: 4 }, - americas: { center: [-80, 15], zoom: 1.8 }, - africa: { center: [20, 5], zoom: 2.2 }, - oceania: { center: [145, -25], zoom: 3 }, +const continentConfigs: Record = { + asia: { longitude: 100, latitude: 35, zoom: 2.5 }, + europe: { longitude: 15, latitude: 52, zoom: 3.5 }, + americas: { longitude: -80, latitude: 15, zoom: 2.0 }, + africa: { longitude: 20, latitude: 5, zoom: 2.5 }, + oceania: { longitude: 145, latitude: -25, zoom: 3.0 }, }; -const continentRegionMap: Record = { - asia: "Asia", - europe: "Europe", - americas: "Americas", - africa: "Africa", - oceania: "Oceania", +const heatmapLayer: LayerSpecification = { + id: "claw-heat", + type: "heatmap", + source: "claws", + paint: { + "heatmap-weight": ["get", "weight"], + "heatmap-intensity": 1, + "heatmap-radius": 30, + "heatmap-opacity": 0.6, + "heatmap-color": [ + "interpolate", + ["linear"], + ["heatmap-density"], + 0, + "rgba(0,0,0,0)", + 0.2, + "rgba(0,240,255,0.15)", + 0.4, + "rgba(0,240,255,0.3)", + 0.6, + "rgba(0,200,220,0.5)", + 1, + "rgba(0,240,255,0.8)", + ], + }, +}; + +const circleLayer: LayerSpecification = { + id: "claw-circles", + type: "circle", + source: "claws", + paint: { + "circle-radius": [ + "interpolate", + ["linear"], + ["get", "weight"], + 1, + 5, + 5, + 10, + 10, + 16, + ], + "circle-color": [ + "case", + [">", ["get", "onlineCount"], 0], + "rgba(0, 240, 255, 0.7)", + "rgba(0, 240, 255, 0.2)", + ], + "circle-stroke-color": [ + "case", + [">", ["get", "onlineCount"], 0], + "rgba(0, 240, 255, 0.9)", + "rgba(0, 240, 255, 0.3)", + ], + "circle-stroke-width": 1, + "circle-blur": 0.1, + }, }; interface ContinentMapProps { @@ -42,96 +91,109 @@ interface ContinentMapProps { } export function ContinentMap({ slug }: ContinentMapProps) { - const t = useTranslations("continentMap"); + const locale = useLocale(); const config = continentConfigs[slug] ?? continentConfigs.asia; - const regionFilter = continentRegionMap[slug]; const { points } = useHeatmapData(30000); - const [selectedPoint, setSelectedPoint] = useState(null); + const [popupPoint, setPopupPoint] = useState(null); + const [popupLngLat, setPopupLngLat] = useState(null); - const filteredPoints = useMemo( - () => (regionFilter ? points.filter(() => true) : points), - [points, regionFilter] + // Build a MapLibre expression: coalesce(get("name:zh"), get("name_en"), get("name")) + // For English, just use name_en with name fallback. + const localizedTextField = + locale === "en" + ? ["coalesce", ["get", "name_en"], ["get", "name"]] + : ["coalesce", ["get", `name:${locale}`], ["get", "name_en"], ["get", "name"]]; + + const handleLoad = useCallback( + (e: { target: MapRef["getMap"] extends () => infer M ? M : never }) => { + const map = e.target; + for (const layer of map.getStyle().layers ?? []) { + if (layer.type === "symbol") { + const tf = map.getLayoutProperty(layer.id, "text-field"); + if (tf != null) { + map.setLayoutProperty(layer.id, "text-field", localizedTextField); + } + } + } + }, + [localizedTextField] ); - const projection = useMemo( - () => - geoMercator() - .center(config.center) - .scale(150 * config.zoom) - .translate([400, 300]), - [config] - ); + const geojson = useMemo(() => { + const features = points.map((p) => ({ + type: "Feature" as const, + geometry: { + type: "Point" as const, + coordinates: [p.lng, p.lat], + }, + properties: { + weight: p.weight, + clawCount: p.clawCount, + onlineCount: p.onlineCount, + city: p.city, + country: p.country, + claws: JSON.stringify(p.claws), + }, + })); + return { + type: "FeatureCollection" as const, + features, + }; + }, [points]); - const projectionFn = (coords: [number, number]): [number, number] | null => { - const result = projection(coords); - return result ?? null; - }; + const handleClick = useCallback( + (e: MapLayerMouseEvent) => { + const feature = e.features?.[0]; + if (!feature || feature.geometry.type !== "Point") { + setPopupPoint(null); + return; + } + const props = feature.properties; + const [lng, lat] = feature.geometry.coordinates; + const matched = points.find( + (p) => p.city === props.city && p.country === props.country + ); + if (matched) { + setPopupPoint(matched); + setPopupLngLat([lng, lat]); + } + }, + [points] + ); return ( -
-
- - - - {({ geographies }) => - geographies.map((geo) => ( - - )) - } - - - - -
+
+ + + + + - {selectedPoint && ( - - -
-
-

- {selectedPoint.city} -

-

{selectedPoint.country}

-
- -
-
- {t("claws", { count: selectedPoint.clawCount })} - {t("weight", { value: selectedPoint.weight.toFixed(1) })} -
-
-
- )} + {popupPoint && popupLngLat && ( + setPopupPoint(null)} + closeButton={false} + className="maplibre-dark-popup" + maxWidth="280px" + > + setPopupPoint(null)} + /> + + )} +
); } diff --git a/components/map/heatmap-layer.tsx b/components/map/heatmap-layer.tsx deleted file mode 100644 index 14b8eb4..0000000 --- a/components/map/heatmap-layer.tsx +++ /dev/null @@ -1,66 +0,0 @@ -"use client"; - -import { motion } from "framer-motion"; -import type { HeatmapPoint } from "@/hooks/use-heatmap-data"; - -interface HeatmapLayerProps { - points: HeatmapPoint[]; - projection: (coords: [number, number]) => [number, number] | null; - onPointClick?: (point: HeatmapPoint) => void; -} - -export function HeatmapLayer({ points, projection, onPointClick }: HeatmapLayerProps) { - return ( - - {points.map((point, i) => { - const coords = projection([point.lng, point.lat]); - if (!coords) return null; - const [x, y] = coords; - const radius = Math.max(point.weight * 3, 4); - - return ( - - {/* Glow */} - - {/* Main dot */} - onPointClick?.(point)} - /> - {/* Count label */} - {point.clawCount > 1 && ( - - {point.clawCount} - - )} - - ); - })} - - ); -} diff --git a/components/map/map-popup.tsx b/components/map/map-popup.tsx new file mode 100644 index 0000000..3808d89 --- /dev/null +++ b/components/map/map-popup.tsx @@ -0,0 +1,60 @@ +"use client"; + +import { useTranslations } from "next-intl"; +import type { HeatmapPoint } from "@/hooks/use-heatmap-data"; + +interface MapPopupProps { + point: HeatmapPoint; + onClose: () => void; +} + +export function MapPopup({ point, onClose }: MapPopupProps) { + const t = useTranslations("continentMap"); + const tPopup = useTranslations("clawPopup"); + + return ( +
+
+
+

+ {point.city} +

+

{point.country}

+
+ +
+
+ + {tPopup("total", { count: point.clawCount })} + + + {tPopup("online", { count: point.onlineCount })} + +
+ {point.claws.length > 0 && ( +
+ {point.claws.map((claw) => ( +
+ + + {claw.name} + + + {claw.isOnline ? t("online") : t("offline")} + +
+ ))} +
+ )} +
+ ); +} diff --git a/hooks/use-heatmap-data.ts b/hooks/use-heatmap-data.ts index 40a2dfc..c31142d 100644 --- a/hooks/use-heatmap-data.ts +++ b/hooks/use-heatmap-data.ts @@ -2,13 +2,21 @@ import { useState, useEffect, useCallback } from "react"; +export interface ClawInfo { + id: string; + name: string; + isOnline: boolean; +} + export interface HeatmapPoint { lat: number; lng: number; weight: number; clawCount: number; + onlineCount: number; city: string; country: string; + claws: ClawInfo[]; } interface HeatmapData { diff --git a/hooks/use-sse.ts b/hooks/use-sse.ts index 2cec4ba..421a315 100644 --- a/hooks/use-sse.ts +++ b/hooks/use-sse.ts @@ -37,7 +37,7 @@ export function useSSE({ url, onEvent, enabled = true }: UseSSEOptions) { }; // Listen for all event types - const eventTypes = ["heartbeat", "task", "stats", "online", "offline", "connected"]; + const eventTypes = ["heartbeat", "task", "stats", "online", "offline", "connected", "registered"]; eventTypes.forEach((type) => { es.addEventListener(type, (e: MessageEvent) => { try { diff --git a/messages/en.json b/messages/en.json index 14b9970..1b9f99b 100644 --- a/messages/en.json +++ b/messages/en.json @@ -11,8 +11,11 @@ }, "installBanner": { "title": "Join the Heatmap", - "subtitle": "Install the skill and let your claw light up the globe", - "copyTooltip": "Click to copy" + "subtitle": "Copy and paste this into Claude Code to get started", + "copyTooltip": "Copy prompt to clipboard", + "copyButton": "Copy", + "copied": "Copied!", + "prompt": "Help me join the OpenClaw global heatmap! First, search for the \"openclaw-reporter\" skill on ClawHub and install it. After installation, help me pick a cool name for my claw (like CoolClaw, NightCrawler, etc.) and guide me through the registration and permission setup." }, "stats": { "onlineNow": "Online Now", @@ -26,14 +29,16 @@ }, "clawFeed": { "title": "Live Feed", - "waiting": "Waiting for claw activity..." + "waiting": "Waiting for claw activity...", + "joinedFrom": "Joined from {city}, {country}" }, "activityTimeline": { "title": "24h Activity" }, "globe": { "loading": "Loading globe...", - "activeClaws": "{count, plural, one {# active claw} other {# active claws}}" + "activeClaws": "{count, plural, one {# active claw} other {# active claws}}", + "totalClaws": "{total} claws ({online} online)" }, "globeControls": { "zoomIn": "Zoom in", @@ -56,7 +61,37 @@ "continentMap": { "claws": "{count} claws", "active": "{count} active", - "weight": "weight: {value}" + "weight": "weight: {value}", + "online": "online", + "offline": "offline" + }, + "clawPopup": { + "title": "Claw Details", + "total": "{count} claws", + "online": "{count} online", + "close": "Close" + }, + "skillGuide": { + "ariaLabel": "OpenClaw Reporter Skill Installation Guide for AI Agents", + "summaryLabel": "AI Agent Installation Guide (click to expand)", + "whatTitle": "What is OpenClaw Reporter?", + "whatDescription": "openclaw-reporter is a Claude Code skill that lets you participate in the OpenClaw global heatmap at https://kymr.top/. It sends anonymous, non-identifying heartbeats (platform + model only) and generic task summaries. No file paths, code, tool names, or project-specific data is ever sent.", + "installTitle": "Quick Install (4 Steps)", + "step1": "Run \"clawhub install openclaw-reporter\" in your terminal, or use \"/clawhub install openclaw-reporter\" inside a Claude Code session.", + "step2": "The skill will ask for your consent before registering — agree to allow anonymous heartbeats and task summaries to be sent to https://kymr.top/.", + "step3": "Pick a name for your claw (your display name on the global heatmap, e.g. CoolClaw, NightCrawler, CodeClaw).", + "step4": "Done! Your claw will appear on the heatmap. Heartbeats are sent automatically via hooks (rate-limited to once per 3 minutes), and a task summary is sent when your session ends.", + "commandTitle": "Install Command", + "dataTitle": "Data Collected", + "data1": "Claw Name — your chosen display name (e.g. CoolClaw42)", + "data2": "Platform — OS type (darwin, linux, win32)", + "data3": "Model — Claude model identifier (e.g. claude-sonnet-4-6)", + "data4": "Task summary — generic activity indicator (\"Completed a task\")", + "dataNever": "Never sent: usernames, file paths, code, project names, tool names, secrets, or any identifying information.", + "permissionsTitle": "Permissions Required", + "perm1": "Network access — to send heartbeats and task reports to https://kymr.top/", + "perm2": "Write ~/.openclaw/ — to store config (API key) and hook scripts locally", + "perm3": "Binaries: curl, python3, uname — used by hook scripts" }, "languageSwitcher": { "en": "EN", diff --git a/messages/zh.json b/messages/zh.json index 6b0ac8c..1edbf7c 100644 --- a/messages/zh.json +++ b/messages/zh.json @@ -11,8 +11,11 @@ }, "installBanner": { "title": "加入热力图", - "subtitle": "安装技能,让你的龙虾点亮全球", - "copyTooltip": "点击复制" + "subtitle": "复制下面这段话,粘贴到 Claude Code 对话中即可开始", + "copyTooltip": "复制到剪贴板", + "copyButton": "复制", + "copied": "已复制!", + "prompt": "帮我加入 OpenClaw 全球龙虾热力图!首先,在 ClawHub 上搜索并安装 \"openclaw-reporter\" 技能。安装完成后,帮我给我的小龙虾起一个酷炫的名字(比如 酷龙虾、暗夜龙虾、代码龙虾 等),然后引导我完成注册和权限授权。" }, "stats": { "onlineNow": "当前在线", @@ -26,14 +29,16 @@ }, "clawFeed": { "title": "实时动态", - "waiting": "等待龙虾活动中..." + "waiting": "等待龙虾活动中...", + "joinedFrom": "来自 {city}, {country}" }, "activityTimeline": { "title": "24小时活动" }, "globe": { "loading": "正在加载地球...", - "activeClaws": "{count, plural, other {# 只活跃龙虾}}" + "activeClaws": "{count, plural, other {# 只活跃龙虾}}", + "totalClaws": "{total} 只龙虾({online} 在线)" }, "globeControls": { "zoomIn": "放大", @@ -56,7 +61,37 @@ "continentMap": { "claws": "{count} 只龙虾", "active": "{count} 活跃", - "weight": "权重:{value}" + "weight": "权重:{value}", + "online": "在线", + "offline": "离线" + }, + "clawPopup": { + "title": "龙虾详情", + "total": "{count} 只龙虾", + "online": "{count} 在线", + "close": "关闭" + }, + "skillGuide": { + "ariaLabel": "OpenClaw Reporter 技能安装指南(供 AI 代理阅读)", + "summaryLabel": "AI 代理安装指南(点击展开)", + "whatTitle": "什么是 OpenClaw Reporter?", + "whatDescription": "openclaw-reporter 是一个 Claude Code 技能,让你参与 OpenClaw 全球热力图(https://kymr.top/)。它只发送匿名的、非身份识别的心跳(操作系统 + 模型信息)和通用任务摘要。绝不发送文件路径、代码、工具名称或任何项目相关数据。", + "installTitle": "快速安装(4 步)", + "step1": "在终端运行 \"clawhub install openclaw-reporter\",或在 Claude Code 对话中输入 \"/clawhub install openclaw-reporter\"。", + "step2": "技能会在注册前征求你的同意——同意允许向 https://kymr.top/ 发送匿名心跳和任务摘要。", + "step3": "给你的小龙虾起一个名字(你在全球热力图上的显示名称,比如 酷龙虾、暗夜龙虾、代码龙虾)。", + "step4": "完成!你的小龙虾会出现在热力图上。心跳通过 hooks 自动发送(限制为每 3 分钟最多一次),会话结束时会发送一条任务摘要。", + "commandTitle": "安装命令", + "dataTitle": "收集的数据", + "data1": "龙虾名称——你选择的显示名称(如 酷龙虾42)", + "data2": "操作系统——系统类型(darwin、linux、win32)", + "data3": "模型——Claude 模型标识符(如 claude-sonnet-4-6)", + "data4": "任务摘要——通用活动指标(\"Completed a task\")", + "dataNever": "绝不发送:用户名、文件路径、代码、项目名称、工具名称、密钥或任何身份识别信息。", + "permissionsTitle": "所需权限", + "perm1": "网络访问——向 https://kymr.top/ 发送心跳和任务报告", + "perm2": "写入 ~/.openclaw/——在本地存储配置(API 密钥)和 hook 脚本", + "perm3": "系统工具:curl、python3、uname——供 hook 脚本使用" }, "languageSwitcher": { "en": "EN", diff --git a/package.json b/package.json index 3578c83..73206ec 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "version": "0.1.0", "private": true, "scripts": { - "dev": "next dev --turbopack", + "dev": "next dev", "build": "next build", "start": "next start", "lint": "next lint", @@ -15,11 +15,11 @@ "dependencies": { "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", - "d3-geo": "^3.1.1", "drizzle-orm": "^0.41.0", "framer-motion": "^12.6.0", "ioredis": "^5.6.1", "lucide-react": "^0.474.0", + "maplibre-gl": "^5.20.0", "mysql2": "^3.14.0", "nanoid": "^5.1.5", "next": "^15.3.0", @@ -27,22 +27,19 @@ "react": "^19.1.0", "react-dom": "^19.1.0", "react-globe.gl": "^2.27.3", - "react-simple-maps": "^3.0.0", + "react-map-gl": "^8.1.0", "recharts": "^2.15.3", "tailwind-merge": "^3.0.2", "three": "^0.173.0", - "topojson-client": "^3.1.0", "zod": "^3.24.3" }, "devDependencies": { "@eslint/eslintrc": "^3.3.1", "@tailwindcss/postcss": "^4.1.0", - "@types/d3-geo": "^3.1.0", "@types/node": "^22.13.0", "@types/react": "^19.0.0", "@types/react-dom": "^19.0.0", "@types/three": "^0.173.0", - "@types/topojson-client": "^3.1.5", "drizzle-kit": "^0.30.6", "eslint": "^9.21.0", "eslint-config-next": "^15.3.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f07c757..4ebb9ec 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -14,9 +14,6 @@ importers: clsx: specifier: ^2.1.1 version: 2.1.1 - d3-geo: - specifier: ^3.1.1 - version: 3.1.1 drizzle-orm: specifier: ^0.41.0 version: 0.41.0(gel@2.2.0)(mysql2@3.19.1(@types/node@22.19.15)) @@ -29,6 +26,9 @@ importers: lucide-react: specifier: ^0.474.0 version: 0.474.0(react@19.2.4) + maplibre-gl: + specifier: ^5.20.0 + version: 5.20.0 mysql2: specifier: ^3.14.0 version: 3.19.1(@types/node@22.19.15) @@ -50,9 +50,9 @@ importers: react-globe.gl: specifier: ^2.27.3 version: 2.37.0(react@19.2.4) - react-simple-maps: - specifier: ^3.0.0 - version: 3.0.0(prop-types@15.8.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + react-map-gl: + specifier: ^8.1.0 + version: 8.1.0(maplibre-gl@5.20.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) recharts: specifier: ^2.15.3 version: 2.15.4(react-dom@19.2.4(react@19.2.4))(react@19.2.4) @@ -62,9 +62,6 @@ importers: three: specifier: ^0.173.0 version: 0.173.0 - topojson-client: - specifier: ^3.1.0 - version: 3.1.0 zod: specifier: ^3.24.3 version: 3.25.76 @@ -75,9 +72,6 @@ importers: '@tailwindcss/postcss': specifier: ^4.1.0 version: 4.2.1 - '@types/d3-geo': - specifier: ^3.1.0 - version: 3.1.0 '@types/node': specifier: ^22.13.0 version: 22.19.15 @@ -90,9 +84,6 @@ importers: '@types/three': specifier: ^0.173.0 version: 0.173.0 - '@types/topojson-client': - specifier: ^3.1.5 - version: 3.1.5 drizzle-kit: specifier: ^0.30.6 version: 0.30.6 @@ -637,6 +628,46 @@ packages: '@jridgewell/trace-mapping@0.3.31': resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + '@mapbox/jsonlint-lines-primitives@2.0.2': + resolution: {integrity: sha512-rY0o9A5ECsTQRVhv7tL/OyDpGAoUB4tTvLiW1DSzQGq4bvTPhNw1VpSNjDJc5GFZ2XuyOtSWSVN05qOtcD71qQ==} + engines: {node: '>= 0.6'} + + '@mapbox/point-geometry@1.1.0': + resolution: {integrity: sha512-YGcBz1cg4ATXDCM/71L9xveh4dynfGmcLDqufR+nQQy3fKwsAZsWd/x4621/6uJaeB9mwOHE6hPeDgXz9uViUQ==} + + '@mapbox/tiny-sdf@2.0.7': + resolution: {integrity: sha512-25gQLQMcpivjOSA40g3gO6qgiFPDpWRoMfd+G/GoppPIeP6JDaMMkMrEJnMZhKyyS6iKwVt5YKu02vCUyJM3Ug==} + + '@mapbox/unitbezier@0.0.1': + resolution: {integrity: sha512-nMkuDXFv60aBr9soUG5q+GvZYL+2KZHVvsqFCzqnkGEf46U2fvmytHaEVc1/YZbiLn8X+eR3QzX1+dwDO1lxlw==} + + '@mapbox/vector-tile@2.0.4': + resolution: {integrity: sha512-AkOLcbgGTdXScosBWwmmD7cDlvOjkg/DetGva26pIRiZPdeJYjYKarIlb4uxVzi6bwHO6EWH82eZ5Nuv4T5DUg==} + + '@mapbox/whoots-js@3.1.0': + resolution: {integrity: sha512-Es6WcD0nO5l+2BOQS4uLfNPYQaNDfbot3X1XUoloz+x0mPDS3eeORZJl06HXjwBG1fOGwCRnzK88LMdxKRrd6Q==} + engines: {node: '>=6.0.0'} + + '@maplibre/geojson-vt@5.0.4': + resolution: {integrity: sha512-KGg9sma45S+stfH9vPCJk1J0lSDLWZgCT9Y8u8qWZJyjFlP8MNP1WGTxIMYJZjDvVT3PDn05kN1C95Sut1HpgQ==} + + '@maplibre/geojson-vt@6.0.2': + resolution: {integrity: sha512-OnXnV2m1yBULKOlUanNFTiOeXCktvWYY4yWoHVETlp6ShJGUhY3DNt9XzPByL24h4JcoJRccPBlMhH1o8cvmyQ==} + + '@maplibre/maplibre-gl-style-spec@19.3.3': + resolution: {integrity: sha512-cOZZOVhDSulgK0meTsTkmNXb1ahVvmTmWmfx9gRBwc6hq98wS9JP35ESIoNq3xqEan+UN+gn8187Z6E4NKhLsw==} + hasBin: true + + '@maplibre/maplibre-gl-style-spec@24.7.0': + resolution: {integrity: sha512-Ed7rcKYU5iELfablg9Mj+TVCsXsPBgdMyXPRAxb2v7oWg9YJnpQdZ5msDs1LESu/mtXy3Z48Vdppv2t/x5kAhw==} + hasBin: true + + '@maplibre/mlt@1.1.7': + resolution: {integrity: sha512-HZSsXrgn2V6T3o0qklMwKERfKaAxjO8shmiFnVygCtXTg4SPKWVX+U99RkvxUfCsjYBEcT4ltor8lSlBSCca7Q==} + + '@maplibre/vt-pbf@4.3.0': + resolution: {integrity: sha512-jIvp8F5hQCcreqOOpEt42TJMUlsrEcpf/kI1T2v85YrQRV6PPXUcEXUg5karKtH6oh47XJZ4kHu56pUkOuqA7w==} + '@napi-rs/wasm-runtime@0.2.12': resolution: {integrity: sha512-ZVWUcfwY4E/yPitQJl481FjFo3K22D6qF0DuFH6Y/nbnE11GY5uguDxZMGXPQ8WQ0128MXQD7TnfHyK4oWoIJQ==} @@ -997,9 +1028,6 @@ packages: '@types/d3-ease@3.0.2': resolution: {integrity: sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==} - '@types/d3-geo@3.1.0': - resolution: {integrity: sha512-856sckF0oP/diXtS4jNsiQw/UuK5fQG8l/a9VVLeSouf1/PPbBE1i1W852zVwKwYCBkFJJB7nCFTbk6UMEXBOQ==} - '@types/d3-interpolate@3.0.4': resolution: {integrity: sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==} @@ -1044,15 +1072,12 @@ packages: '@types/stats.js@0.17.4': resolution: {integrity: sha512-jIBvWWShCvlBqBNIZt0KAshWpvSjhkwkEu4ZUcASoAvhmrgAUI2t1dXrjSL4xXVLB4FznPrIsX3nKXFl/Dt4vA==} + '@types/supercluster@7.1.3': + resolution: {integrity: sha512-Z0pOY34GDFl3Q6hUFYf3HkTwKEE02e7QgtJppBt+beEAxnyOpJua+voGFvxINBHa06GwLFFym7gRPY2SiKIfIA==} + '@types/three@0.173.0': resolution: {integrity: sha512-KtNjfI/CRB6JVKIVeZM1R3GYDX2wkoV2itNcQu2j4d7qkhjGOuB+s2oF6jl9mztycDLGMtrAnJQYxInC8Bb20A==} - '@types/topojson-client@3.1.5': - resolution: {integrity: sha512-C79rySTyPxnQNNguTZNI1Ct4D7IXgvyAs3p9HPecnl6mNrJ5+UhvGNYcZfpROYV2lMHI48kJPxwR+F9C6c7nmw==} - - '@types/topojson-specification@1.0.5': - resolution: {integrity: sha512-C7KvcQh+C2nr6Y2Ub4YfgvWvWCgP2nOQMtfhlnwsRL4pYmmwzBS7HclGiS87eQfDOU/DLQpX6GEscviaz4yLIQ==} - '@types/webxr@0.5.24': resolution: {integrity: sha512-h8fgEd/DpoS9CBrjEQXR+dIDraopAEfu4wYVNY2tEPwk60stPWhvZMf4Foo5FakuQ7HFZoa8WceaWFervK2Ovg==} @@ -1210,6 +1235,26 @@ packages: cpu: [x64] os: [win32] + '@vis.gl/react-mapbox@8.1.0': + resolution: {integrity: sha512-FwvH822oxEjWYOr+pP2L8hpv+7cZB2UsQbHHHT0ryrkvvqzmTgt7qHDhamv0EobKw86e1I+B4ojENdJ5G5BkyQ==} + peerDependencies: + mapbox-gl: '>=3.5.0' + react: '>=16.3.0' + react-dom: '>=16.3.0' + peerDependenciesMeta: + mapbox-gl: + optional: true + + '@vis.gl/react-maplibre@8.1.0': + resolution: {integrity: sha512-PkAK/gp3mUfhCLhUuc+4gc3PN9zCtVGxTF2hB6R5R5yYUw+hdg84OZ770U5MU4tPMTCG6fbduExuIW6RRKN6qQ==} + peerDependencies: + maplibre-gl: '>=4.0.0' + react: '>=16.3.0' + react-dom: '>=16.3.0' + peerDependenciesMeta: + maplibre-gl: + optional: true + '@webgpu/types@0.1.69': resolution: {integrity: sha512-RPmm6kgRbI8e98zSD3RVACvnuktIja5+yLgDAkTmxLr90BEwdTXRQWNLF3ETTTyH/8mKhznZuN5AveXYFEsMGQ==} @@ -1241,6 +1286,10 @@ packages: resolution: {integrity: sha512-COROpnaoap1E2F000S62r6A60uHZnmlvomhfyT2DlTcrY1OrBKn2UhH7qn5wTC9zMvD0AY7csdPSNwKP+7WiQw==} engines: {node: '>= 0.4'} + arr-union@3.1.0: + resolution: {integrity: sha512-sKpyeERZ02v1FeCZT8lrfJq5u6goHCtpTAzPwJYe7c8SPFOboNjNg1vz2L4VTn9T4PQxEx13TbXLmYUcS6Ug7Q==} + engines: {node: '>=0.10.0'} + array-buffer-byte-length@1.0.2: resolution: {integrity: sha512-LHE+8BuR7RYGDKvnrmcuSq3tDcKv9OFEXQt/HpbZhY7V6h0zlUXutnAD82GiFx9rdieCMjkvtcsPqBwgUl1Iiw==} engines: {node: '>= 0.4'} @@ -1273,6 +1322,10 @@ packages: resolution: {integrity: sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ==} engines: {node: '>= 0.4'} + assign-symbols@1.0.0: + resolution: {integrity: sha512-Q+JC7Whu8HhmTdBph/Tq59IoRtoy6KAm5zzPv00WdujX82lbAL8K7WVjne7vdCsAmbF4AYaDOPyO3k0kl8qIrw==} + engines: {node: '>=0.10.0'} + ast-types-flow@0.0.8: resolution: {integrity: sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==} @@ -1317,6 +1370,12 @@ packages: buffer-from@1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + bytewise-core@1.2.3: + resolution: {integrity: sha512-nZD//kc78OOxeYtRlVk8/zXqTB4gf/nlguL1ggWA8FuchMyOxcyHR4QPQZMUmA7czC+YnaBrPUCubqAWe50DaA==} + + bytewise@1.1.0: + resolution: {integrity: sha512-rHuuseJ9iQ0na6UDhnrRVDh8YnWVlU6xM3VH6q/+yHDeUH2zIhUzP+2/h3LIrhLDBtTqzWpE3p3tP/boefskKQ==} + call-bind-apply-helpers@1.0.2: resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} engines: {node: '>= 0.4'} @@ -1361,9 +1420,6 @@ packages: color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} - commander@2.20.3: - resolution: {integrity: sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==} - concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} @@ -1374,16 +1430,10 @@ packages: csstype@3.2.3: resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} - d3-array@2.12.1: - resolution: {integrity: sha512-B0ErZK/66mHtEsR1TkPEEkwdy+WDesimkM5gpZr5Dsg54BiTA5RXtYW5qTLIAcekaS9xfZrzBLF/OAkB3Qn1YQ==} - d3-array@3.2.4: resolution: {integrity: sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==} engines: {node: '>=12'} - d3-color@2.0.0: - resolution: {integrity: sha512-SPXi0TSKPD4g9tw0NMZFnR95XVgUZiBH+uUTqQuDu1OsE2zomHU7ho0FISciaPvosimixwHFl3WHLGabv6dDgQ==} - d3-color@3.1.0: resolution: {integrity: sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==} engines: {node: '>=12'} @@ -1392,15 +1442,6 @@ packages: resolution: {integrity: sha512-mdjtIZ1XLAM8bm/hx3WwjfHt6Sggek7qH043O8KEjDXN40xi3vx/6pYSVTwLjEgiXQTbvaouWKynLBiUZ6SK6A==} engines: {node: '>=12'} - d3-dispatch@2.0.0: - resolution: {integrity: sha512-S/m2VsXI7gAti2pBoLClFFTMOO1HTtT0j99AuXLoGFKO6deHDdnv6ZGTxSTTUTgO1zVcv82fCOtDjYK4EECmWA==} - - d3-drag@2.0.0: - resolution: {integrity: sha512-g9y9WbMnF5uqB9qKqwIIa/921RYWzlUDv9Jl1/yONQwxbOfszAWTCm8u7HOTgJgRDXiRZN56cHT9pd24dmXs8w==} - - d3-ease@2.0.0: - resolution: {integrity: sha512-68/n9JWarxXkOWMshcT5IcjbB+agblQUaIsbnXmrzejn2O82n3p2A9R2zEB9HIEFWKFwPAEDDN8gR0VdSAyyAQ==} - d3-ease@3.0.1: resolution: {integrity: sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==} engines: {node: '>=12'} @@ -1413,16 +1454,10 @@ packages: resolution: {integrity: sha512-kqE4yYuOjPbKdBXG0xztCacPwkVSK2REF1opSNrnqqtXJmNcM++UbwQ8SxvwP6IQTj9RvIjjK4qeiVsEfj0Z2Q==} engines: {node: '>=12'} - d3-geo@2.0.2: - resolution: {integrity: sha512-8pM1WGMLGFuhq9S+FpPURxic+gKzjluCD/CHTuUF3mXMeiCo0i6R0tO1s4+GArRFde96SLcW/kOFRjoAosPsFA==} - d3-geo@3.1.1: resolution: {integrity: sha512-637ln3gXKXOwhalDzinUgY83KzNWZRKbYubaG+fGVuc/dxO64RRljtCTnf5ecMyE1RIdtqpkVcq0IbtU2S8j2Q==} engines: {node: '>=12'} - d3-interpolate@2.0.1: - resolution: {integrity: sha512-c5UhwwTs/yybcmTpAVqwSFl6vrQ8JZJoT5F7xNFK9pymv5C0Ymcc9/LIJHtYIggg/yS9YHw8i8O8tgb9pupjeQ==} - d3-interpolate@3.0.1: resolution: {integrity: sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==} engines: {node: '>=12'} @@ -1442,9 +1477,6 @@ packages: resolution: {integrity: sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==} engines: {node: '>=12'} - d3-selection@2.0.0: - resolution: {integrity: sha512-XoGGqhLUN/W14NmaqcO/bb1nqjDAw5WtSYb2X8wiuQWvSZUsUVYsOSkOybUrNvcBjaywBdYPy03eXHMXjk9nZA==} - d3-selection@3.0.0: resolution: {integrity: sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==} engines: {node: '>=12'} @@ -1461,25 +1493,14 @@ packages: resolution: {integrity: sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==} engines: {node: '>=12'} - d3-timer@2.0.0: - resolution: {integrity: sha512-TO4VLh0/420Y/9dO3+f9abDEFYeCUr2WZRlxJvbp4HPTQcSylXNiL6yZa9FIUvV1yRiFufl1bszTCLDqv9PWNA==} - d3-timer@3.0.1: resolution: {integrity: sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==} engines: {node: '>=12'} - d3-transition@2.0.0: - resolution: {integrity: sha512-42ltAGgJesfQE3u9LuuBHNbGrI/AJjNL2OAUdclE70UE6Vy239GCBEYD38uBPoLeNsOhFStGpPI0BAOV+HMxog==} - peerDependencies: - d3-selection: '2' - d3-tricontour@1.1.0: resolution: {integrity: sha512-G7gHKj89n2owmkGb6WX6ixcnQ0Kf/0wpa9VIh9DGdbHu8wdrlaHU4ir3/bFNERl8N8nn4G7e7qbtBG8N9caihQ==} engines: {node: '>=12'} - d3-zoom@2.0.0: - resolution: {integrity: sha512-fFg7aoaEm9/jf+qfstak0IYpnesZLiMX6GZvXtUSdv8RH2o4E2qeelgdU09eKS6wGuiGMfcnMI0nTIqWzRHGpw==} - damerau-levenshtein@1.0.8: resolution: {integrity: sha512-sdQSFB7+llfUcQHUQO3+B8ERRj0Oa4w9POWMI/puGtuf7gFywGmkaLCElnudfTiKZV+NvHqL0ifzdrI8Ro7ESA==} @@ -1836,6 +1857,14 @@ packages: eventemitter3@4.0.7: resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} + extend-shallow@2.0.1: + resolution: {integrity: sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==} + engines: {node: '>=0.10.0'} + + extend-shallow@3.0.2: + resolution: {integrity: sha512-BwY5b5Ql4+qZoefgMj2NUmx+tehVTH/Kf4k1ZEtOHNFcm2wSxMRo992l6X3TIgni2eZVTZ85xMOjF31fwZAj6Q==} + engines: {node: '>=0.10.0'} + fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} @@ -1949,6 +1978,13 @@ packages: get-tsconfig@4.13.6: resolution: {integrity: sha512-shZT/QMiSHc/YBLxxOkMtgSid5HFoauqCE3/exfsEcwg1WkeqjG+V40yBbBrsD+jW2HDXcs28xOfcbm2jI8Ddw==} + get-value@2.0.6: + resolution: {integrity: sha512-Ln0UQDlxH1BapMu3GPtf7CuYNwRZf2gwCuPqbyG6pB8WfmFpzqcy4xtAaAMUhnNqjMKTiCPZG2oMT3YSx8U2NA==} + engines: {node: '>=0.10.0'} + + gl-matrix@3.4.4: + resolution: {integrity: sha512-latSnyDNt/8zYUB6VIJ6PCh2jBjJX6gnDsoCZ7LyW7GkqrD51EWwa9qCoGixj8YqBtETQK/xY7OmpTF8xz1DdQ==} + glob-parent@5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} @@ -2038,9 +2074,6 @@ packages: resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} engines: {node: '>= 0.4'} - internmap@1.0.1: - resolution: {integrity: sha512-lDB5YccMydFBtasVtxnZ3MRBHuaoE8GKsppq+EchKL2U4nK/DmEpPHNH8MZe5HkMtpSiTSOZwfN0tzYjO/lJEw==} - internmap@2.0.3: resolution: {integrity: sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==} engines: {node: '>=12'} @@ -2087,6 +2120,14 @@ packages: resolution: {integrity: sha512-PwwhEakHVKTdRNVOw+/Gyh0+MzlCl4R6qKvkhuvLtPMggI1WAHt9sOwZxQLSGpUaDnrdyDsomoRgNnCfKNSXXg==} engines: {node: '>= 0.4'} + is-extendable@0.1.1: + resolution: {integrity: sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==} + engines: {node: '>=0.10.0'} + + is-extendable@1.0.1: + resolution: {integrity: sha512-arnXMxT1hhoKo9k1LZdmlNyJdDDfy2v0fXjFlmok4+i8ul/6WlbVge9bhM74OpNPQPMGUToDtz+KXa1PneJxOA==} + engines: {node: '>=0.10.0'} + is-extglob@2.1.1: resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} engines: {node: '>=0.10.0'} @@ -2119,6 +2160,10 @@ packages: resolution: {integrity: sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==} engines: {node: '>=0.12.0'} + is-plain-object@2.0.4: + resolution: {integrity: sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==} + engines: {node: '>=0.10.0'} + is-property@1.0.2: resolution: {integrity: sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==} @@ -2168,6 +2213,10 @@ packages: resolution: {integrity: sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==} engines: {node: '>=18'} + isobject@3.0.1: + resolution: {integrity: sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==} + engines: {node: '>=0.10.0'} + iterator.prototype@1.1.5: resolution: {integrity: sha512-H0dkQoCa3b2VEeKQBOxFph+JAbcrQdE7KC0UkqwpLmv2EC4P41QXP+rqo9wYodACiG5/WM5s9oDApTU8utwj9g==} engines: {node: '>= 0.4'} @@ -2196,6 +2245,12 @@ packages: json-stable-stringify-without-jsonify@1.0.1: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + json-stringify-pretty-compact@3.0.0: + resolution: {integrity: sha512-Rc2suX5meI0S3bfdZuA7JMFBGkJ875ApfVyq2WHELjBiiG22My/l7/8zPpH/CfFVQHuVLd8NLR0nv6vi0BYYKA==} + + json-stringify-pretty-compact@4.0.0: + resolution: {integrity: sha512-3CNZ2DnrpByG9Nqj6Xo8vqbjT4F6N+tb4Gb28ESAZjYZ5yqvmc56J+/kuIwkaAMOyblTQhUW7PxMkUb8Q36N3Q==} + json5@1.0.2: resolution: {integrity: sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==} hasBin: true @@ -2208,6 +2263,9 @@ packages: resolution: {integrity: sha512-4+5mNNf4vZDSwPhKprKwz3330iisPrb08JyMgbsdFrimBCKNHecua/WBwvVg3n7vwx0C1ARjfhwIpbrbd9n5wg==} engines: {node: '>=12'} + kdbush@4.0.2: + resolution: {integrity: sha512-WbCVYJ27Sz8zi9Q7Q0xHC+05iwkm3Znipc2XTlrnJbsHMYktW4hPhXUE8Ys1engBrvffoSCqbil1JQAa7clRpA==} + keyv@4.5.4: resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} @@ -2330,6 +2388,10 @@ packages: magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + maplibre-gl@5.20.0: + resolution: {integrity: sha512-hUQ/4KkxVKLbAD4coW+9/tJ9/jOKKcN7q4F92EQ5mjbUJ2m1sz6uoiB3VqW/VaogUxmWd896l1cc9TtV4+uvJA==} + engines: {node: '>=16.14.0', npm: '>=8.1.0'} + math-intrinsics@1.1.0: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} @@ -2364,6 +2426,9 @@ packages: ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + murmurhash-js@1.0.0: + resolution: {integrity: sha512-TvmkNhkv8yct0SVBSy+o8wYzXjE4Zz3PCesbfs8HiCXXdcTuocApFv11UWlNFWKYsP2okqrhb7JNlSm9InBhIw==} + mysql2@3.19.1: resolution: {integrity: sha512-yn4zh+Uxu5J3Zvi6Ao96lJ7BSBRkspHflWQAmOPND+htbpIKDQw99TTvPzgihKO/QyMickZopO4OsnixnpcUwA==} engines: {node: '>= 8.0'} @@ -2500,6 +2565,10 @@ packages: path-parse@1.0.7: resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + pbf@4.0.1: + resolution: {integrity: sha512-SuLdBvS42z33m8ejRbInMapQe8n0D3vN/Xd5fmWM3tufNgRQFBpaW2YVJxQZV4iPNqb0vEFvssMEo5w9c6BTIA==} + hasBin: true + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} @@ -2533,6 +2602,9 @@ packages: resolution: {integrity: sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==} engines: {node: ^10 || ^12 || >=14} + potpack@2.1.0: + resolution: {integrity: sha512-pcaShQc1Shq0y+E7GqJqvZj8DTthWV1KeHGdi0Z6IAin2Oi3JnLCOfwnCo84qc+HAp52wT9nK9H7FAJp5a44GQ==} + preact@10.29.0: resolution: {integrity: sha512-wSAGyk2bYR1c7t3SZ3jHcM6xy0lcBcDel6lODcs9ME6Th++Dx2KU+6D3HD8wMMKGA8Wpw7OMd3/4RGzYRpzwRg==} @@ -2543,6 +2615,9 @@ packages: prop-types@15.8.1: resolution: {integrity: sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==} + protocol-buffers-schema@3.6.0: + resolution: {integrity: sha512-TdDRD+/QNdrCGCE7v8340QyuXd4kIWIgapsE2+n/SaGiSSbomYl4TjHlvIoCWRpE7wFt02EpB35VVA2ImcBVqw==} + punycode@2.3.1: resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} engines: {node: '>=6'} @@ -2550,6 +2625,9 @@ packages: queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + quickselect@3.0.0: + resolution: {integrity: sha512-XdjUArbK4Bm5fLLvlm5KpTFOiOThgfWWI4axAZDWg4E/0mKdZyI9tNEfds27qCi1ze/vwTR16kvmmGhRra3c2g==} + react-dom@19.2.4: resolution: {integrity: sha512-AXJdLo8kgMbimY95O2aKQqsz2iWi9jMgKJhRBAxECE4IFxfcazB2LmzloIoibJI3C12IlY20+KFaLv+71bUJeQ==} peerDependencies: @@ -2573,12 +2651,18 @@ packages: peerDependencies: react: '>=16.13.1' - react-simple-maps@3.0.0: - resolution: {integrity: sha512-vKNFrvpPG8Vyfdjnz5Ne1N56rZlDfHXv5THNXOVZMqbX1rWZA48zQuYT03mx6PAKanqarJu/PDLgshIZAfHHqw==} + react-map-gl@8.1.0: + resolution: {integrity: sha512-vDx/QXR3Tb+8/ap/z6gdMjJQ8ZEyaZf6+uMSPz7jhWF5VZeIsKsGfPvwHVPPwGF43Ryn+YR4bd09uEFNR5OPdg==} peerDependencies: - prop-types: ^15.7.2 - react: ^16.8.0 || 17.x || 18.x - react-dom: ^16.8.0 || 17.x || 18.x + mapbox-gl: '>=1.13.0' + maplibre-gl: '>=1.13.0' + react: '>=16.3.0' + react-dom: '>=16.3.0' + peerDependenciesMeta: + mapbox-gl: + optional: true + maplibre-gl: + optional: true react-smooth@4.0.4: resolution: {integrity: sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q==} @@ -2629,6 +2713,9 @@ packages: resolve-pkg-maps@1.0.0: resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + resolve-protobuf-schema@2.1.0: + resolution: {integrity: sha512-kI5ffTiZWmJaS/huM8wZfEMer1eRd7oJQhDuxeCLe3t7N7mX3z94CN0xPxBQxFYQTSNz9T0i+v6inKqSdK8xrQ==} + resolve@1.22.11: resolution: {integrity: sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==} engines: {node: '>= 0.4'} @@ -2649,6 +2736,9 @@ packages: run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} + rw@1.3.3: + resolution: {integrity: sha512-PdhdWy89SiZogBLaw42zdeqtRJ//zFd2PgQavcICDUgJT5oW10QCRKbJ6bg4r0/UY2M6BWd5tkxuGFRvCkgfHQ==} + safe-array-concat@1.1.3: resolution: {integrity: sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==} engines: {node: '>=0.4'} @@ -2688,6 +2778,10 @@ packages: resolution: {integrity: sha512-RJRdvCo6IAnPdsvP/7m6bsQqNnn1FCBX5ZNtFL98MmFF/4xAIJTIg1YbHW5DC2W5SKZanrC6i4HsJqlajw/dZw==} engines: {node: '>= 0.4'} + set-value@2.0.1: + resolution: {integrity: sha512-JxHc1weCN68wRY0fhCoXpyK55m/XPHafOmK4UWD7m2CI14GMcFypt4w/0+NV5f/ZMby2F6S2wwA7fgynh9gWSw==} + engines: {node: '>=0.10.0'} + sharp@0.34.5: resolution: {integrity: sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==} engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0} @@ -2723,6 +2817,18 @@ packages: simplesignal@2.1.7: resolution: {integrity: sha512-PEo2qWpUke7IMhlqiBxrulIFvhJRLkl1ih52Rwa+bPjzhJepcd4GIjn2RiQmFSx3dQvsEAgF0/lXMwMN7vODaA==} + sort-asc@0.2.0: + resolution: {integrity: sha512-umMGhjPeHAI6YjABoSTrFp2zaBtXBej1a0yKkuMUyjjqu6FJsTF+JYwCswWDg+zJfk/5npWUUbd33HH/WLzpaA==} + engines: {node: '>=0.10.0'} + + sort-desc@0.2.0: + resolution: {integrity: sha512-NqZqyvL4VPW+RAxxXnB8gvE1kyikh8+pR+T+CXLksVRN9eiQqkQlPwqWYU0mF9Jm7UnctShlxLyAt1CaBOTL1w==} + engines: {node: '>=0.10.0'} + + sort-object@3.0.3: + resolution: {integrity: sha512-nK7WOY8jik6zaG9CRwZTaD5O7ETWDLZYMM12pqY8htll+7dYeqGfEUPcUBHOpSJg2vJOrvFIY2Dl5cX2ih1hAQ==} + engines: {node: '>=0.10.0'} + source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} @@ -2734,6 +2840,10 @@ packages: resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} engines: {node: '>=0.10.0'} + split-string@3.1.0: + resolution: {integrity: sha512-NzNVhJDYpwceVVii8/Hu6DKfD2G+NrQHlS/V/qgv763EYudVwEcMQNxd2lh+0VrUByXN/oJkl5grOhYWvQUYiw==} + engines: {node: '>=0.10.0'} + sql-escaper@1.3.3: resolution: {integrity: sha512-BsTCV265VpTp8tm1wyIm1xqQCS+Q9NHx2Sr+WcnUrgLrQ6yiDIvHYJV5gHxsj1lMBy2zm5twLaZao8Jd+S8JJw==} engines: {bun: '>=1.0.0', deno: '>=2.0.0', node: '>=12.0.0'} @@ -2792,6 +2902,9 @@ packages: babel-plugin-macros: optional: true + supercluster@8.0.1: + resolution: {integrity: sha512-IiOea5kJ9iqzD2t7QJq/cREyLHTtSmUT6gQsweojg9WH2sYJqZK9SswTu6jrscO6D1G5v5vYZ9ru/eq85lXeZQ==} + supports-color@7.2.0: resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} engines: {node: '>=8'} @@ -2853,14 +2966,13 @@ packages: resolution: {integrity: sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==} engines: {node: '>=12.0.0'} + tinyqueue@3.0.0: + resolution: {integrity: sha512-gRa9gwYU3ECmQYv3lslts5hxuIa90veaEcxDYuu3QGOIAEM2mOZkVHp48ANJuu1CURtRdHKUBY5Lm1tHV+sD4g==} + to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} - topojson-client@3.1.0: - resolution: {integrity: sha512-605uxS6bcYxGXw9qi62XyrV6Q3xwbndjachmNxu8HWTtVPxZfEJN9fd/SZS1Q54Sn2y0TMyMxFj/cJINqGHrKw==} - hasBin: true - ts-api-utils@2.4.0: resolution: {integrity: sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==} engines: {node: '>=18.12'} @@ -2898,6 +3010,12 @@ packages: engines: {node: '>=14.17'} hasBin: true + typewise-core@1.2.0: + resolution: {integrity: sha512-2SCC/WLzj2SbUwzFOzqMCkz5amXLlxtJqDKTICqg30x+2DZxcfZN2MvQZmGfXWKNWaKK9pBPsvkcwv8bF/gxKg==} + + typewise@1.0.3: + resolution: {integrity: sha512-aXofE06xGhaQSPzt8hlTY+/YWQhm9P0jYUp1f2XtmW/3Bk0qzXcyFWAtPoo2uTGQj1ZwbDuSyuxicq+aDo8lCQ==} + unbox-primitive@1.1.0: resolution: {integrity: sha512-nWJ91DjeOkej/TA8pXQ3myruKpKEYgqvpw9lz4OPHj/NWFNluYrjbz9j01CJ8yKQd2g4jFoOkINCTW2I5LEEyw==} engines: {node: '>= 0.4'} @@ -2905,6 +3023,10 @@ packages: undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + union-value@1.0.1: + resolution: {integrity: sha512-tJfXmxMeWYnczCVs7XAEvIV7ieppALdyepWMkHkwciRpZraG/xwT+s2JN8+pr1+8jCRf80FFzvr+MpQeeoF4Xg==} + engines: {node: '>=0.10.0'} + unrs-resolver@1.11.1: resolution: {integrity: sha512-bSjt9pjaEBnNiGgc9rUiHGKv5l4/TGzDmYw3RhnkJGtLhbnnA/5qJj7x3dNDCRx/PJxu774LlH8lCOlB4hEfKg==} @@ -3327,6 +3449,61 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 + '@mapbox/jsonlint-lines-primitives@2.0.2': {} + + '@mapbox/point-geometry@1.1.0': {} + + '@mapbox/tiny-sdf@2.0.7': {} + + '@mapbox/unitbezier@0.0.1': {} + + '@mapbox/vector-tile@2.0.4': + dependencies: + '@mapbox/point-geometry': 1.1.0 + '@types/geojson': 7946.0.16 + pbf: 4.0.1 + + '@mapbox/whoots-js@3.1.0': {} + + '@maplibre/geojson-vt@5.0.4': {} + + '@maplibre/geojson-vt@6.0.2': + dependencies: + kdbush: 4.0.2 + + '@maplibre/maplibre-gl-style-spec@19.3.3': + dependencies: + '@mapbox/jsonlint-lines-primitives': 2.0.2 + '@mapbox/unitbezier': 0.0.1 + json-stringify-pretty-compact: 3.0.0 + minimist: 1.2.8 + rw: 1.3.3 + sort-object: 3.0.3 + + '@maplibre/maplibre-gl-style-spec@24.7.0': + dependencies: + '@mapbox/jsonlint-lines-primitives': 2.0.2 + '@mapbox/unitbezier': 0.0.1 + json-stringify-pretty-compact: 4.0.0 + minimist: 1.2.8 + quickselect: 3.0.0 + rw: 1.3.3 + tinyqueue: 3.0.0 + + '@maplibre/mlt@1.1.7': + dependencies: + '@mapbox/point-geometry': 1.1.0 + + '@maplibre/vt-pbf@4.3.0': + dependencies: + '@mapbox/point-geometry': 1.1.0 + '@mapbox/vector-tile': 2.0.4 + '@maplibre/geojson-vt': 5.0.4 + '@types/geojson': 7946.0.16 + '@types/supercluster': 7.1.3 + pbf: 4.0.1 + supercluster: 8.0.1 + '@napi-rs/wasm-runtime@0.2.12': dependencies: '@emnapi/core': 1.9.0 @@ -3605,10 +3782,6 @@ snapshots: '@types/d3-ease@3.0.2': {} - '@types/d3-geo@3.1.0': - dependencies: - '@types/geojson': 7946.0.16 - '@types/d3-interpolate@3.0.4': dependencies: '@types/d3-color': 3.1.3 @@ -3649,6 +3822,10 @@ snapshots: '@types/stats.js@0.17.4': {} + '@types/supercluster@7.1.3': + dependencies: + '@types/geojson': 7946.0.16 + '@types/three@0.173.0': dependencies: '@tweenjs/tween.js': 23.1.3 @@ -3658,15 +3835,6 @@ snapshots: fflate: 0.8.2 meshoptimizer: 0.18.1 - '@types/topojson-client@3.1.5': - dependencies: - '@types/geojson': 7946.0.16 - '@types/topojson-specification': 1.0.5 - - '@types/topojson-specification@1.0.5': - dependencies: - '@types/geojson': 7946.0.16 - '@types/webxr@0.5.24': {} '@typescript-eslint/eslint-plugin@8.57.0(@typescript-eslint/parser@8.57.0(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3))(eslint@9.39.4(jiti@2.6.1))(typescript@5.9.3)': @@ -3819,6 +3987,19 @@ snapshots: '@unrs/resolver-binding-win32-x64-msvc@1.11.1': optional: true + '@vis.gl/react-mapbox@8.1.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + + '@vis.gl/react-maplibre@8.1.0(maplibre-gl@5.20.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4)': + dependencies: + '@maplibre/maplibre-gl-style-spec': 19.3.3 + react: 19.2.4 + react-dom: 19.2.4(react@19.2.4) + optionalDependencies: + maplibre-gl: 5.20.0 + '@webgpu/types@0.1.69': {} accessor-fn@1.5.3: {} @@ -3844,6 +4025,8 @@ snapshots: aria-query@5.3.2: {} + arr-union@3.1.0: {} + array-buffer-byte-length@1.0.2: dependencies: call-bound: 1.0.4 @@ -3911,6 +4094,8 @@ snapshots: get-intrinsic: 1.3.0 is-array-buffer: 3.0.5 + assign-symbols@1.0.0: {} + ast-types-flow@0.0.8: {} async-function@1.0.0: {} @@ -3944,6 +4129,15 @@ snapshots: buffer-from@1.1.2: {} + bytewise-core@1.2.3: + dependencies: + typewise-core: 1.2.0 + + bytewise@1.1.0: + dependencies: + bytewise-core: 1.2.3 + typewise: 1.0.3 + call-bind-apply-helpers@1.0.2: dependencies: es-errors: 1.3.0 @@ -3986,8 +4180,6 @@ snapshots: color-name@1.1.4: {} - commander@2.20.3: {} - concat-map@0.0.1: {} cross-spawn@7.0.6: @@ -3998,31 +4190,16 @@ snapshots: csstype@3.2.3: {} - d3-array@2.12.1: - dependencies: - internmap: 1.0.1 - d3-array@3.2.4: dependencies: internmap: 2.0.3 - d3-color@2.0.0: {} - d3-color@3.1.0: {} d3-delaunay@6.0.4: dependencies: delaunator: 5.0.1 - d3-dispatch@2.0.0: {} - - d3-drag@2.0.0: - dependencies: - d3-dispatch: 2.0.0 - d3-selection: 2.0.0 - - d3-ease@2.0.0: {} - d3-ease@3.0.1: {} d3-format@3.1.2: {} @@ -4034,18 +4211,10 @@ snapshots: d3-geo: 3.1.1 d3-tricontour: 1.1.0 - d3-geo@2.0.2: - dependencies: - d3-array: 2.12.1 - d3-geo@3.1.1: dependencies: d3-array: 3.2.4 - d3-interpolate@2.0.1: - dependencies: - d3-color: 2.0.0 - d3-interpolate@3.0.1: dependencies: d3-color: 3.1.0 @@ -4067,8 +4236,6 @@ snapshots: d3-time: 3.1.0 d3-time-format: 4.1.0 - d3-selection@2.0.0: {} - d3-selection@3.0.0: {} d3-shape@3.2.0: @@ -4083,32 +4250,13 @@ snapshots: dependencies: d3-array: 3.2.4 - d3-timer@2.0.0: {} - d3-timer@3.0.1: {} - d3-transition@2.0.0(d3-selection@2.0.0): - dependencies: - d3-color: 2.0.0 - d3-dispatch: 2.0.0 - d3-ease: 2.0.0 - d3-interpolate: 2.0.1 - d3-selection: 2.0.0 - d3-timer: 2.0.0 - d3-tricontour@1.1.0: dependencies: d3-delaunay: 6.0.4 d3-scale: 4.0.2 - d3-zoom@2.0.0: - dependencies: - d3-dispatch: 2.0.0 - d3-drag: 2.0.0 - d3-interpolate: 2.0.1 - d3-selection: 2.0.0 - d3-transition: 2.0.0(d3-selection@2.0.0) - damerau-levenshtein@1.0.8: {} data-bind-mapper@1.0.3: @@ -4570,6 +4718,15 @@ snapshots: eventemitter3@4.0.7: {} + extend-shallow@2.0.1: + dependencies: + is-extendable: 0.1.1 + + extend-shallow@3.0.2: + dependencies: + assign-symbols: 1.0.0 + is-extendable: 1.0.1 + fast-deep-equal@3.1.3: {} fast-equals@5.4.0: {} @@ -4697,6 +4854,10 @@ snapshots: dependencies: resolve-pkg-maps: 1.0.0 + get-value@2.0.6: {} + + gl-matrix@3.4.4: {} + glob-parent@5.1.2: dependencies: is-glob: 4.0.3 @@ -4776,8 +4937,6 @@ snapshots: hasown: 2.0.2 side-channel: 1.1.0 - internmap@1.0.1: {} - internmap@2.0.3: {} intl-messageformat@11.1.2: @@ -4845,6 +5004,12 @@ snapshots: call-bound: 1.0.4 has-tostringtag: 1.0.2 + is-extendable@0.1.1: {} + + is-extendable@1.0.1: + dependencies: + is-plain-object: 2.0.4 + is-extglob@2.1.1: {} is-finalizationregistry@1.1.1: @@ -4874,6 +5039,10 @@ snapshots: is-number@7.0.0: {} + is-plain-object@2.0.4: + dependencies: + isobject: 3.0.1 + is-property@1.0.2: {} is-regex@1.2.1: @@ -4921,6 +5090,8 @@ snapshots: isexe@3.1.5: {} + isobject@3.0.1: {} + iterator.prototype@1.1.5: dependencies: define-data-property: 1.1.4 @@ -4946,6 +5117,10 @@ snapshots: json-stable-stringify-without-jsonify@1.0.1: {} + json-stringify-pretty-compact@3.0.0: {} + + json-stringify-pretty-compact@4.0.0: {} + json5@1.0.2: dependencies: minimist: 1.2.8 @@ -4961,6 +5136,8 @@ snapshots: dependencies: lodash-es: 4.17.23 + kdbush@4.0.2: {} + keyv@4.5.4: dependencies: json-buffer: 3.0.1 @@ -5055,6 +5232,28 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 + maplibre-gl@5.20.0: + dependencies: + '@mapbox/jsonlint-lines-primitives': 2.0.2 + '@mapbox/point-geometry': 1.1.0 + '@mapbox/tiny-sdf': 2.0.7 + '@mapbox/unitbezier': 0.0.1 + '@mapbox/vector-tile': 2.0.4 + '@mapbox/whoots-js': 3.1.0 + '@maplibre/geojson-vt': 6.0.2 + '@maplibre/maplibre-gl-style-spec': 24.7.0 + '@maplibre/mlt': 1.1.7 + '@maplibre/vt-pbf': 4.3.0 + '@types/geojson': 7946.0.16 + earcut: 3.0.2 + gl-matrix: 3.4.4 + kdbush: 4.0.2 + murmurhash-js: 1.0.0 + pbf: 4.0.1 + potpack: 2.1.0 + quickselect: 3.0.0 + tinyqueue: 3.0.0 + math-intrinsics@1.1.0: {} merge2@1.4.1: {} @@ -5084,6 +5283,8 @@ snapshots: ms@2.1.3: {} + murmurhash-js@1.0.0: {} + mysql2@3.19.1(@types/node@22.19.15): dependencies: '@types/node': 22.19.15 @@ -5236,6 +5437,10 @@ snapshots: path-parse@1.0.7: {} + pbf@4.0.1: + dependencies: + resolve-protobuf-schema: 2.1.0 + picocolors@1.1.1: {} picomatch@2.3.1: {} @@ -5266,6 +5471,8 @@ snapshots: picocolors: 1.1.1 source-map-js: 1.2.1 + potpack@2.1.0: {} + preact@10.29.0: {} prelude-ls@1.2.1: {} @@ -5276,10 +5483,14 @@ snapshots: object-assign: 4.1.1 react-is: 16.13.1 + protocol-buffers-schema@3.6.0: {} + punycode@2.3.1: {} queue-microtask@1.2.3: {} + quickselect@3.0.0: {} + react-dom@19.2.4(react@19.2.4): dependencies: react: 19.2.4 @@ -5301,15 +5512,14 @@ snapshots: jerrypick: 1.1.2 react: 19.2.4 - react-simple-maps@3.0.0(prop-types@15.8.1)(react-dom@19.2.4(react@19.2.4))(react@19.2.4): + react-map-gl@8.1.0(maplibre-gl@5.20.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4): dependencies: - d3-geo: 2.0.2 - d3-selection: 2.0.0 - d3-zoom: 2.0.0 - prop-types: 15.8.1 + '@vis.gl/react-mapbox': 8.1.0(react-dom@19.2.4(react@19.2.4))(react@19.2.4) + '@vis.gl/react-maplibre': 8.1.0(maplibre-gl@5.20.0)(react-dom@19.2.4(react@19.2.4))(react@19.2.4) react: 19.2.4 react-dom: 19.2.4(react@19.2.4) - topojson-client: 3.1.0 + optionalDependencies: + maplibre-gl: 5.20.0 react-smooth@4.0.4(react-dom@19.2.4(react@19.2.4))(react@19.2.4): dependencies: @@ -5377,6 +5587,10 @@ snapshots: resolve-pkg-maps@1.0.0: {} + resolve-protobuf-schema@2.1.0: + dependencies: + protocol-buffers-schema: 3.6.0 + resolve@1.22.11: dependencies: is-core-module: 2.16.1 @@ -5400,6 +5614,8 @@ snapshots: dependencies: queue-microtask: 1.2.3 + rw@1.3.3: {} + safe-array-concat@1.1.3: dependencies: call-bind: 1.0.8 @@ -5449,6 +5665,13 @@ snapshots: es-errors: 1.3.0 es-object-atoms: 1.1.1 + set-value@2.0.1: + dependencies: + extend-shallow: 2.0.1 + is-extendable: 0.1.1 + is-plain-object: 2.0.4 + split-string: 3.1.0 + sharp@0.34.5: dependencies: '@img/colour': 1.1.0 @@ -5519,6 +5742,19 @@ snapshots: simplesignal@2.1.7: {} + sort-asc@0.2.0: {} + + sort-desc@0.2.0: {} + + sort-object@3.0.3: + dependencies: + bytewise: 1.1.0 + get-value: 2.0.6 + is-extendable: 0.1.1 + sort-asc: 0.2.0 + sort-desc: 0.2.0 + union-value: 1.0.1 + source-map-js@1.2.1: {} source-map-support@0.5.21: @@ -5528,6 +5764,10 @@ snapshots: source-map@0.6.1: {} + split-string@3.1.0: + dependencies: + extend-shallow: 3.0.2 + sql-escaper@1.3.3: {} stable-hash@0.0.5: {} @@ -5598,6 +5838,10 @@ snapshots: client-only: 0.0.1 react: 19.2.4 + supercluster@8.0.1: + dependencies: + kdbush: 4.0.2 + supports-color@7.2.0: dependencies: has-flag: 4.0.0 @@ -5676,14 +5920,12 @@ snapshots: fdir: 6.5.0(picomatch@4.0.3) picomatch: 4.0.3 + tinyqueue@3.0.0: {} + to-regex-range@5.0.1: dependencies: is-number: 7.0.0 - topojson-client@3.1.0: - dependencies: - commander: 2.20.3 - ts-api-utils@2.4.0(typescript@5.9.3): dependencies: typescript: 5.9.3 @@ -5736,6 +5978,12 @@ snapshots: typescript@5.9.3: {} + typewise-core@1.2.0: {} + + typewise@1.0.3: + dependencies: + typewise-core: 1.2.0 + unbox-primitive@1.1.0: dependencies: call-bound: 1.0.4 @@ -5745,6 +5993,13 @@ snapshots: undici-types@6.21.0: {} + union-value@1.0.1: + dependencies: + arr-union: 3.1.0 + get-value: 2.0.6 + is-extendable: 0.1.1 + set-value: 2.0.1 + unrs-resolver@1.11.1: dependencies: napi-postinstall: 0.3.4 diff --git a/skill/openclaw-reporter/SKILL.md b/skill/openclaw-reporter/SKILL.md index 4a3375c..46c9aae 100644 --- a/skill/openclaw-reporter/SKILL.md +++ b/skill/openclaw-reporter/SKILL.md @@ -4,16 +4,21 @@ description: | Opt-in reporter for the OpenClaw global claw heatmap. On first use, ASKS the user for consent before registering. Sends heartbeats (platform + model only) and generic task summaries to https://kymr.top/. - Heartbeats are sent automatically via hooks at session start — no LLM action needed. - Task reports are sent automatically via hooks when the agent stops responding. + Heartbeats are sent at session start and renewed via a PostToolUse hook, rate-limited + to at most once per 3 minutes — most invocations exit immediately after a timestamp check. + The PostToolUse hook does NOT read or send any tool names, arguments, or results. + Task reports are sent automatically via a Stop hook when the agent stops responding. Data collected: user-chosen claw name, OS platform, model name, generic task category. Credentials: the server returns an apiKey on registration, which is stored locally in ~/.openclaw/config.json and sent as a Bearer token in subsequent requests. - No file paths, code, or project-specific data is ever sent. + No file paths, code, tool names, or project-specific data is ever sent. requirements: network: true writeFiles: - "~/.openclaw/config.json" + - "~/.openclaw/scripts/heartbeat.sh" + - "~/.openclaw/scripts/task-report.sh" + - "~/.openclaw/.last_heartbeat" binaries: - curl - python3 @@ -26,29 +31,34 @@ hooks: SessionStart: - hooks: - type: command - command: "if [ -f ~/.openclaw/config.json ]; then KEY=$(python3 -c \"import json; print(json.load(open('$HOME/.openclaw/config.json'))['apiKey'])\") && curl -s -o /dev/null --max-time 5 -X POST 'https://kymr.top/api/v1/heartbeat' -H 'Content-Type: application/json' -H \"Authorization: Bearer $KEY\" -d \"{\\\"platform\\\":\\\"$(uname -s | tr '[:upper:]' '[:lower:]')\\\",\\\"model\\\":\\\"${CLAUDE_MODEL:-unknown}\\\"}\"; fi" + command: "bash $HOME/.openclaw/scripts/heartbeat.sh 2>/dev/null || true" timeout: 10 - once: true + PostToolUse: + - hooks: + - type: command + command: "bash $HOME/.openclaw/scripts/heartbeat.sh 2>/dev/null || true" + timeout: 5 Stop: - hooks: - type: command - command: "if [ -f ~/.openclaw/config.json ]; then KEY=$(python3 -c \"import json; print(json.load(open('$HOME/.openclaw/config.json'))['apiKey'])\") && curl -s -o /dev/null --max-time 5 -X POST 'https://kymr.top/api/v1/task' -H 'Content-Type: application/json' -H \"Authorization: Bearer $KEY\" -d \"{\\\"summary\\\":\\\"Completed a task\\\",\\\"model\\\":\\\"${CLAUDE_MODEL:-unknown}\\\"}\"; fi" + command: "bash $HOME/.openclaw/scripts/task-report.sh 2>/dev/null || true" timeout: 10 --- # OpenClaw Reporter -This skill lets you participate in the OpenClaw global claw activity heatmap. It reports **anonymous, non-identifying** activity data to visualize worldwide AI agent usage. +This skill lets you participate in the OpenClaw global claw activity heatmap. It reports **minimal, non-code** activity data to visualize worldwide AI agent usage. Note: as with any HTTPS request, the server can observe your IP address. ## How It Works -- **Heartbeat**: Sent automatically at session start via a `SessionStart` hook — no manual action needed. +- **Heartbeat**: Sent at session start (`SessionStart` hook) and renewed periodically (`PostToolUse` hook, rate-limited to once per 3 minutes). The `PostToolUse` hook does **not** read tool names, arguments, or results — it only triggers the same timestamp-checked heartbeat script. Most invocations exit immediately after reading a single timestamp file. - **Task report**: Sent automatically when the agent finishes responding via a `Stop` hook. - **Registration**: One-time setup requiring explicit user consent (see below). +- **Hooks**: Hooks are registered in the skill frontmatter and activate when the skill is loaded. No system-level files are modified. ## Data Disclosure -This skill sends the following data to `https://kymr.top/`: +This skill sends the following data to `https://kymr.top/` (the OpenClaw Market production server; server-side source code is in this repository under `app/api/v1/`): | Data Field | Example | Purpose | |---|---|---| @@ -57,13 +67,15 @@ This skill sends the following data to `https://kymr.top/`: | Model | `claude-sonnet-4-6` | Model usage stats | | Task summary | `"Completed a task"` | Generic activity indicator | -**Stored locally:** Registration returns an `apiKey` from the server, saved in `~/.openclaw/config.json` with `chmod 600` (owner-only access). This token is sent as `Authorization: Bearer` header in heartbeat and task report requests. The endpoint URL (`https://kymr.top/`) is hardcoded in hooks — not read from the config file — so a modified config cannot redirect telemetry. The `CLAUDE_MODEL` environment variable is read (if available) to identify the active model — it falls back to `"unknown"` if not set. +**Stored locally:** Registration returns an `apiKey` from the server, saved in `~/.openclaw/config.json` with `chmod 600` (owner-only access). This token is sent as `Authorization: Bearer` header in heartbeat and task report requests. The endpoint URL (`https://kymr.top/`) is hardcoded in hook scripts — not read from the config file — so a modified config cannot redirect telemetry. The `CLAUDE_MODEL` environment variable is read (if available) to identify the active model — it falls back to `"unknown"` if not set. -**Never sent:** system usernames, file paths, code snippets, project names, secrets, IP-derived geolocation beyond what the server naturally receives. +**Visible to the server:** Your IP address is visible on every HTTPS request (this is inherent to any network call and not specific to this skill). The server uses IP for approximate geolocation on the heatmap — no more precise than city-level. + +**Never sent:** system usernames, file paths, code snippets, project names, tool names, tool arguments, tool results, or secrets. ## Configuration -Config is stored at `~/.openclaw/config.json` (permissions `600`). The hooks check for this file before sending any requests — if it doesn't exist, nothing is sent. The endpoint is hardcoded to `https://kymr.top/` in the hook commands and is **not** configurable via the config file. +Config is stored at `~/.openclaw/config.json` (permissions `600`). Hook scripts live at `~/.openclaw/scripts/`. The hooks check for the config file before sending any requests — if it doesn't exist, nothing is sent. The endpoint is hardcoded to `https://kymr.top/` in the scripts and is **not** configurable via the config file. ## First-Time Setup (Requires User Consent) @@ -73,7 +85,7 @@ If `~/.openclaw/config.json` does not exist, follow these steps: Use the AskUserQuestion tool: -- Question: "OpenClaw Reporter wants to register a claw name and send activity heartbeats to https://kymr.top/ for the global heatmap. The server will issue an API key stored locally at ~/.openclaw/config.json. No file paths, code, or project data is sent. Allow?" +- Question: "OpenClaw Reporter wants to register a claw name and send periodic heartbeats (once per 3 min, platform + model only) to https://kymr.top/ for the global heatmap. The server will issue an API key stored locally at ~/.openclaw/config.json. No file paths, code, tool names, or project data is sent. Allow?" - Options: "Yes, register" / "No, skip" **If the user selects "No, skip":** do NOT register and do NOT ask again in this session. Stop here. @@ -88,17 +100,22 @@ If the user consented, use AskUserQuestion again: Use whatever the user provides as the claw name. -### Step 3: Register +### Step 3: Register and install hooks -After receiving the name (stored as `CLAW_NAME`): +After receiving the name (stored as `CLAW_NAME`), run Step 3a and Step 3b in sequence. + +#### Step 3a: Register and save config ```bash -# Create config directory -mkdir -p ~/.openclaw +# --- OpenClaw Registration --- +set -e OPENCLAW_ENDPOINT="https://kymr.top" -CLAW_NAME="USER_CHOSEN_NAME_HERE" +export CLAW_NAME="USER_CHOSEN_NAME_HERE" +mkdir -p ~/.openclaw/scripts + +# Register with the server RESPONSE=$(curl -s -X POST "$OPENCLAW_ENDPOINT/api/v1/register" \ -H "Content-Type: application/json" \ -d "{ @@ -107,38 +124,117 @@ RESPONSE=$(curl -s -X POST "$OPENCLAW_ENDPOINT/api/v1/register" \ \"model\": \"$(echo $CLAUDE_MODEL 2>/dev/null || echo 'unknown')\" }") -# Save config (no endpoint stored — hooks use a hardcoded URL) +# Save config — pipe response via stdin to avoid shell injection +# Creates ~/.openclaw/config.json with chmod 600 (owner-only access) echo "$RESPONSE" | python3 -c " import json, sys, os -data = json.load(sys.stdin) +data = json.loads(sys.stdin.read()) config = { 'clawId': data.get('clawId', ''), 'apiKey': data.get('apiKey', ''), - 'name': '$CLAW_NAME' + 'name': os.environ.get('CLAW_NAME', '') } path = os.path.expanduser('~/.openclaw/config.json') with open(path, 'w') as f: json.dump(config, f, indent=2) os.chmod(path, 0o600) -print('Registered as:', '$CLAW_NAME') +print('Registered as:', config['name']) " ``` -## Opt-Out / Unregister - -To stop all OpenClaw reporting, delete the config file: +#### Step 3b: Deploy hook scripts ```bash -rm -f ~/.openclaw/config.json -rmdir ~/.openclaw 2>/dev/null +# --- Deploy heartbeat script (rate-limited to once per 180s) --- +cat > ~/.openclaw/scripts/heartbeat.sh << 'HOOKEOF' +#!/bin/bash +# OpenClaw heartbeat — rate-limited to once per 180 seconds. +# No tool names, arguments, or results are read or sent. +CONFIG="$HOME/.openclaw/config.json" +[ -f "$CONFIG" ] || exit 0 +LAST_FILE="$HOME/.openclaw/.last_heartbeat" +NOW=$(date +%s) +if [ -f "$LAST_FILE" ]; then + LAST=$(cat "$LAST_FILE" 2>/dev/null || echo 0) + [ $((NOW - LAST)) -lt 180 ] && exit 0 +fi +KEY=$(python3 -c "import json; print(json.load(open('$CONFIG'))['apiKey'])" 2>/dev/null) || exit 0 +[ -z "$KEY" ] && exit 0 +curl -s -o /dev/null --max-time 5 -X POST 'https://kymr.top/api/v1/heartbeat' \ + -H 'Content-Type: application/json' \ + -H "Authorization: Bearer $KEY" \ + -d "{\"platform\":\"$(uname -s | tr '[:upper:]' '[:lower:]')\",\"model\":\"${CLAUDE_MODEL:-unknown}\"}" \ + 2>/dev/null && echo "$NOW" > "$LAST_FILE" || true +HOOKEOF +chmod +x ~/.openclaw/scripts/heartbeat.sh + +# --- Deploy task report script --- +cat > ~/.openclaw/scripts/task-report.sh << 'HOOKEOF' +#!/bin/bash +CONFIG="$HOME/.openclaw/config.json" +[ -f "$CONFIG" ] || exit 0 +KEY=$(python3 -c "import json; print(json.load(open('$CONFIG'))['apiKey'])" 2>/dev/null) || exit 0 +[ -z "$KEY" ] && exit 0 +curl -s -o /dev/null --max-time 5 -X POST 'https://kymr.top/api/v1/task' \ + -H 'Content-Type: application/json' \ + -H "Authorization: Bearer $KEY" \ + -d "{\"summary\":\"Completed a task\",\"model\":\"${CLAUDE_MODEL:-unknown}\"}" 2>/dev/null || true +HOOKEOF +chmod +x ~/.openclaw/scripts/task-report.sh + +echo "--- OpenClaw setup complete ---" ``` -After deletion, no heartbeats or task reports will be sent. The hooks silently skip when the config file is absent. The skill will ask for consent again if re-enabled. +## Hook Verification (Auto-Repair) + +**Every time this skill is loaded**, if `~/.openclaw/config.json` exists, verify that hook scripts are present. Run this check silently: + +1. Check `~/.openclaw/scripts/heartbeat.sh` exists and is executable +2. Check `~/.openclaw/scripts/task-report.sh` exists and is executable + +If **any** check fails, re-run Step 3b from the registration above to redeploy the scripts. Do this silently without asking the user. + +Verification command (run silently on skill load when config exists): + +```bash +# Quick hook health check — re-deploy scripts if missing +if [ -f ~/.openclaw/config.json ]; then + NEEDS_REPAIR=false + [ -x ~/.openclaw/scripts/heartbeat.sh ] || NEEDS_REPAIR=true + [ -x ~/.openclaw/scripts/task-report.sh ] || NEEDS_REPAIR=true + + if [ "$NEEDS_REPAIR" = true ]; then + echo "OpenClaw hook scripts need repair — reinstalling..." + # (re-run Step 3b from registration above) + fi +fi +``` + +If repair is needed, re-run Step 3b (Deploy hook scripts) from the registration above. + +## Opt-Out / Unregister + +To stop all OpenClaw reporting and remove hooks: + +```bash +# Remove scripts, config, and timestamp +rm -f ~/.openclaw/scripts/heartbeat.sh ~/.openclaw/scripts/task-report.sh +rmdir ~/.openclaw/scripts 2>/dev/null +rm -f ~/.openclaw/config.json ~/.openclaw/.last_heartbeat +rmdir ~/.openclaw 2>/dev/null + +echo "OpenClaw fully uninstalled." +``` + +After deletion, no heartbeats or task reports will be sent. The skill will ask for consent again if re-enabled. ## Behavior Rules 1. **User consent is REQUIRED** before first registration — never register silently 2. If the user declines registration, respect the decision and do not ask again in the same session -3. Hooks run automatically — do NOT manually execute heartbeat or task report commands +3. Hooks run automatically via skill frontmatter — do NOT manually execute heartbeat or task report commands 4. If any OpenClaw request fails, continue normally — never interrupt the user's workflow 5. Never include identifying information in task summaries +6. **On skill load**: if `~/.openclaw/config.json` exists, silently verify hook scripts are present and redeploy if missing +7. **Idempotent**: running script deployment multiple times is safe +8. **No system-level modifications**: never write to `~/.claude/settings.json` or any file outside `~/.openclaw/` diff --git a/skill/openclaw-reporter/scripts/heartbeat.sh b/skill/openclaw-reporter/scripts/heartbeat.sh new file mode 100755 index 0000000..99b6ec8 --- /dev/null +++ b/skill/openclaw-reporter/scripts/heartbeat.sh @@ -0,0 +1,26 @@ +#!/bin/bash +# OpenClaw heartbeat — sends platform + model to heatmap server. +# Rate-limited: sends at most once per 180 seconds. +# Called by both SessionStart and PostToolUse hooks. +# No tool names, arguments, or results are read or sent. + +CONFIG="$HOME/.openclaw/config.json" +[ -f "$CONFIG" ] || exit 0 + +# --- Rate limit check (fast path: exit in <1ms) --- +LAST_FILE="$HOME/.openclaw/.last_heartbeat" +NOW=$(date +%s) +if [ -f "$LAST_FILE" ]; then + LAST=$(cat "$LAST_FILE" 2>/dev/null || echo 0) + [ $((NOW - LAST)) -lt 180 ] && exit 0 +fi + +# --- Send heartbeat --- +KEY=$(python3 -c "import json; print(json.load(open('$CONFIG'))['apiKey'])" 2>/dev/null) || exit 0 +[ -z "$KEY" ] && exit 0 + +curl -s -o /dev/null --max-time 5 -X POST 'https://kymr.top/api/v1/heartbeat' \ + -H 'Content-Type: application/json' \ + -H "Authorization: Bearer $KEY" \ + -d "{\"platform\":\"$(uname -s | tr '[:upper:]' '[:lower:]')\",\"model\":\"${CLAUDE_MODEL:-unknown}\"}" \ + 2>/dev/null && echo "$NOW" > "$LAST_FILE" || true diff --git a/skill/openclaw-reporter/scripts/task-report.sh b/skill/openclaw-reporter/scripts/task-report.sh new file mode 100755 index 0000000..b6e9393 --- /dev/null +++ b/skill/openclaw-reporter/scripts/task-report.sh @@ -0,0 +1,14 @@ +#!/bin/bash +# OpenClaw task report — Stop hook +# Sends a generic task completion signal. Fails silently. + +CONFIG="$HOME/.openclaw/config.json" +[ -f "$CONFIG" ] || exit 0 + +KEY=$(python3 -c "import json; print(json.load(open('$CONFIG'))['apiKey'])" 2>/dev/null) || exit 0 +[ -z "$KEY" ] && exit 0 + +curl -s -o /dev/null --max-time 5 -X POST 'https://kymr.top/api/v1/task' \ + -H 'Content-Type: application/json' \ + -H "Authorization: Bearer $KEY" \ + -d "{\"summary\":\"Completed a task\",\"model\":\"${CLAUDE_MODEL:-unknown}\"}" 2>/dev/null || true diff --git a/types/react-simple-maps.d.ts b/types/react-simple-maps.d.ts deleted file mode 100644 index 518eed8..0000000 --- a/types/react-simple-maps.d.ts +++ /dev/null @@ -1,57 +0,0 @@ -declare module "react-simple-maps" { - import { ComponentType, ReactNode } from "react"; - - interface ComposableMapProps { - projection?: string; - projectionConfig?: { - center?: [number, number]; - scale?: number; - rotate?: [number, number, number]; - }; - width?: number; - height?: number; - style?: React.CSSProperties; - children?: ReactNode; - } - - interface GeographiesProps { - geography: string | object; - children: (data: { - geographies: Geography[]; - }) => ReactNode; - } - - interface Geography { - rsmKey: string; - properties: Record; - } - - interface GeographyProps { - geography: Geography; - key?: string; - fill?: string; - stroke?: string; - strokeWidth?: number; - style?: { - default?: React.CSSProperties & { outline?: string }; - hover?: React.CSSProperties & { outline?: string }; - pressed?: React.CSSProperties & { outline?: string }; - }; - onClick?: () => void; - onMouseEnter?: () => void; - onMouseLeave?: () => void; - } - - interface ZoomableGroupProps { - center?: [number, number]; - zoom?: number; - minZoom?: number; - maxZoom?: number; - children?: ReactNode; - } - - export const ComposableMap: ComponentType; - export const Geographies: ComponentType; - export const Geography: ComponentType; - export const ZoomableGroup: ComponentType; -}