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

@e-llm-studio/federated-map-view

v0.0.3

Published

Reusable, backend-agnostic Google Map component with modular map features (search, layers, markers, polygon drawing).

Downloads

85

Readme

🗺️ @e-llm-studio/federated-map-view

A reusable, backend-agnostic Google Map component for React. It renders the map and all map-only UI (search, layer switching, markers, info windows, predefined boundaries, custom-boundary drawing, and a filter drawer) and emits events — it never fetches data or persists anything. Your app owns the data and reacts to callbacks.


Table of Contents

  1. Overview
  2. Features
  3. Installation
  4. Quick Start
  5. FederatedMapView Props
  6. Feature Guides
  7. Pass-through DOM Attributes (elementAttrs)
  8. TypeScript & Generics
  9. Advanced: useBoundaryDrawing
  10. Exported Utilities & Constants
  11. Local Development
  12. Design Principles
  13. License

Overview

FederatedMapView is the host component: it owns the GoogleMap instance and all map-only UI state, then exposes a small, declarative props API. Every piece of data (markers, boundaries, filter categories) comes in through props, and every user action goes back out through callbacks — so the component drops into any backend/state setup without change.

Map features are organized as self-contained modules under src/features/ (e.g. polygon-drawing), which keeps each feature's UI, styles, and types together and easy to extend.


Features

  • 🔍 Places search — Google Places autocomplete that pans the map to the picked place
  • 🗂️ Layer switching — roadmap / satellite / terrain / hybrid + custom dark & light styles
  • 📍 Markers + info windows — labelled pins with async-resolvable HTML info windows, a "View details" hook, and a highlight ring for a located pin
  • 🧲 Marker collision — an optional Advanced-Markers mode that thins out overlapping pins at low zoom and reveals them on zoom-in (needs a vector Map ID)
  • 🎯 Click-to-select — a controlled "selected point" marker for pick-a-location flows
  • 🟩 Predefined boundaries — render read-only polygons or a GeoJSON URL the component fetches itself (with optional fitBounds), plus a secondary highlight layer
  • ✏️ Custom boundary drawing — draw polygons with a live rubber-band, undo/redo, vertex/midpoint reshape handles, whole-shape drag, explicit save, batch save, containment + self-intersection guards
  • 🗃️ Manage Custom Boundaries — a slide-out sidebar to search, edit, and delete saved boundaries
  • 🧰 "Search By" filter drawer — accordion of consumer-driven categories with server-side pagination, debounced search, skeleton loading, and a "Show on Map" mode picker
  • 🔀 "View Permits" toggle — show/hide permit markers from a switch over the map (controlled or uncontrolled)
  • 📋 Permits list panel — an exported PermitsListPanel the host opens from the summary pill's "View" button (the map stays UI-agnostic — it just reports the click)
  • 📡 Viewport eventsonBoundsChanged on map idle so you can fetch data for the visible area
  • 🏷️ Pass-through DOM attributes — inject id / data-* onto specific controls via elementAttrs (handy for E2E tests or UI-driving agents)
  • 🧩 Fully generic — data-carrying shapes are generic over your record type T; no app-specific type leaks into the library
  • 📦 Zero bundled assets — inline SVG / icon-font glyphs; image icons come in via *IconUrl props

Installation

npm install @e-llm-studio/federated-map-view
# or
yarn add @e-llm-studio/federated-map-view

Peer Dependencies

The consuming app must install and provide these:

| Package | Used for | | --- | --- | | react, react-dom | React runtime (>=16.8, <19) | | @react-google-maps/api | Google Maps JS loader + map primitives | | lucide-react | Toolbar / popup glyphs | | primereact + primeicons | Filter drawer + Manage Custom Boundaries controls |

npm install @react-google-maps/api lucide-react primereact primeicons

PrimeReact Setup (required)

The filter drawer and the Manage Custom Boundaries panel are built on PrimeReact, so your app must initialize PrimeReact once at its root — wrap the tree in PrimeReactProvider and import a theme. Skipping this makes PrimeReact's overlay components throw Cannot read properties of undefined (reading 'hideOverlaysOnDocumentScrolling') on scroll.

import { PrimeReactProvider } from "primereact/api";
import "primereact/resources/themes/lara-light-blue/theme.css"; // any theme
import "primereact/resources/primereact.min.css";
import "primeicons/primeicons.css";

