npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

map4ng

v1.0.7

Published

Independent Angular implementation of modern map components, inspired by mapcn.dev.

Readme

map4ng

Componentes de mapa para Angular 21+. Basado en MapLibre GL, estilizado con Tailwind CSS 4.
Inspirado en mapcn.dev. Signals nativos, standalone components, cero NgModules.


Índice

  1. Instalación
  2. Arranque rápido
  3. Referencia de API
    3.1 MapComponent (<map-gl>)
    3.2 MapControlsComponent (<map-controls>)
    3.3 MapMarkerComponent (<map-marker>)
    3.4 MarkerContentComponent (<marker-content>)
    3.5 MarkerLabelComponent (<marker-label>)
    3.6 MarkerTooltipComponent (<marker-tooltip>)
    3.7 MarkerPopupComponent (<marker-popup>)
    3.8 MapPopupComponent (<map-popup>)
    3.9 MapRouteComponent (<map-route>)
    3.10 MapArcComponent (<map-arc>)
    3.11 MapClusterLayerComponent (<map-cluster-layer>)
    3.12 MapPolygonComponent (<map-polygon>)
    3.13 MapThemeService
    3.14 injectMap()
    3.15 Tokens de inyección
  4. Sistema de temas
  5. Uso avanzado
  6. Guía de estilos
  7. Recetas
  8. Resolución de problemas

1. Instalación

Prerrequisitos

  • Angular 19+ (Angular 20 recomendado)
  • Tailwind CSS 4 — ejecuta ng add tailwindcss primero en un proyecto limpio (si se trabaja con Angular 19, es necesaria bien sea una instalación manual o correr el schematic sin haber descargado las dependencias anteriormente)

Método recomendado: ng add

El esquemático configura automáticamente dependencias y estilos:

ng add map4ng

Esto realiza automáticamente:

  1. Añade las dependencias peer al package.json:
    • maplibre-gl ^5.0.0
    • @lucide/angular ^1.14.0
    • tailwindcss ^4.0.0
    • tailwindcss-animate ^1.0.7
  2. Actualiza src/styles.css inyectando:
    • @plugin "tailwindcss-animate" (junto a @import "tailwindcss")
    • @import "map4ng/styles/lib.css" (CSS de la librería)
    • @source "../node_modules/map4ng" (escaneo de clase Tailwind)
  3. Ejecuta npm install

Resultado esperado en src/styles.css

@import "tailwindcss";
@plugin "tailwindcss-animate";
@import "map4ng/styles/lib.css";
@source "../node_modules/map4ng";

Estilos base (opcional)

Añade a src/styles.css después del contenido generado:

