Token导航 LogoToken导航

Agent Skill · 前端设计

maplibre-react(maplibre React 前端)

用于辅助前端页面、组件、样式和交互逻辑的开发与维护。它适合让 Agent 生成或审查 React、Next.js、Vue、Tailwind、CSS 等相关代码,整理组件结构,或定位布局和性能问题。使用时需要结合项目现有设计系统、路由和构建方式,避免只生成孤立片段;涉及页面改动时,应配合本地预览和构建检查确认视觉效果。

maplibre-react 是前端设计类 Agent Skill,可用宿主为 Codex、Claude、Cursor、Gemini CLI,通过GitHub 仓库获取,权限为权限未标注;许可证信息还不明确,第三方扫描已通过,按安装量在前端设计类的 10,216 个 Skill 里排第 5,067。

总安装 745周安装 32GitHub Stars 公开资料未说明下载 261核验 2026-05-01

安装说明

本站只整理中文说明,不托管安装包,也不代用户安装。
GitHub
许可证
unknown

安装方式

通过对话安装

复制提示词发给支持本地命令或 Skills 的 AI 助手,先确认命令和权限,再让它执行。

请帮我安装这个 Agent Skill:maplibre-react(maplibre React 前端)
来源仓库:https://github.com/ohall/thesituation
仓库路径:skills/maplibre-react
安装命令:
npx skills add https://github.com/ohall/thesituation --skill maplibre-react
安装前请先检查当前环境是否支持对应 CLI,并向我确认将要执行的命令、安装目录、联网范围和文件读写权限;确认后再执行。

命令行安装

复制命令到本机终端执行。该命令会通过 npx skills 从第三方来源获取 Skill;本站只展示命令,不托管安装包,也不自动执行。

skills.shnpx skills
npx skills add https://github.com/ohall/thesituation --skill maplibre-react

这个 Skill 是做什么的

前端设计类 Skill 关注页面结构、视觉规范和组件写法,输出通常是可运行的界面代码。

用于辅助前端页面、组件和样式逻辑的开发与维护。支持 React、Next.js、Vue 等框架相关代码生成与审查。通过 npx skills add 命令从 GitHub 仓库安装使用。需结合项目现有设计系统和构建方式,避免生成孤立片段。

maplibre-react 属于前端设计类 Skill,可作为该场景下的辅助能力补充。

来源仓库内路径:skills/maplibre-react

上手步骤

4 步
  1. 1

    确认自己的宿主工具

    公开资料标注支持 Codex、Claude、Cursor、Gemini CLI;先确认自己用的是其中之一,再按对应工具的 Skills 目录放置。

  2. 2

    拿到安装命令

    可以直接复制的命令是 npx skills add https://github.com/ohall/thesituation --skill maplibre-react;命令里的占位符换成自己的路径或密钥再执行。

  3. 3

    安装后重启宿主

    命令执行完要完全退出宿主工具再打开,让新装的 Skill 被重新扫描到,否则列表里可能看不到。

  4. 4

    按最小权限试用

    权限没有明确标注,第一次使用时只给一个无关紧要的任务,观察它到底读了哪些文件和网络。

常见问题

6 条

maplibre-react 怎么安装?

在终端执行 npx skills add https://github.com/ohall/thesituation --skill maplibre-react。装完完全退出宿主工具再打开,让它重新扫描本地 Skill。

maplibre-react 支持哪些 AI 工具?

公开资料标注支持 Codex、Claude、Cursor、Gemini CLI。其他工具只要能读取本地 Skill 目录,一般也能手动放进去,但要自己确认触发方式是否一致。

maplibre-react 需要什么权限?

权限级别没有明确标注。第一次使用时建议只给无关紧要的任务,观察它实际读写了哪些内容。

maplibre-react 是免费的吗?

公开资料没有标注明确许可证,收费和商用限制都无法确认。正式使用前请到来源仓库核对授权说明。

maplibre-react 用的人多吗?

公开数据是累计安装 745 次,近一周安装 32 次,下载 261 次。这些数字来自第三方目录统计,只能当作热度参考,不代表质量或安全性。

maplibre-react 安全吗?

第三方扫描结果为通过,但这只覆盖已知的静态检查项,不能替代自己读一遍源码和安装命令。

值得选的地方和要先想清楚的地方

值得选的地方

  • 同时适配 4 种工具,换客户端不用重新找替代品。
  • 第三方扫描已通过,常见的高风险行为没有命中。