export default function Root() {
  return (
    <PrimeReactProvider>
      <App /> {/* renders <FederatedMapView /> somewhere inside */}
    </PrimeReactProvider>
  );
}

You also need a Google Maps JavaScript API key with the Maps JavaScript API and Places API enabled, and the key's referrer restrictions must allow your app's origin.


Quick Start

import { useMemo, useState } from "react";
import {
  FederatedMapView,
  type MapMarker,
  type MapBoundary,
  type BoundaryDraft,
} from "@e-llm-studio/federated-map-view";

// Your own record type — carried through the map and echoed back in callbacks.
type Permit = { permitNumber: string; status: string };

const markers: MapMarker<Permit>[] = [
  {
    id: 1,
    position: { lat: 40.7484, lng: -73.9857 },
    label: "Empire State",
    data: { permitNumber: "P-1001", status: "Approved" },
  },
];

export default function MapScreen() {
  const [boundaries, setBoundaries] = useState<MapBoundary<Permit>[]>([]);

  const drawing = useMemo(
    () => ({
      enabled: true,
      boundaries,
      // A new boundary was saved — assign an id and persist it (return the id).
      onBoundarySave: (draft: BoundaryDraft) => {
        const id = `b-${Date.now()}`;
        setBoundaries((prev) => [...prev, { id, ...draft }]);
        return id;
      },
      onBoundaryDelete: (id: string | number) =>
        setBoundaries((prev) => prev.filter((b) => b.id !== id)),
    }),
    [boundaries]
  );

  return (
    // Give the map a real height — it fills its container.
    <div style={{ height: "100vh", width: "100%" }}>
      <FederatedMapView<Permit>
        mapConfig={{
          googleMapsApiKey: GOOGLE_MAPS_API_KEY, // from your own env/config
          center: { lat: 40.7549, lng: -73.984 },
          zoom: 13,
        }}
        markers={markers}
        drawing={drawing}
        infoWindow={{
          enabled: true,
          resolveContent: (m) =>
            `<strong>${m.label}</strong><br/>Permit: ${m.data?.permitNumber}`,
        }}
        actions={{
          onMarkerClick: (m) => console.log("marker", m.id),
          onBoundsChanged: (b) => console.log("viewport", b),
        }}
      />
    </div>
  );
}

Height mattersFederatedMapView fills its parent. A parent with 0 height renders an invisible map.


FederatedMapView Props

The component is generic over T (your record type). Only mapConfig is required.

| Prop | Type | Required | Description | | --- | --- | --- | --- | | mapConfig | MapConfig | ✅ | API key, center/zoom, region, libraries, and API version. See Map Configuration. | | search | SearchConfig | – | Places search box (on by default). | | layer | LayerConfig | – | Base-map layer switcher (on by default). | | markers | MapMarker<T>[] | – | Pins to render (with labels + payloads). | | selectedPoint | SelectedPoint | – | A single controlled marker for click-to-select flows. | | highlightedMarkerId | string \| number \| null | – | Emphasize one marker (raised + highlight ring). Clear with undefined/null. | | predefinedBoundaries | PredefinedBoundaries | – | Read-only polygons or a GeoJSON URL to fetch & draw. | | highlightedBoundaries | MapPolygon<T>[] | – | Secondary polygon layer drawn above predefinedBoundaries (blue by default). | | infoWindow | InfoWindowConfig<T> | – | Marker info-window content resolver. | | drawing | BoundaryDrawingConfig<T> | – | Custom-boundary drawing + Manage panel. | | filters | FiltersConfig | – | The "Search By" filter drawer. | | permitSummary | PermitSummaryConfig | – | Bottom-center summary pill; "View" reports a click (host renders the list). | | permitsToggle | PermitsToggleConfig | – | "View Permits" switch that shows/hides the markers. | | actions | MapActions<T> | – | Map/marker event callbacks. | | config | MapViewConfig | – | Map controls, markerMode (collision), container className. | | loadingFallback | ReactNode | – | Shown while the Maps script loads (default Loading...). |


Feature Guides

1. Map Configuration (mapConfig)

| Field | Type | Description | | --- | --- | --- | | googleMapsApiKey | string | Required. Your Maps JS API key. | | center | LatLng | Initial center. Defaults to the library's defaultCenter. | | zoom | number | Initial zoom. Defaults to DEFAULT_ZOOM. | | region | string | Localization/biasing country code — does not move the map. | | libraries | ("places" \| "drawing" \| "geometry" \| "visualization" \| "marker")[] | Extra Maps libraries. Defaults to ["places", "marker"]. | | mapId | string | Vector Map ID. Required for marker collision (see Marker Collision). Makes the map a vector map — Google then ignores the JS styles, so dark/light styling must come from cloud styling on the Map ID. | | version | string | Maps API version ("weekly", "quarterly", or e.g. "3.58"). |