@layer base {
  * {
    border-color: var(--color-border, #e2e8f0);
  }
  body {
    font-family: "Inter", ui-sans-serif, system-ui, sans-serif;
    background-color: var(--color-background, #ffffff);
    color: var(--color-foreground, #020817);
    transition:
      color 300ms,
      background-color 300ms;
  }
}

Fuente Inter (opcional pero recomendado)

Añade en index.html:

<link rel="preconnect" href="https://fonts.googleapis.com" />
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet" />

Instalación manual (alternativa)

Si prefieres instalar manualmente, las peer dependencies necesarias son:

npm install map4ng maplibre-gl tailwindcss tailwindcss-animate @lucide/angular

Y debes añadir manualmente a src/styles.css:

@import "tailwindcss";
@plugin "tailwindcss-animate";
@import "map4ng/styles/lib.css";
@source "../node_modules/map4ng";

Nota: El @import de lib.css es equivalente a añadirlo en angular.json. Contiene las clases Tailwind precompiladas de la librería y los estilos base de MapLibre GL.


2. Arranque rápido

Tu primer mapa

// app.component.ts
import { Component } from "@angular/core";
import { MapComponent, MapControlsComponent } from "map4ng";

@Component({
  selector: "app-root",
  imports: [MapComponent, MapControlsComponent],
  template: `
    <div style="height: 100vh; width: 100vw;">
      <map-gl [center]="[-3.7, 40.4]" [zoom]="5">
        <map-controls [showFullscreen]="true"></map-controls>
      </map-gl>
    </div>
  `,
})
export class AppComponent {}

Mapa con marcadores

import { Component } from "@angular/core";
import { MapComponent, MapMarkerComponent, MarkerContentComponent, MarkerTooltipComponent } from "map4ng";

@Component({
  selector: "app-root",
  imports: [MapComponent, MapMarkerComponent, MarkerContentComponent, MarkerTooltipComponent],
  template: `
    <div style="height: 100vh; width: 100vw;">
      <map-gl [center]="[-0.12, 51.5]" [zoom]="6">
        <map-marker [longitude]="-0.1276" [latitude]="51.5074">
          <marker-content>
            <div class="size-4 rounded-full border-2 border-white bg-blue-500 shadow-lg"></div>
          </marker-content>
          <marker-tooltip>London</marker-tooltip>
        </map-marker>
        <map-marker [longitude]="2.3522" [latitude]="48.8566">
          <marker-content>
            <div class="size-4 rounded-full border-2 border-white bg-blue-500 shadow-lg"></div>
          </marker-content>
          <marker-tooltip>Paris</marker-tooltip>
        </map-marker>
      </map-gl>
    </div>
  `,
})
export class AppComponent {}

3. Referencia de API

3.1 MapComponent (<map-gl>)

El componente raíz. Inicializa MapLibre, gestiona temas, y provee los tokens de inyección para componentes hijos.

Selector

<map-gl></map-gl>

Modelos (two-way binding con [( )])

| Nombre | Tipo | Default | Descripción | | --------- | ------------ | -------- | -------------------------- | | center | LngLatLike | [0, 0] | Centro [longitud, latitud] | | zoom | number | 0 | Nivel de zoom | | bearing | number | 0 | Rotación (grados) | | pitch | number | 0 | Inclinación (grados) |

Estos son model() signals. Se sincronizan bidireccionalmente con el mapa: al navegar, se actualizan automáticamente. Usa [( )] o binding unidireccional + event listener.

Inputs

| Nombre | Tipo | Default | Descripción | | ------------------- | --------------------------------------------- | -------------- | --------------------------------------------------------- | | projection | ProjectionSpecification | — | Proyección. Ej: { type: 'globe' } | | theme | 'light' \| 'dark' | auto | Forzar tema. Si no se especifica, detecta automáticamente | | styles | { light?: StyleOption, dark?: StyleOption } | Carto defaults | Estilos de mapa personalizados | | loading | boolean | false | Mostrar overlay de carga | | className | string | '' | Clases CSS adicionales | | scrollZoom | boolean \| Options | true | Zoom con scroll | | dragPan | boolean \| Options | true | Arrastre con ratón | | dragRotate | boolean \| Options | true | Rotación con ratón | | touchZoomRotate | boolean \| Options | true | Zoom/rotación táctil | | doubleClickZoom | boolean | true | Zoom con doble click | | keyboard | boolean | true | Atajos de teclado | | boxZoom | boolean | true | Zoom con rectángulo | | interactive | boolean | true | Interactividad general | | renderWorldCopies | boolean | false | Múltiples copias del mundo | | mapOptions | MapOptions | {} | Opciones extra de MapLibre (minZoom, maxZoom, etc.) |

Outputs

| Nombre | Tipo | Descripción | | ------------------ | ------------- | -------------------------------- | | onViewportChange | MapViewport | Emite en cada cambio de viewport |

Types

type MapViewport = {
  center: [number, number]; // [longitude, latitude]
  zoom: number;
  bearing: number;
  pitch: number;
};

type MapStyleOption = string | StyleSpecification;
type MapRef = MapLibreMap;

Ejemplo: viewport controlado (two-way binding)

@Component({
  template: ` <map-gl [(center)]="center" [(zoom)]="zoom" [(bearing)]="bearing" [(pitch)]="pitch" (onViewportChange)="onChange($event)"></map-gl> `,
})
class Example {
  center = signal<LngLatLike>([-74.006, 40.7128]);
  zoom = signal(8);
  bearing = signal(0);
  pitch = signal(0);

  onChange(vp: MapViewport) {
    console.log("Viewport:", vp);
  }
}

También puedes escuchar solo (onViewportChange) para obtener { center, zoom, bearing, pitch } en cada movimiento.

Ejemplo: mapa 3D (globo)

<map-gl [center]="[-113, 43]" [zoom]="3.2" [projection]="{ type: 'globe' }" [pitch]="24" [mapOptions]="{ minZoom: 1.2, maxZoom: 8 }"></map-gl>

3.2 MapControlsComponent (<map-controls>)

Controles de mapa: zoom, brújula, geolocalización, pantalla completa.

Selector

<map-controls></map-controls>

Inputs

| Nombre | Tipo | Default | Descripción | | ---------------- | -------------------------------------------------------------- | ---------------- | -------------------------------- | | position | 'top-left' \| 'top-right' \| 'bottom-left' \| 'bottom-right' | 'bottom-right' | Posición en el mapa | | showZoom | boolean | true | Botones de zoom +/- | | showCompass | boolean | false | Botón de brújula (reset bearing) | | showLocate | boolean | false | Botón de geolocalización | | showFullscreen | boolean | false | Botón de pantalla completa | | className | string | '' | Clases CSS adicionales |

Outputs

| Nombre | Tipo | Descripción | | ---------- | ----------------------------------------- | ----------------------------- | | onLocate | { longitude: number, latitude: number } | Emite coordenadas del usuario |


3.3 MapMarkerComponent (<map-marker>)

Coloca un marcador en el mapa. Soporta contenido personalizado, popup, tooltip y label.

Selector

<map-marker></map-marker>

Inputs

| Nombre | Tipo | Default | Descripción | | ------------------- | ------------------------------- | ------------ | --------------------------- | | longitude | number | required | Longitud | | latitude | number | required | Latitud | | draggable | boolean | false | Permitir arrastre | | offset | PointLike | — | Desplazamiento del marcador | | rotation | number | 0 | Rotación (grados) | | rotationAlignment | 'map' \| 'viewport' \| 'auto' | 'auto' | Alineación de rotación | | pitchAlignment | 'map' \| 'viewport' \| 'auto' | 'auto' | Alineación de inclinación | | anchor | PointAnchor | 'center' | Punto de anclaje | | color | string | '#3b82f6' | Color del dot por defecto | | scale | number | 1 | Escala del dot por defecto | | className | string | '' | Clases CSS adicionales | | options | MarkerOptions | {} | Opciones extra de MapLibre |

Outputs

| Nombre | Tipo | Descripción | | -------------- | ------------------------------ | -------------------- | | onClick | MouseEvent | Click en el marcador | | onMouseEnter | MouseEvent | Ratón entra | | onMouseLeave | MouseEvent | Ratón sale | | onDragStart | { lng: number, lat: number } | Inicio de arrastre | | onDrag | { lng: number, lat: number } | Durante arrastre | | onDragEnd | { lng: number, lat: number } | Fin de arrastre |

Sub-componentes

Dentro de <map-marker> puedes usar:

  • <marker-content> — contenido visual del marcador
  • <marker-label> — etiqueta de texto
  • <marker-tooltip> — tooltip al hacer hover
  • <marker-popup> — popup al hacer click

3.4 MarkerContentComponent (<marker-content>)

Envuelve el contenido visual del marcador. Si no se proporciona, muestra un círculo azul por defecto.

Inputs

| Nombre | Tipo | Default | Descripción | | ----------- | -------- | ------- | ---------------------- | | className | string | '' | Clases CSS adicionales |


3.5 MarkerLabelComponent (<marker-label>)

Etiqueta de texto posicionada encima o debajo del marcador.

Inputs

| Nombre | Tipo | Default | Descripción | | ----------- | ------------------- | ------- | ----------------------------- | | position | 'top' \| 'bottom' | 'top' | Posición relativa al marcador | | className | string | '' | Clases CSS adicionales |


3.6 MarkerTooltipComponent (<marker-tooltip>)

Tooltip que aparece al hacer hover sobre el marcador.

Inputs

| Nombre | Tipo | Default | Descripción | | ----------- | -------------- | ------- | -------------------------- | | className | string | '' | Clases CSS adicionales | | options | PopupOptions | {} | Opciones de MapLibre Popup |

Comportamiento

  • Se muestra al hacer mouseenter en el marcador
  • Se oculta al hacer mouseleave
  • Fondo oscuro, texto claro
  • Se cierra automáticamente al hacer click en cualquier parte

3.7 MarkerPopupComponent (<marker-popup>)

Popup que se abre al hacer click en el marcador. Adjunto al marcador.

Inputs

| Nombre | Tipo | Default | Descripción | | ------------- | -------------- | ------- | --------------------------- | | closeButton | boolean | false | Mostrar botón X para cerrar | | className | string | '' | Clases CSS adicionales | | options | PopupOptions | {} | Opciones de MapLibre Popup |

Outputs

| Nombre | Tipo | Descripción | | --------- | ------ | ------------------------------- | | onClose | void | Emite cuando el popup se cierra |


3.8 MapPopupComponent (<map-popup>)

Popup independiente posicionado en coordenadas específicas. No requiere un marcador.

Selector

<map-popup></map-popup>

Inputs

| Nombre | Tipo | Default | Descripción | | ------------- | -------------- | ------------ | -------------------------- | | longitude | number | required | Longitud | | latitude | number | required | Latitud | | closeButton | boolean | false | Mostrar botón X | | className | string | '' | Clases CSS adicionales | | options | PopupOptions | {} | Opciones de MapLibre Popup |

Outputs

| Nombre | Tipo | Descripción | | --------- | ------ | ------------------------------- | | onClose | void | Emite cuando el popup se cierra |


3.9 MapRouteComponent (<map-route>)

Dibuja una línea conectando coordenadas.

Inputs

| Nombre | Tipo | Default | Descripción | | ------------- | -------------------- | ------------ | ------------------------------------ | | coordinates | [number, number][] | required | Pares [lng, lat] | | id | string | auto | Identificador de capa | | color | string | '#3b82f6' | Color de línea | | width | number | 3 | Grosor (px) | | opacity | number | 0.8 | Opacidad (0-1) | | dashArray | number[] | — | Patrón de guiones [largo, espacio] | | interactive | boolean | true | Habilitar eventos click/hover |

Outputs

| Nombre | Tipo | Descripción | | -------------- | ------ | ------------------- | | onClick | void | Ruta clickeada | | onMouseEnter | void | Ratón entra en ruta | | onMouseLeave | void | Ratón sale de ruta |


3.10 MapArcComponent (<map-arc>)

Dibuja curvas Bézier entre pares de coordenadas. Ideal para rutas aéreas, marítimas, o conexiones origen-destino.

Inputs

| Nombre | Tipo | Default | Descripción | | ------------- | --------------- | -------------------------------------------------------------- | ------------------------------------ | | data | MapArcDatum[] | required | Array de arcos | | id | string | auto | Identificador de capa | | curvature | number | 0.2 | Curvatura (0 = recta) | | samples | number | 64 | Puntos de interpolación | | paint | LinePaint | { line-color: '#4285F4', line-width: 2, line-opacity: 0.85 } | Estilo de línea | | hoverPaint | LinePaint | — | Estilo al hacer hover | | layout | LineLayout | { line-join: 'round', line-cap: 'round' } | Layout de línea | | interactive | boolean | true | Habilitar hover/click | | beforeId | string | — | ID de capa antes de la cual insertar |

Types

interface MapArcDatum {
  id: string | number;
  from: [number, number];
  to: [number, number];
  [key: string]: any; // propiedades extra usables en paint expressions
}

interface MapArcEvent<T> {
  arc: T;
  longitude: number;
  latitude: number;
  originalEvent: MapMouseEvent;
}

Outputs

| Nombre | Tipo | Descripción | | --------- | --------------------- | --------------------------- | | onClick | MapArcEvent | Arco clickeado | | onHover | MapArcEvent \| null | Hover cambió (null = salió) |

Ejemplo: arcos con color por categoría

@Component({
  template: `
    <map-gl [center]="[20, 20]" [zoom]="0.8">
      <map-arc
        [data]="lanes"
        [paint]="{
          'line-color': ['match', ['get', 'mode'], 'air', '#a78bfa', 'sea', '#34d399', '#888'],
          'line-width': 1.5,
        }"
        [hoverPaint]="{ 'line-width': 3, 'line-opacity': 1 }"
      ></map-arc>
    </map-gl>
  `,
})
class Example {
  lanes = [
    {
      id: "1",
      from: [121.4, 31.2],
      to: [-118.2, 34.0],
      mode: "sea",
      volume: "24.8k",
    },
    {
      id: "2",
      from: [151.2, -33.8],
      to: [139.6, 35.6],
      mode: "air",
      volume: "640t",
    },
  ];
}

3.11 MapClusterLayerComponent (<map-cluster-layer>)

Visualiza grandes conjuntos de datos con clustering automático.

Inputs

| Nombre | Tipo | Default | Descripción | | ------------------- | ------------------------------------- | ----------------------------------- | -------------------------------- | | data | string \| GeoJSON.FeatureCollection | required | Datos GeoJSON o URL | | clusterMaxZoom | number | 14 | Zoom máximo para clustering | | clusterRadius | number | 50 | Radio de cluster (px) | | clusterColors | [string, string, string] | ['#51bbd6', '#f1f075', '#f28cb1'] | Colores [pequeño, medio, grande] | | clusterThresholds | [number, number] | [100, 750] | Umbrales de cantidad de puntos | | pointColor | string | '#3b82f6' | Color de puntos individuales |

Outputs

| Nombre | Tipo | Descripción | | ---------------- | ---------------------------------------- | -------------------------- | | onPointClick | { feature, coordinates } | Punto individual clickeado | | onClusterClick | { clusterId, coordinates, pointCount } | Cluster clickeado |

Comportamiento por defecto

  • Click en cluster: hace zoom para expandir el cluster
  • Click en punto: emite onPointClick
  • Si onClusterClick tiene listeners, no hace zoom automático

3.12 MapPolygonComponent (<map-polygon>)

Dibuja áreas delimitadas por polígonos GeoJSON con relleno y borde configurables.

Inputs

| Nombre | Tipo | Default | Descripción | | --------------- | ---------------------- | ------------ | ------------------------------------------ | | coordinates | [number, number][][] | required | Array de anillos de coordenadas [lng, lat] | | id | string | auto | Identificador de capa | | fillColor | string | '#3b82f6' | Color de relleno | | fillOpacity | number | 0.2 | Opacidad de relleno | | strokeColor | string | '#3b82f6' | Color del borde | | strokeWidth | number | 2 | Grosor del borde (px) | | strokeOpacity | number | 0.8 | Opacidad del borde | | dashArray | number[] | — | Patrón de guiones [largo, espacio] | | interactive | boolean | true | Habilitar eventos click/hover | | beforeId | string | — | Insertar antes de otra capa |

Outputs

| Nombre | Tipo | Descripción | | -------------- | --------------- | ------------------ | | onClick | MapMouseEvent | Polígono clickeado | | onMouseEnter | MapMouseEvent | Ratón entra | | onMouseLeave | MapMouseEvent | Ratón sale |

Ejemplo: delimitar un área

<map-gl [center]="[-75.498, 10.397]" [zoom]="16" [pitch]="45">
  <map-polygon [coordinates]="[areaCoords]" fillColor="#ef4444" [fillOpacity]="0.20" strokeColor="#dc2626" [strokeWidth]="2.5"></map-polygon>
</map-gl>
areaCoords: [number, number][] = [
  [-75.4985, 10.3988], [-75.4968, 10.3988],
  [-75.4960, 10.3971], [-75.4964, 10.3963],
  [-75.4971, 10.3957], [-75.4980, 10.3954],
  [-75.4989, 10.3955], [-75.4997, 10.3961],
  [-75.4985, 10.3988], // cerrar el polígono
];

Ejemplo: múltiples áreas con dash

<map-polygon [coordinates]="[zonaA, zonaB]" fillColor="#22c55e" strokeColor="#16a34a" [dashArray]="[4, 2]"></map-polygon>

3.13 MapThemeService

Servicio providedIn: 'root' que detecta y gestiona el tema claro/oscuro.

API

@Injectable({ providedIn: "root" })
class MapThemeService {
  theme: Signal<"light" | "dark">; // Tema actual
  toggleTheme(): void; // Alternar y persistir vía clase 'dark' en <html>
  setTheme(theme: Theme): void; // Forzar tema específico
}

Funcionamiento

  1. Detecta la clase dark en document.documentElement
  2. Si no hay clase, usa prefers-color-scheme: dark
  3. Observa cambios en la clase y en la media query
  4. toggleTheme() añade/quita la clase dark

Uso

import { inject } from '@angular/core';
import { MapThemeService } from 'map4ng';

@Component({ ... })
class MyComponent {
  themeService = inject(MapThemeService);

  toggleTheme() {
    this.themeService.toggleTheme();
    // El mapa <map-gl> reacciona automáticamente al cambio de tema
  }
}

3.14 injectMap()

Función para acceder a la instancia del mapa y su estado de carga desde cualquier componente hijo de <map-gl>.

API

function injectMap(): {
  map: Signal<MapLibreMap | null>;
  isLoaded: Signal<boolean>;
};

Uso

import { Component, effect, signal, inject } from '@angular/core';
import { injectMap } from 'map4ng';

@Component({
  selector: 'app-zoom-display',
  template: `<p>Zoom: {{ zoom() }}</p>`,
})
class ZoomDisplay {
  { map, isLoaded } = injectMap();  // destructurable!
  zoom = signal(0);

  constructor() {
    effect((onCleanup) => {
      const m = this.map();
      if (!m) return;
      const handler = () => this.zoom.set(m.getZoom());
      m.on('zoom', handler);
      onCleanup(() => m.off('zoom', handler));
    });
  }
}

3.15 Tokens de inyección

Tokens subyacentes

injectMap() es un wrapper de conveniencia. También puedes inyectar los tokens directamente:

import { MAP_INSTANCE, MAP_LOADED, MARKER_INSTANCE } from "map4ng";

Tokens de inyección

| Token | Tipo | Proveído por | Descripción | | ----------------- | ------------------------ | -------------- | ------------------------------------ | | MAP_INSTANCE | Signal<Map \| null> | <map-gl> | Instancia del mapa | | MAP_LOADED | Signal<boolean> | <map-gl> | Si el estilo del mapa ha cargado | | MARKER_INSTANCE | Signal<Marker \| null> | <map-marker> | Instancia del marcador | | MAP_CONFIG | MapConfig | root | Configuración de estilos por defecto |

MAP_CONFIG

El token MAP_CONFIG define los estilos de mapa por defecto para tema claro/oscuro. Se puede sobreescribir en app.config.ts:

import { MAP_CONFIG } from "map4ng";

export const appConfig: ApplicationConfig = {
  providers: [
    {
      provide: MAP_CONFIG,
      useValue: {
        lightStyle: "https://tiles.openfreemap.org/styles/bright",
        darkStyle: "https://tiles.openfreemap.org/styles/liberty",
      },
    },
  ],
};

4. Sistema de temas

Cómo funciona

El sistema de temas de la librería usa CSS custom properties definidas en dos capas:

  1. :root — variables para tema claro
  2. .dark — variables para tema oscuro (se añade al <html>)

Las variables definidas son:

:root {
  --color-background: #ffffff;
  --color-foreground: #020817;
  --color-card: #ffffff;
  --color-card-foreground: #020817;
  --color-popover: #ffffff;
  --color-popover-foreground: #020817;
  --color-primary: #0f172a;
  --color-primary-foreground: #f8fafc;
  --color-secondary: #f1f5f9;
  --color-secondary-foreground: #0f172a;
  --color-muted: #f1f5f9;
  --color-muted-foreground: #64748b;
  --color-accent: #f1f5f9;
  --color-accent-foreground: #0f172a;
  --color-border: #e2e8f0;
  --color-ring: #3b82f6;
}

.dark {
  --color-background: #020817;
  --color-foreground: #f8fafc;
  /* ... resto de variables oscuras ... */
}

Personalizar colores

Sobrescribe las variables en tu styles.css:

@layer base {
  :root {
    --color-primary: #2563eb; /* tu azul */
    --color-background: #fafafa; /* tu fondo */
    --color-border: #d4d4d8; /* tu borde */
  }
  .dark {
    --color-primary: #3b82f6;
    --color-background: #0a0a0a;
    --color-border: #27272a;
  }
}

Cambiar tiles del mapa

<!-- OpenStreetMap -->
<map-gl [styles]="{ light: osmUrl, dark: osmUrl }"></map-gl>

<!-- MapTiler (necesita API key) -->
<map-gl [styles]="{ light: maptilerUrl, dark: maptilerDarkUrl }"></map-gl>

5. Uso avanzado

Añadir capas personalizadas

Patrón para añadir fuentes y capas GeoJSON, heatmap, etc.:

import { Component, effect, inject, OnDestroy } from "@angular/core";
import { MAP_INSTANCE, MAP_LOADED } from "map4ng";
import type { Map } from "maplibre-gl";

@Component({
  selector: "app-custom-layer",
  template: ``, // sin template
})
export class CustomLayerComponent implements OnDestroy {
  private map = inject(MAP_INSTANCE);
  private loaded = inject(MAP_LOADED);

  private sourceId = "my-source";
  private layerId = "my-layer";

  constructor() {
    effect((onCleanup) => {
      const m = this.map();
      if (!this.loaded() || !m) return;

      m.addSource(this.sourceId, {
        type: "geojson",
        data: "https://example.com/data.geojson",
      });

      m.addLayer({
        id: this.layerId,
        type: "circle",
        source: this.sourceId,
        paint: { "circle-color": "#ff0000", "circle-radius": 8 },
      });

      onCleanup(() => {
        try {
          m.removeLayer(this.layerId);
        } catch {}
        try {
          m.removeSource(this.sourceId);
        } catch {}
      });
    });
  }

  ngOnDestroy() {
    const m = this.map();
    if (m) {
      try {
        m.removeLayer(this.layerId);
      } catch {}
      try {
        m.removeSource(this.sourceId);
      } catch {}
    }
  }
}

Uso:

<map-gl [center]="[2, 40]" [zoom]="4">
  <app-custom-layer></app-custom-layer>
</map-gl>

Acceso directo al mapa

Para operaciones imperativas:

const { map } = injectMap();

// Volar a una ubicación
effect(() => {
  const m = this.map();
  m?.flyTo({ center: [lng, lat], zoom: 14, duration: 2000 });
});

// Añadir un evento de mapa
effect((onCleanup) => {
  const m = this.map();
  if (!m) return;
  const handler = (e) => console.log("click", e.lngLat);
  m.on("click", handler);
  onCleanup(() => m.off("click", handler));
});

SSR (Server-Side Rendering)

Todos los componentes verifican isPlatformBrowser() antes de interactuar con el DOM. En SSR, los componentes se renderizan como placeholders. El mapa se inicializa solo en el cliente tras la hidratación.

// No se necesita configuración especial
// El mapa se inicializa automáticamente en el navegador

6. Guía de estilos

Clases Tailwind disponibles

La librería exporta un CSS precompilado con ~200 clases Tailwind usadas por sus componentes. Las clases de color personalizadas incluyen:

| Clase | Propiedad CSS | | ------------------------- | ------------------------------------------- | | bg-background | background-color: var(--color-background) | | text-foreground | color: var(--color-foreground) | | bg-popover | background-color: var(--color-popover) | | text-popover-foreground | color: var(--color-popover-foreground) | | bg-muted | background-color: var(--color-muted) | | text-muted-foreground | color: var(--color-muted-foreground) | | bg-accent | background-color: var(--color-accent) | | border-border | border-color: var(--color-border) | | bg-primary | background-color: var(--color-primary) |

Todas las variantes (hover:, focus-visible:, dark:, disabled:) también están incluidas.

Estilizar marcadores

<!-- Dot simple -->
<marker-content>
  <div class="size-4 rounded-full border-2 border-white bg-blue-500 shadow-lg"></div>
</marker-content>

<!-- Dot con texto -->
<marker-content>
  <div class="flex size-6 items-center justify-center rounded-full border-2 border-white bg-emerald-500 text-[10px] font-bold text-white shadow-lg">1</div>
</marker-content>

<!-- Icono personalizado -->
<marker-content>
  <div class="text-2xl">📍</div>
</marker-content>

<!-- Precio (estilo inmobiliario) -->
<marker-content>
  <div class="px-2 py-1 bg-white border border-border rounded-full text-xs font-bold shadow-sm">$1.2M</div>
</marker-content>

Estilizar popups

<marker-popup [closeButton]="true" className="w-64 p-0 overflow-hidden">
  <div class="flex flex-col">
    <div class="h-32 bg-muted relative">
      <!-- imagen -->
    </div>
    <div class="p-4 flex flex-col gap-2">
      <h3 class="font-bold text-lg">Título</h3>
      <p class="text-xs text-muted-foreground">Descripción</p>
    </div>
  </div>
</marker-popup>

Layout del mapa

<!-- Pantalla completa -->
<div class="h-screen w-full">
  <map-gl ...></map-gl>
</div>

<!-- Tarjeta con altura fija -->
<div class="h-[500px] w-full border rounded-xl overflow-hidden shadow-lg">
  <map-gl ...></map-gl>
</div>

<!-- Mapa con overlay -->
<div class="relative h-[600px] w-full">
  <map-gl ...></map-gl>
  <div class="absolute top-4 left-4 z-10 bg-background/80 backdrop-blur rounded-lg border p-3 shadow">
    <!-- Controles o info -->
  </div>
</div>

7. Recetas

Mapa analytics con marcadores y tarjetas

@Component({
  imports: [MapComponent, MapMarkerComponent, MarkerContentComponent, MarkerTooltipComponent, MapControlsComponent],
  template: `
    <div class="h-[600px] w-full border rounded-2xl overflow-hidden shadow-2xl relative">
      <map-gl [center]="[-2, 16]" [zoom]="1.5" [scrollZoom]="false">
        <map-controls [showFullscreen]="true"></map-controls>
        @for (loc of locations; track loc.city) {
          <map-marker [longitude]="loc.lng" [latitude]="loc.lat">
            <marker-content>
              <div class="rounded-full bg-blue-500/70" [style.width.px]="loc.size * 3" [style.height.px]="loc.size * 3"></div>
            </marker-content>
            <marker-tooltip>
              <p class="font-medium">{{ loc.city }}</p>
              <p class="mt-0.5">{{ loc.size }} active users</p>
            </marker-tooltip>
          </map-marker>
        }
      </map-gl>
      <!-- Overlay de analytics -->
      <div class="absolute top-4 left-4 z-10 w-60 bg-background/80 backdrop-blur border rounded-xl p-4 shadow-sm">
        <p class="text-[10px] tracking-wider uppercase opacity-60">Users last 7 days</p>
        <p class="text-3xl font-semibold">3,803</p>
        <div class="mt-2 flex items-center gap-1 text-xs text-emerald-500">+12.5% vs previous 7 days</div>
      </div>
    </div>
  `,
})
class AnalyticsMap {
  locations = [
    { city: "San Francisco", lng: -122.41, lat: 37.77, size: 16 },
    { city: "New York", lng: -74.0, lat: 40.71, size: 15 },
    { city: "London", lng: -0.12, lat: 51.5, size: 14 },
    { city: "Tokyo", lng: 139.69, lat: 35.68, size: 12 },
  ];
}

Ruta de delivery con progreso

@Component({
  imports: [MapComponent, MapMarkerComponent, MarkerContentComponent, MarkerTooltipComponent, MapRouteComponent],
  template: `
    <div class="relative h-[500px] w-full border rounded-xl overflow-hidden shadow-lg">
      <map-gl [center]="[-74.006, 40.7128]" [zoom]="13" [scrollZoom]="false">
        <map-route [coordinates]="path" color="#3b82f6" [width]="5" [opacity]="0.8"></map-route>

        <!-- Tienda -->
        <map-marker [longitude]="-74.015" [latitude]="40.705">
          <marker-content>
            <div class="p-1 bg-background border-2 border-blue-500 rounded-lg shadow-md text-blue-500">🏪</div>
          </marker-content>
          <marker-tooltip>Store</marker-tooltip>
        </map-marker>

        <!-- Courier (con ping animado) -->
        <map-marker [longitude]="-73.998" [latitude]="40.715">
          <marker-content>
            <div class="relative flex items-center justify-center">
              <div class="absolute size-8 bg-blue-500/30 rounded-full animate-ping"></div>
              <div class="size-4 bg-blue-600 rounded-full border-2 border-white shadow-lg"></div>
            </div>
          </marker-content>
          <marker-tooltip>Courier: In transit</marker-tooltip>
        </map-marker>

        <!-- Destino -->
        <map-marker [longitude]="-73.985" [latitude]="40.725">
          <marker-content>
            <div class="p-1 bg-background border-2 border-orange-500 rounded-full shadow-md text-orange-500">📍</div>
          </marker-content>
          <marker-tooltip>Delivery Address</marker-tooltip>
        </map-marker>
      </map-gl>

      <div class="absolute bottom-4 left-4 bg-background/90 backdrop-blur p-3 rounded-lg border shadow-sm z-10">
        <h3 class="font-bold text-sm">Order #4292</h3>
        <p class="text-xs opacity-60">Estimated delivery: 12 mins</p>
      </div>
    </div>
  `,
})
class DeliveryTracker {
  path: [number, number][] = [
    [-74.015, 40.705],
    [-74.01, 40.71],
    [-74.005, 40.715],
    [-73.998, 40.715],
    [-73.99, 40.72],
    [-73.985, 40.725],
  ];
}

Mapa inmobiliario con popups de propiedades

@Component({
  imports: [MapComponent, MapMarkerComponent, MarkerContentComponent, MarkerTooltipComponent, MarkerPopupComponent, MapControlsComponent],
  template: `
    <div class="h-[600px] w-full border rounded-2xl overflow-hidden relative shadow-2xl">
      <map-gl [center]="[-118.24, 34.05]" [zoom]="11.5" [scrollZoom]="false">
        <map-controls position="bottom-right" [showCompass]="true"></map-controls>
        @for (p of properties; track p.id) {
          <map-marker [longitude]="p.lng" [latitude]="p.lat">
            <marker-content>
              <div class="px-2 py-1 bg-white border border-border shadow-sm rounded-full text-[10px] font-bold">
                {{ p.price }}
              </div>
            </marker-content>
            <marker-tooltip className="border shadow-lg p-2 rounded-md">
              <span class="font-bold text-xs">{{ p.price }}</span>
              <span class="text-[10px] opacity-60 ml-2">{{ p.type }} · {{ p.beds }} bds</span>
            </marker-tooltip>
            <marker-popup [closeButton]="true" className="w-64 p-0 overflow-hidden">
              <div class="flex flex-col">
                <div class="h-32 bg-muted relative">
                  <div class="absolute top-2 left-2 px-2 py-0.5 bg-white/80 backdrop-blur rounded text-[10px] font-medium uppercase">
                    {{ p.type }}
                  </div>
                </div>
                <div class="p-4 flex flex-col gap-2">
                  <div class="flex justify-between">
                    <h3 class="font-bold text-lg">{{ p.price }}</h3>
                    <div class="flex gap-2 text-xs opacity-60">
                      <span>{{ p.beds }} bds</span>
                      <span>{{ p.baths }} ba</span>
                    </div>
                  </div>
                  <p class="text-xs opacity-60">{{ p.sqft }}</p>
                </div>
              </div>
            </marker-popup>
          </map-marker>
        }
      </map-gl>
    </div>
  `,
})
class RealEstateMap {
  properties = [
    {
      id: "1",
      lat: 34.0522,
      lng: -118.2437,
      price: "$1.2M",
      type: "House",
      beds: 3,
      baths: 2,
      sqft: "2,100 sqft",
    },
    {
      id: "2",
      lat: 34.0622,
      lng: -118.2537,
      price: "$850K",
      type: "Condo",
      beds: 2,
      baths: 1,
      sqft: "1,200 sqft",
    },
    {
      id: "3",
      lat: 34.0422,
      lng: -118.2337,
      price: "$2.4M",
      type: "Modern",
      beds: 4,
      baths: 3.5,
      sqft: "3,800 sqft",
    },
  ];
}

Heatmap con capa personalizada

import { Component, effect, inject, OnDestroy } from "@angular/core";
import { MAP_INSTANCE, MAP_LOADED } from "map4ng";

const DATA_URL = "https://maplibre.org/maplibre-gl-js/docs/assets/earthquakes.geojson";

@Component({
  selector: "app-earthquake-heatmap",
  template: ``,
})
export class EarthquakeHeatmapComponent implements OnDestroy {
  private map = inject(MAP_INSTANCE);
  private loaded = inject(MAP_LOADED);

  private sourceId = "eq-source";
  private heatId = "eq-heat";
  private pointId = "eq-points";

  constructor() {
    effect((onCleanup) => {
      const m = this.map();
      if (!this.loaded() || !m) return;
      this.addLayers(m);
      onCleanup(() => this.removeLayers(m));
    });
  }

  private addLayers(m: any) {
    m.addSource(this.sourceId, { type: "geojson", data: DATA_URL });
    m.addLayer({
      id: this.heatId,
      type: "heatmap",
      source: this.sourceId,
      maxzoom: 9,
      paint: {
        "heatmap-weight": ["interpolate", ["linear"], ["get", "mag"], 0, 0, 6, 1],
        "heatmap-intensity": ["interpolate", ["linear"], ["zoom"], 0, 1, 9, 3],
        "heatmap-color": ["interpolate", ["linear"], ["heatmap-density"], 0, "rgba(33,102,172,0)", 0.2, "rgb(103,169,207)", 0.4, "rgb(209,229,240)", 0.6, "rgb(253,219,199)", 0.8, "rgb(239,138,98)", 1, "rgb(178,24,43)"],
        "heatmap-radius": ["interpolate", ["linear"], ["zoom"], 0, 2, 9, 20],
        "heatmap-opacity": ["interpolate", ["linear"], ["zoom"], 7, 1, 9, 0],
      },
    });
  }

  private removeLayers(m: any) {
    try {
      m.removeLayer(this.pointId);
    } catch {}
    try {
      m.removeLayer(this.heatId);
    } catch {}
    try {
      m.removeSource(this.sourceId);
    } catch {}
  }

  ngOnDestroy() {
    const m = this.map();
    if (m) this.removeLayers(m);
  }
}

8. Resolución de problemas

Los estilos Tailwind no se ven

Síntoma: Los controles aparecen sin fondo, texto sin color, bordes invisibles.

Causa: El CSS precompilado de la librería no se está cargando.

Solución: Verifica que src/styles.css incluya el import de la librería después de @plugin "tailwindcss-animate":

@import "tailwindcss";
@plugin "tailwindcss-animate";
@import "map4ng/styles/lib.css";
@source "../node_modules/map4ng";

Si usaste ng add map4ng, estas líneas se añaden automáticamente. Si instalaste manualmente, verifica que @import "tailwindcss" exista y que el @import de lib.css esté presente.

El mapa no se renderiza

Síntoma: Pantalla en blanco, sin mapa.

Causas posibles:

  1. maplibre-gl no está instalado (npm install maplibre-gl)
  2. El @import "map4ng/styles/lib.css" no está en src/styles.css
  3. El componente no se importó correctamente (verifica los imports en tu componente)
  4. No hay altura definida en el contenedor

Solución:

<!-- Asegúrate de dar altura explícita -->
<div style="height: 100vh; width: 100vw;">
  <map-gl ...></map-gl>
</div>

Los marcadores no aparecen

Síntoma: El mapa se ve pero los marcadores no.

Causas:

  1. Latitud/Longitud intercambiadas (primero va lng, luego lat)
  2. El mapa aún no ha cargado (los marcadores se añaden cuando el mapa está listo)
  3. Coordenadas fuera del viewport visible

Solución:

<!-- Correcto: [longitude, latitude] -->
<map-marker [longitude]="-74.006" [latitude]="40.7128">
  <!-- Incorrecto: latitud primero -->
  <map-marker [longitude]="40.7128" [latitude]="-74.006"></map-marker
></map-marker>

Tooltip aparece como línea vertical

Síntoma: El tooltip se muestra pero es extremadamente estrecho.

Causa: El contenido del tooltip colapsa.

Solución: Ya corregido en la librería (v1.0.0+). Si persiste, añade className="whitespace-nowrap" al <marker-tooltip>.

Error "maplibre-gl is not ESM"

Síntoma: Warning de build: CommonJS or AMD dependencies can cause optimization bailouts.

Causa: maplibre-gl se distribuye como CJS/AMD.

Solución: Es un warning inofensivo. No afecta al funcionamiento. Para eliminarlo, añade a angular.json:

"build": {
  "options": {
    "allowedCommonJsDependencies": ["maplibre-gl"]
  }
}

La fuente Inter no carga

Síntoma: El texto usa la fuente sans-serif del sistema.

Solución: Añade el link de Google Fonts en src/index.html:

<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&display=swap" rel="stylesheet" />

Y añade a src/styles.css:

body {
  font-family: "Inter", ui-sans-serif, system-ui, sans-serif;
}

Resumen de componentes

| Selector | Componente | Descripción | | --------------------- | -------------------------- | ------------------------------------ | | <map-gl> | MapComponent | Contenedor principal del mapa | | <map-controls> | MapControlsComponent | Zoom, brújula, localizar, fullscreen | | <map-marker> | MapMarkerComponent | Marcador en coordenadas | | <marker-content> | MarkerContentComponent | Contenido visual del marcador | | <marker-popup> | MarkerPopupComponent | Popup al hacer click en marcador | | <marker-tooltip> | MarkerTooltipComponent | Tooltip al hover en marcador | | <marker-label> | MarkerLabelComponent | Etiqueta de texto del marcador | | <map-popup> | MapPopupComponent | Popup standalone en coordenadas | | <map-route> | MapRouteComponent | Línea entre coordenadas | | <map-arc> | MapArcComponent | Curva Bézier entre pares de puntos | | <map-cluster-layer> | MapClusterLayerComponent | Capa de clustering GeoJSON | | <map-polygon> | MapPolygonComponent | Polígono GeoJSON con relleno y borde |

| Servicio/Función | Descripción | | ----------------- | -------------------------------------------- | | MapThemeService | Servicio de detección y toggle de tema | | injectMap() | Acceso reactivo a la instancia del mapa | | MAP_INSTANCE | Token de inyección: instancia del mapa | | MAP_LOADED | Token de inyección: estado de carga | | MARKER_INSTANCE | Token de inyección: instancia del marcador | | MAP_CONFIG | Token de inyección: configuración de estilos |