先想清楚的地方

  • 许可证没有明确标注,能不能商用、能不能再分发都不确定。
  • 第三方目录整理,版本更新和行为细节以来源仓库为准。

适不适合你

更适合

  • 已经在用 Codex,想直接扩展它能力的人。
  • 需要处理前端设计类任务、又不想每次都手写提示词的人。

不太适合

  • 需要明确授权才能用到公司项目里的场景。
  • 需要厂商级技术支持和 SLA 的生产系统。

前端设计类 Skill 对比

6 个同类
Skill总安装GitHub 星权限许可证扫描
frontend-design

前端设计 · Codex、Claude

8,823,360126,072只读MIT通过
vercel-composition-patterns

Vercel 组合模式 · Codex、Claude

3,858,81625,893需要联网MIT通过
vercel-react-native-skills

Vercel React native skills 前端 · Codex、Claude

2,468,66425,878只读MIT通过
adapt

adapt 搜索 · Codex、Claude

2,027,04023,125操作浏览器未明确通过
bolder

bolder 分析 · Codex、Claude

2,004,28823,130只读未明确通过
maplibre-react

maplibre React 前端 · 本页

745—权限未标注未明确通过

按安装量取同分类前几名;数字来自第三方目录统计,只作热度参考,不代表质量结论。

SKILL.md 原文

MapLibre GL JS in React

Basic Map Component

// src/components/map/TacticalMap.tsx
'use client';

import { useEffect, useRef, useCallback } from 'react';
import maplibregl from 'maplibre-gl';
import 'maplibre-gl/dist/maplibre-gl.css';
import { useIncidentStore } from '@/stores/incidents';

const NYC_CENTER: [number, number] = [-73.98, 40.75];
const DEFAULT_ZOOM = 11;

export function TacticalMap() {
  const mapContainer = useRef<HTMLDivElement>(null);
  const map = useRef<maplibregl.Map | null>(null);
  const { incidents, selectIncident } = useIncidentStore();

  // Initialize map once
  useEffect(() => {
    if (!mapContainer.current || map.current) return;

    map.current = new maplibregl.Map({
      container: mapContainer.current,
      style: '/map-style-dark.json',
      center: NYC_CENTER,
      zoom: DEFAULT_ZOOM,
      maxZoom: 18,
      minZoom: 8,
    });

    map.current.on('load', () => {
      setupSources(map.current!);
      setupLayers(map.current!);
      setupEventHandlers(map.current!);
    });

    // Cleanup
    return () => {
      map.current?.remove();
      map.current = null;
    };
  }, []);

  // Update incidents when data changes
  useEffect(() => {
    if (!map.current?.isStyleLoaded()) return;

    const source = map.current.getSource('incidents') as maplibregl.GeoJSONSource;
    if (source) {
      source.setData(incidentsToGeoJSON(incidents));
    }
  }, [incidents]);

  return (
    <div
      ref={mapContainer}
      className="w-full h-full"
      style={{ background: '#0a0a0f' }}
    />
  );
}

GeoJSON Conversion

// src/lib/geo.ts
import type { Incident } from '@/types/incidents';

interface GeoJSONFeature {
  type: 'Feature';
  geometry: {
    type: 'Point';
    coordinates: [number, number];
  };
  properties: {
    id: string;
    title: string;
    category: string;
    severity: string;
    eventTime: string;
  };
}

export function incidentsToGeoJSON(incidents: Incident[]): GeoJSON.FeatureCollection {
  const features: GeoJSONFeature[] = incidents
    .filter(i => i.location)
    .map(incident => ({
      type: 'Feature',
      geometry: incident.location!,
      properties: {
        id: incident.id,
        title: incident.title,
        category: incident.category,
        severity: incident.severity,
        eventTime: incident.eventTime.toISOString(),
      },
    }));

  return {
    type: 'FeatureCollection',
    features,
  };
}

Source Setup with Clustering

function setupSources(map: maplibregl.Map) {
  map.addSource('incidents', {
    type: 'geojson',
    data: { type: 'FeatureCollection', features: [] },
    cluster: true,
    clusterMaxZoom: 14, // Cluster until zoom 14
    clusterRadius: 50, // Pixel radius for clustering
    clusterProperties: {
      // Aggregate severity counts per cluster
      critical: ['+', ['case', ['==', ['get', 'severity'], 'critical'], 1, 0]],
      high: ['+', ['case', ['==', ['get', 'severity'], 'high'], 1, 0]],
    },
  });
}

Layer Setup