The Maps loader is a global singleton. If your app mounts more than one useJsApiLoader (e.g. a second FederatedMapView), every call must pass the same mapConfig — libraries and mapId included — or Google throws "Loader must not be called again with different options".

2. Search (search)

search={{
  enabled: true,
  placeholder: "Search a location",
  onPlaceSelected: (place) => console.log(place.formatted_address),
}}

The component pans to the selected place; you decide what else to do. Requires the places library (loaded by default).

3. Layers (layer)

layer={{
  enabled: true,
  selected: "roadmap", // roadmap | satellite | terrain | hybrid | darkMode | lightMode
  onChange: (id) => console.log("layer", id),
}}

darkMode / lightMode apply the library's bundled map styles; the rest map to native Google map types. Override the switcher entries with layer.options.

4. Markers & Info Windows (markers + infoWindow)

markers={[
  {
    id: 1,
    position: { lat: 40.75, lng: -73.98 },
    label: "Empire State",   // pill label above the pin
    iconUrls: ["/pin.png"],  // optional; omit for the default Google pin
    data: myRecord,          // echoed back in marker callbacks
  },
]}
infoWindow={{
  enabled: true,
  // Return an HTML string (may be async, e.g. reverse-geocode).
  resolveContent: (marker) => `
    <div>
      <strong>${marker.label}</strong>
      <a href="#" data-fmv-view-details>View details</a>
    </div>`,
}}

Any element carrying the data-fmv-view-details attribute inside the returned HTML becomes a click target that fires actions.onMarkerViewDetails.

To emphasize one pin (e.g. the permit the user just located in a list), pass its id — the marker is raised above the others and given a highlight ring:

<FederatedMapView highlightedMarkerId={locatedPermitId /* or null to clear */} />

Marker Collision

For viewports that hold far more markers than fit on screen (e.g. a state-wide view of thousands of permits), set config.markerMode to "collision". The map then uses Advanced Markers and draws only the pins that fit at the current zoom, revealing the rest as the user zooms in. MapMarker.priority (higher wins) picks which marker survives a collision.

<FederatedMapView
  mapConfig={{ googleMapsApiKey: KEY, mapId: YOUR_VECTOR_MAP_ID }}
  markers={[{ id: 1, position, label: "P-1", priority: 10 /* … */ }]}
  config={{ markerMode: "collision" }} // default: "default" (draw every pin)
/>

Collision requires a vector map, so mapConfig.mapId must be set — without it the mode silently falls back to "default". The mode changes only what is drawn; your own counts (e.g. the "Permits Found" pill) still reflect the full data set you passed in.

5. Selected Point (selectedPoint)

A single controlled marker + optional info window for "pick a location" flows. Set it in response to onMapClick, clear it on close:

const [point, setPoint] = useState<SelectedPoint>();

<FederatedMapView
  selectedPoint={point}
  actions={{
    onMapClick: (latLng) =>
      setPoint({
        position: latLng,
        render: () => <div>Selected: {latLng.lat.toFixed(4)}</div>,
        onClose: () => setPoint(undefined),
      }),
  }}
/>

6. Predefined & Highlighted Boundaries

predefinedBoundaries — read-only geometry drawn on load, either explicit polygons or a GeoJSON URL the component fetches itself:

// Explicit polygons
predefinedBoundaries={[{ id: "z1", paths: [{ lat, lng }, ...] }]}

// …or a GeoJSON URL (component fetches + draws; fitBounds zooms to it)
predefinedBoundaries={{
  geoJsonUrl: "https://example.com/georgia.geojson",
  fitBounds: true,
}}

highlightedBoundaries — an optional second polygon layer drawn above the predefined one, for one-off highlights such as the regions currently selected in the filter drawer. It defaults to a blue theme so it reads as distinct from the (green) predefined outline; each polygon may still override its own style:

highlightedBoundaries={selectedRegions /* MapPolygon<T>[] */}

7. Custom Boundary Drawing + Manage Panel (drawing)

Enables the draw/edit toolbar, the save popup, and the Manage Custom Boundaries sidebar. The library only renders and edits boundaries — your app owns persistence via the callbacks.

drawing={{
  enabled: true,
  boundaries,                       // saved boundaries you control
  minPoints: 3,
  // Optional geometry guards.
  isPointAllowed: (pt) => insideState(pt),                      // containment
  onRestricted: (ctx) => toast.error(`Cannot ${ctx} outside the state`),
  onSelfIntersect: (ctx) => toast.error("Boundary can't cross itself"),
  onBoundaryCreate: (draft) => toast(`${draft.name} created`),  // details captured
  onBoundarySave: (draft) => persist(draft),                    // return assigned id
  onBoundariesSave: async (drafts) => persistBatch(drafts),     // batch save; may be async (see below)
  onDuplicateName: (name) => toast.error(`"${name}" already exists`),
  onBoundaryUpdate: (update) => persistUpdate(update),
  onBoundaryDelete: (id) => remove(id),
  onBoundarySelect: (b) => console.log("selected", b?.id),
  onEditSessionChange: (editing) => setEditing(editing),        // { name } while editing, null otherwise
  onUnsavedChange: (hasUnsaved) => setDirty(hasUnsaved),        // warn before navigating away
  onManageOpenChange: (open) => setManageOpen(open),            // mirror panel state
}}

Async batch save & failure recovery. onBoundariesSave may return a Promise<boolean | void>. Resolving to false (or rejecting) tells the library the save failed, so it keeps the creation session alive — the drawn boundaries stay on the map and the user can fix the problem (e.g. a name the backend rejected as duplicate) and click Save again without redrawing. Resolving to anything else (or a non-Promise return) is treated as success and ends the session.

onBoundariesSave: async (drafts) => {
  try {
    await api.saveBoundaries(drafts);
    return true;   // success → session ends
  } catch {
    toast.error("Save failed — your boundaries are still here.");
    return false;  // failure → session stays open, nothing lost
  }
}

Drawing UX: click to add points (a live line trails the cursor), click the first point to close the loop → a save form captures name/description. The toolbar's Save commits via onBoundarySave (or onBoundariesSave for a batch). Undo/redo step one vertex/edit at a time. Saved boundaries can be re-entered from the Manage sidebar → Edit (drag the shape and its vertex/midpoint handles) → Update Boundary persists via onBoundaryUpdate.

| Key drawing field | Type | Description | | --- | --- | --- | | enabled | boolean | Turn the feature on. | | boundaries | MapBoundary<T>[] | Saved boundaries (consumer-owned). | | boundaryStyle / activeStyle | PolygonStyle | Styles for saved vs. in-progress polygons. | | minPoints | number | Min vertices to close a polygon (default 3). | | isPointAllowed | (pt: LatLng) => boolean | Reject points outside an allowed area. | | onRestricted | (ctx: "draw" \| "edit") => void | A gesture was blocked by isPointAllowed. | | onSelfIntersect | (ctx: "draw" \| "edit") => void | A gesture was blocked because the outline would cross itself. | | onBoundaryCreate | (draft) => void | Details first captured (pre-save) — for a "created" toast. | | onBoundarySave | (draft) => void \| id | New boundary saved — return the assigned id. | | onBoundariesSave | (drafts) => void \| id[] \| Promise<boolean \| void> | Batch save of a session; async resolve false = failed (keeps session). | | onDuplicateName | (name: string) => void | A name already used by another drawn/saved boundary was rejected. | | onBoundaryUpdate | (update) => void \| id | Existing boundary's shape/details changed. | | onBoundaryDelete | (id) => void | Boundary deleted. | | onBoundarySelect | (b \| null) => void | Boundary selected / deselected. | | onEditSessionChange | ({ name } \| null) => void | An edit session on an existing boundary started ({ name }) or ended (null). | | onUnsavedChange | (hasUnsaved: boolean) => void | There is (or is no longer) unsaved boundary work — warn before exit. | | onManageOpenChange | (open: boolean) => void | The Manage panel opened/closed (mirror it in your state). | | texts | BoundaryDrawingTexts | i18n / white-label string overrides. | | elementAttrs | BoundaryElementAttrs<T> | Pass-through DOM attributes (see elementAttrs). |

8. Filters — "Search By" drawer (filters)

An accordion of consumer-driven categories with server-side pagination and debounced search. The library renders; your app supplies and paginates the data.