// Severity color mapping
const SEVERITY_COLORS = {
  critical: '#ff2d55',
  high: '#ff6b35',
  moderate: '#ffb800',
  low: '#00d4ff',
  info: '#a1a1aa',
};

function setupLayers(map: maplibregl.Map) {
  // Cluster circles
  map.addLayer({
    id: 'clusters',
    type: 'circle',
    source: 'incidents',
    filter: ['has', 'point_count'],
    paint: {
      'circle-color': [
        'case',
        ['>', ['get', 'critical'], 0], SEVERITY_COLORS.critical,
        ['>', ['get', 'high'], 0], SEVERITY_COLORS.high,
        SEVERITY_COLORS.moderate,
      ],
      'circle-radius': [
        'step',
        ['get', 'point_count'],
        20,  // 20px for < 10
        10, 30, // 30px for 10-49
        50, 40, // 40px for 50+
      ],
      'circle-opacity': 0.8,
      'circle-stroke-width': 2,
      'circle-stroke-color': '#ffffff',
    },
  });

  // Cluster count labels
  map.addLayer({
    id: 'cluster-count',
    type: 'symbol',
    source: 'incidents',
    filter: ['has', 'point_count'],
    layout: {
      'text-field': '{point_count_abbreviated}',
      'text-font': ['Open Sans Bold'],
      'text-size': 14,
    },
    paint: {
      'text-color': '#ffffff',
    },
  });

  // Individual incident points
  map.addLayer({
    id: 'incidents-point',
    type: 'circle',
    source: 'incidents',
    filter: ['!', ['has', 'point_count']],
    paint: {
      'circle-color': [
        'match',
        ['get', 'severity'],
        'critical', SEVERITY_COLORS.critical,
        'high', SEVERITY_COLORS.high,
        'moderate', SEVERITY_COLORS.moderate,
        'low', SEVERITY_COLORS.low,
        SEVERITY_COLORS.info,
      ],
      'circle-radius': 8,
      'circle-stroke-width': 2,
      'circle-stroke-color': '#ffffff',
    },
  });

  // Pulsing animation for critical incidents
  map.addLayer({
    id: 'incidents-pulse',
    type: 'circle',
    source: 'incidents',
    filter: ['all',
      ['!', ['has', 'point_count']],
      ['==', ['get', 'severity'], 'critical'],
    ],
    paint: {
      'circle-color': SEVERITY_COLORS.critical,
      'circle-radius': 16,
      'circle-opacity': 0.3,
    },
  });
}

Event Handlers

function setupEventHandlers(map: maplibregl.Map) {
  // Change cursor on hover
  map.on('mouseenter', 'incidents-point', () => {
    map.getCanvas().style.cursor = 'pointer';
  });

  map.on('mouseleave', 'incidents-point', () => {
    map.getCanvas().style.cursor = '';
  });

  // Click handler for incidents
  map.on('click', 'incidents-point', (e) => {
    if (!e.features?.length) return;

    const feature = e.features[0];
    const id = feature.properties?.id;

    if (id) {
      // Update store
      useIncidentStore.getState().selectIncident(id);

      // Center on incident
      const coords = (feature.geometry as GeoJSON.Point).coordinates as [number, number];
      map.flyTo({ center: coords, zoom: 15 });
    }
  });

  // Click handler for clusters
  map.on('click', 'clusters', async (e) => {
    const features = map.queryRenderedFeatures(e.point, { layers: ['clusters'] });
    if (!features.length) return;

    const clusterId = features[0].properties?.cluster_id;
    const source = map.getSource('incidents') as maplibregl.GeoJSONSource;

    const zoom = await source.getClusterExpansionZoom(clusterId);
    const coords = (features[0].geometry as GeoJSON.Point).coordinates as [number, number];

    map.flyTo({ center: coords, zoom });
  });
}

Popup Component

// src/components/map/IncidentPopup.tsx
'use client';

import { useEffect, useRef } from 'react';
import maplibregl from 'maplibre-gl';
import { createRoot } from 'react-dom/client';
import type { Incident } from '@/types/incidents';

interface Props {
  map: maplibregl.Map;
  incident: Incident;
  onClose: () => void;
}