filters={{
  enabled: true,
  categories,                    // FilterCategory[] you fetch & paginate
  isLoading,                     // shows a skeleton while true
  selectedByCategory: { districts: ["d1"] },
  search: persistedTerm,         // seeds the input each time the drawer opens
  emptyText: "No results found", // shown in a category with no items
  onSearchChange: (q) => refetchCategories(q),  // debounced
  onLoadMore: (categoryId) => fetchNextPage(categoryId),
  onSearch: ({ predefined, custom }) => applyFilters(predefined, custom),
  onOpenChange: (open) => setDrawerOpen(open),  // mirror the drawer's state
  custom: { items: customBoundaryItems, onItemMenuClick: openMenu },
  // "Show on Map" popup beside the Search By trigger.
  boundaryDisplayMode,                                  // "common" | "all"
  onBoundaryDisplayModeChange: (m) => setDisplayMode(m),
}}

A FilterCategory is { id, label, items, total?, hasMore?, isLoading? }. Set hasMore when more items exist server-side and append the next page to items in your onLoadMore handler.

The drawer is display-only — it never filters the map itself. onSearch hands you the selections (predefined keyed by category id, custom a flat list of boundary ids) and your app decides what to render.

"Show on Map" (BoundaryDisplayMode) lets the user choose how selected boundaries are drawn — "common" (only the overlapping area) or "all" (every selected boundary in full). It's controlled: you own the value and redraw accordingly.

Only one side panel is open at a time. The filter drawer and the Manage Custom Boundaries sidebar are full-height overlays on the same edge, so opening one closes the other. Both report every transition via filters.onOpenChange / drawing.onManageOpenChange — including a panel closed implicitly — so mirrored state can't drift.

9. View Permits Toggle (permitsToggle)

A switch rendered over the map (top-right). When on, permit markers are drawn; when off, they're hidden. Works uncontrolled (seeded from defaultOn, default true) or controlled (pass value + onChange):

// Uncontrolled — the map owns the state
permitsToggle={{ enabled: true, label: "View Permits", defaultOn: true }}

// Controlled — you own it
permitsToggle={{
  enabled: true,
  label: "View Permits",
  value: viewPermits,
  onChange: setViewPermits,
}}

10. Permit Summary + Permits List Panel

permitSummary is a presentational bottom-center pill (count + optional "View" action). It holds no list of its own — the map stays UI-agnostic and just reports the View click via onView; the host decides what to open.

permitSummary={{ visible: true, count: 12, label: "Permits Found", onView: () => setOpen(true) }}

visible must be true — an empty permitSummary={{}} renders nothing. The pill is also hidden automatically while a boundary is being drawn/edited.

The library exports a styled PermitsListPanel so you don't have to rebuild one — but you render it, owning its open state and rows:

import { FederatedMapView, PermitsListPanel } from "@e-llm-studio/federated-map-view";
import type { PermitListItem } from "@e-llm-studio/federated-map-view";

const [open, setOpen] = useState(false);
const rows: PermitListItem[] = permits.map((p) => ({
  id: p.id, title: p.number, description: p.address, status: p.status, meta: p.issuedOn,
}));

<div style={{ position: "relative", height: "100vh" }}>
  <FederatedMapView
    permitSummary={{ visible: true, count: rows.length, onView: () => setOpen((o) => !o) }}
    /* … */
  />
  {open && (
    <PermitsListPanel title="Permits Found" items={rows} onClose={() => setOpen(false)} />
  )}
</div>

PermitsListPanel is absolutely positioned (bottom-left, above the pill), so render it as a sibling of the map inside a position: relative box. A row's status string is auto-mapped to a colour badge (approved/pending/rejected…).

11. Map Events (actions)

| Callback | Fired when | | --- | --- | | onLoad(map) | The google.maps.Map instance is ready. | | onMapClick(latLng, event) | The user clicks empty map (ignored while drawing). | | onBoundsChanged(bounds) | Map goes idle — use it to fetch data for the viewport. | | onMarkerClick(marker) | A marker is clicked. | | onMarkerViewDetails(marker) | A [data-fmv-view-details] element in an info window is clicked. | | onMarkerInfoWindowClose(marker) | The user dismisses a marker's info window. | | onStreetViewToggle(isStreetView) | Street View is entered/exited. |


Pass-through DOM Attributes (elementAttrs)

Every major control accepts consumer-supplied DOM attributes (id, data-*, …) that the library spreads verbatim onto the matching node. The library stays agnostic about their meaning — it's a hook for E2E test selectors or a UI-driving agent, without the library needing to know such a thing exists.