export function IncidentPopup({ map, incident, onClose }: Props) {
  const popupRef = useRef<maplibregl.Popup | null>(null);

  useEffect(() => {
    if (!incident.location) return;

    const container = document.createElement('div');
    const root = createRoot(container);

    root.render(
      <div className="p-3 min-w-[200px]">
        <div className="flex items-center gap-2 mb-2">
          <span className={`w-2 h-2 rounded-full bg-severity-${incident.severity}`} />
          <span className="text-xs uppercase text-text-secondary">
            {incident.category}
          </span>
        </div>
        <h3 className="font-medium text-sm mb-1">{incident.title}</h3>
        {incident.locationText && (
          <p className="text-xs text-text-secondary">{incident.locationText}</p>
        )}
        <p className="text-xs text-text-muted mt-2">
          {new Date(incident.eventTime).toLocaleTimeString()}
        </p>
      </div>
    );

    popupRef.current = new maplibregl.Popup({
      closeButton: true,
      closeOnClick: false,
      className: 'incident-popup',
    })
      .setLngLat(incident.location.coordinates as [number, number])
      .setDOMContent(container)
      .addTo(map);

    popupRef.current.on('close', onClose);

    return () => {
      root.unmount();
      popupRef.current?.remove();
    };
  }, [map, incident, onClose]);

  return null;
}

Popup Styles

/* src/app/globals.css */
.incident-popup .maplibregl-popup-content {
  background: var(--bg-secondary);
  border: 1px solid var(--bg-tertiary);
  border-radius: 8px;
  padding: 0;
  color: var(--text-primary);
  box-shadow: 0 4px 20px rgba(0, 0, 0, 0.5);
}

.incident-popup .maplibregl-popup-tip {
  border-top-color: var(--bg-secondary);
}

.incident-popup .maplibregl-popup-close-button {
  color: var(--text-secondary);
  font-size: 18px;
  padding: 4px 8px;
}

Dark Tactical Map Style

// public/map-style-dark.json
{
  "version": 8,
  "name": "Tactical Dark",
  "sources": {
    "protomaps": {
      "type": "vector",
      "url": "pmtiles:///tiles/nyc.pmtiles"
    }
  },
  "layers": [
    {
      "id": "background",
      "type": "background",
      "paint": { "background-color": "#0a0a0f" }
    },
    {
      "id": "water",
      "type": "fill",
      "source": "protomaps",
      "source-layer": "water",
      "paint": { "fill-color": "#12121a" }
    },
    {
      "id": "roads",
      "type": "line",
      "source": "protomaps",
      "source-layer": "roads",
      "paint": {
        "line-color": "#1a1a24",
        "line-width": 1
      }
    },
    {
      "id": "buildings",
      "type": "fill",
      "source": "protomaps",
      "source-layer": "buildings",
      "paint": {
        "fill-color": "#15151f",
        "fill-opacity": 0.5
      }
    }
  ]
}

Fit Bounds to Incidents

function fitToIncidents(map: maplibregl.Map, incidents: Incident[]) {
  const validIncidents = incidents.filter(i => i.location);
  if (validIncidents.length === 0) return;

  const bounds = new maplibregl.LngLatBounds();

  validIncidents.forEach(incident => {
    bounds.extend(incident.location!.coordinates as [number, number]);
  });

  map.fitBounds(bounds, {
    padding: 50,
    maxZoom: 15,
    duration: 1000,
  });
}

Animate to Location

function flyToIncident(map: maplibregl.Map, incident: Incident) {
  if (!incident.location) return;

  map.flyTo({
    center: incident.location.coordinates as [number, number],
    zoom: 16,
    duration: 1500,
    essential: true,
  });
}

Get Visible Bounds

function getVisibleBbox(map: maplibregl.Map): [number, number, number, number] {
  const bounds = map.getBounds();
  return [
    bounds.getWest(),  // sw_lng
    bounds.getSouth(), // sw_lat
    bounds.getEast(),  // ne_lng
    bounds.getNorth(), // ne_lat
  ];
}

Performance Tips

  1. Use clustering — essential for 1000+ points
  2. Debounce updates — don't update source on every state change
  3. Limit features — query with viewport bounds
  4. Use WebGL layers — avoid HTML markers for many points
  5. Simplify geometries — reduce polygon complexity server-side

平台分布

Codex

39.26%
按下载量换算102

Claude

28.11%
按下载量换算73

Cursor

17.97%
按下载量换算47

Gemini CLI

8.73%
按下载量换算23

安全审计

Gen Agent Trust Hub

通过

Socket

通过

Snyk

通过

权限和风险

权限未标注

当前来源未能明确判断权限范围,默认进入异常复核队列。

安装前确认

本站仅展示第三方公开信息,不托管安装包,不提供自动安装或运行环境。安装前应自行审查源码、依赖和命令行为。目前只有一个公开来源,建议同时核对源仓库。

继续浏览同类 Skills