<FederatedMapView
  search={{ elementAttrs: { "data-testid": "map-search" } }}
  drawing={{
    enabled: true,
    elementAttrs: {
      createTrigger: { "data-testid": "draw-boundary" },
      saveDrawing: { "data-testid": "save-boundary" },
      infoEditDetails: { "data-testid": "boundary-edit" },   // info popup's pencil
      infoDelete: { "data-testid": "boundary-delete" },      // info popup's trash
      // per-row attrs in the Manage panel are functions of the boundary
      boundaryEdit: (b) => ({ "data-testid": `edit-${b.id}` }),
      boundaryDelete: (b) => ({ "data-testid": `delete-${b.id}` }),
    },
  }}
  filters={{
    enabled: true,
    elementAttrs: {
      trigger: { "data-testid": "search-by" },
      applyButton: { "data-testid": "apply-filters" },
      item: (categoryId, item, selected) => ({
        "data-testid": `${categoryId}-${item.id}`,
        "data-selected": selected,
      }),
    },
  }}
  permitsToggle={{
    enabled: true,
    elementAttrs: { show: { "data-testid": "permits-on" }, hide: { "data-testid": "permits-off" } },
  }}
/>

Types: ElementAttrs (the base record), BoundaryElementAttrs<T>, and FilterElementAttrs.

Two behaviours worth knowing — both exist because some controls can't be driven with a single click:

  • permitsToggle.elementAttrs — a switch can't express "turn it ON", so supplying these also renders two always-mounted, screen-reader-only buttons that set the state explicitly.
  • filters.elementAttrs.displayOption — the "Show on Map" options live in a popup that only mounts while open, so supplying this also renders screen-reader-only twins that pick the mode in one step.

The Places suggestion dropdown is rendered by Google's own widget in a separate container, so it can't be annotated — a programmatic consumer can set the search input's text, but only a real user can pick a suggestion.


TypeScript & Generics

Every data-carrying prop is generic over your record type T. Passing the type parameter once flows it end-to-end — what you attach to a marker's data comes back correctly typed in the callbacks:

<FederatedMapView<Permit>
  markers={permitMarkers}
  actions={{ onMarkerClick: (m) => m.data?.permitNumber /* typed as string */ }}
/>

Advanced: useBoundaryDrawing

For consumers building a fully custom toolbar/popup around the same draw/edit/move/undo engine, the imperative hook is exported:

import { useBoundaryDrawing } from "@e-llm-studio/federated-map-view";
import type { BoundaryDrawingApi } from "@e-llm-studio/federated-map-view";

const boundary = useBoundaryDrawing(map, drawingConfig);
// boundary.startCreating(), boundary.setTool("draw"), boundary.undo(),
// boundary.saveActive(), boundary.hasUnsavedChanges, boundary.mode, …

Most apps should use the built-in toolbar via the drawing prop instead.


Exported Utilities & Constants

import {
  FederatedMapView,    // the map component
  PermitsListPanel,    // host-rendered permits list (see Permit Summary)
  useBoundaryDrawing,  // the imperative drawing engine (advanced)
  containerStyle,      // default map container style
  defaultCenter,       // default map center
  DEFAULT_ZOOM,        // default zoom
  darkModeStyle,       // dark map style array
  lightModeStyle,      // light map style array
  DEFAULT_LAYER_OPTIONS,
  mapUtils,            // marker/overlay/street-view helpers
} from "@e-llm-studio/federated-map-view";

Local Development

This package ships a small demo harness (not part of the published build):

npm install
npm start        # http://localhost:4000

On first load, paste a Google Maps API key into the prompt (kept in localStorage). The demo wires up markers, info windows, a seed boundary, and the drawing/manage flows so you can exercise every callback.

npm run build    # produces the publishable dist/ (ESM + CJS + type declarations)

src/App.tsx / src/index.tsx are demo-only and excluded from the build.


Design Principles

  • No data fetching, store access, or business logic. The consumer provides data via props and reacts via callbacks; the host app owns persistence.
  • Generic payloads. Data-carrying shapes are generic over T, so no app-specific type leaks into the library.
  • No bundled assets. Icons are inline SVG or icon-font packages; image icons come in via *IconUrl props.
  • Self-contained styles. Plain, fmv--prefixed CSS imported as a side effect — no CSS-module or Tailwind setup required in the host app.

License

MIT © e-LLM Studio