mirror of
https://github.com/AnmolSaini16/mapcn
synced 2026-04-25 16:14:54 +02:00
Merge pull request #48 from AnmolSaini16/feat/controlled-viewport-and-docs-polish
Add controlled viewport API, new examples, and docs UI improvements
This commit is contained in:
File diff suppressed because one or more lines are too long
@@ -34,18 +34,14 @@ function CopyButton({ text }: { text: string }) {
|
||||
export function Hero() {
|
||||
return (
|
||||
<div className="relative">
|
||||
<div className="absolute inset-0 -z-10 overflow-hidden">
|
||||
<div className="absolute inset-x-0 -inset-y-32 -z-10 overflow-hidden">
|
||||
<div
|
||||
className="absolute inset-0 opacity-[0.4] dark:opacity-[0.15]"
|
||||
className="absolute inset-0 opacity-[0.3] dark:opacity-[0.15]"
|
||||
style={{
|
||||
backgroundImage: `radial-gradient(circle, currentColor 1px, transparent 1px)`,
|
||||
backgroundSize: "24px 24px",
|
||||
}}
|
||||
/>
|
||||
|
||||
<div className="absolute -top-40 -right-40 size-96 bg-emerald-500/10 rounded-full blur-3xl" />
|
||||
<div className="absolute -bottom-40 -left-40 size-96 bg-sky-500/10 rounded-full blur-3xl" />
|
||||
|
||||
<div className="absolute inset-0 bg-linear-to-b from-background via-transparent to-background" />
|
||||
</div>
|
||||
|
||||
@@ -63,11 +59,11 @@ export function Hero() {
|
||||
</p>
|
||||
|
||||
<div className="mt-8 animate-fade-up delay-300 w-full max-w-xl">
|
||||
<div className="bg-card border border-border rounded-lg shadow-sm overflow-hidden">
|
||||
<div className="bg-card border border-border rounded-lg shadow-xs overflow-hidden">
|
||||
<div className="flex items-center gap-1.5 px-3 py-2 border-b border-border/50">
|
||||
<span className="size-2.5 rounded-full bg-red-500/40" />
|
||||
<span className="size-2.5 rounded-full bg-yellow-500/40" />
|
||||
<span className="size-2.5 rounded-full bg-green-500/40" />
|
||||
<span className="size-2 rounded-full bg-foreground/20" />
|
||||
<span className="size-2 rounded-full bg-foreground/20" />
|
||||
<span className="size-2 rounded-full bg-foreground/20" />
|
||||
</div>
|
||||
|
||||
<div className="flex items-center gap-3 px-4 py-3 font-mono text-sm">
|
||||
@@ -88,9 +84,7 @@ export function Hero() {
|
||||
</Link>
|
||||
</Button>
|
||||
<Button variant="ghost" size="lg" asChild>
|
||||
<Link href="/docs/basic-map" className="text-muted-foreground">
|
||||
View Components
|
||||
</Link>
|
||||
<Link href="/docs/basic-map">View Components</Link>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -4,15 +4,15 @@ import { ExamplesGrid } from "./_components/examples-grid";
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<div className="flex flex-col pb-28">
|
||||
<div className="flex flex-col pb-32">
|
||||
<Header className="border-b border-transparent" />
|
||||
|
||||
<main className="flex-1">
|
||||
<section className="relative w-full py-20">
|
||||
<section className="relative w-full py-24">
|
||||
<Hero />
|
||||
</section>
|
||||
|
||||
<section className="container px-6">
|
||||
<section className="container">
|
||||
<ExamplesGrid />
|
||||
</section>
|
||||
</main>
|
||||
|
||||
@@ -33,7 +33,7 @@ export function DocsSidebar() {
|
||||
<SidebarContent className="pt-8 no-scrollbar overflow-x-hidden">
|
||||
{docsNavigation.map((group) => (
|
||||
<SidebarGroup key={group.title} className="px-1">
|
||||
<SidebarGroupLabel className="text-[11px] font-medium uppercase tracking-wider text-muted-foreground/70">
|
||||
<SidebarGroupLabel className="text-[11px] font-medium uppercase tracking-wider text-muted-foreground/80">
|
||||
{group.title}
|
||||
</SidebarGroupLabel>
|
||||
<SidebarGroupContent>
|
||||
|
||||
@@ -57,28 +57,33 @@ export function DocsToc({ items, className }: DocsTocProps) {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={cn("flex flex-col gap-2.5 text-sm", className)}>
|
||||
<p className="text-muted-foreground text-xs font-medium mb-1">
|
||||
On This Page
|
||||
</p>
|
||||
{items.map((item) => (
|
||||
<a
|
||||
key={item.slug}
|
||||
href={`#${item.slug}`}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
document.getElementById(item.slug)?.scrollIntoView({
|
||||
behavior: "smooth",
|
||||
});
|
||||
}}
|
||||
className={cn(
|
||||
"text-muted-foreground hover:text-foreground text-[0.8rem] no-underline transition-colors",
|
||||
item.slug === activeHeading && "text-foreground"
|
||||
)}
|
||||
>
|
||||
{item.title}
|
||||
</a>
|
||||
))}
|
||||
<div className={cn("flex flex-col gap-1", className)}>
|
||||
<p className="text-foreground text-xs font-medium mb-2">On This Page</p>
|
||||
<div className="relative">
|
||||
<div className="absolute left-0 top-1 bottom-1 w-px bg-border" />
|
||||
<div className="flex flex-col gap-1">
|
||||
{items.map((item) => {
|
||||
const isActive = item.slug === activeHeading;
|
||||
return (
|
||||
<a
|
||||
key={item.slug}
|
||||
href={`#${item.slug}`}
|
||||
className={cn(
|
||||
"relative pl-3 py-1 text-[13px] no-underline transition-colors",
|
||||
isActive
|
||||
? "text-foreground"
|
||||
: "text-muted-foreground hover:text-foreground"
|
||||
)}
|
||||
>
|
||||
{isActive && (
|
||||
<div className="absolute left-0 top-1 bottom-1 w-px bg-foreground rounded-full" />
|
||||
)}
|
||||
{item.title}
|
||||
</a>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ interface DocsHeaderProps {
|
||||
|
||||
export function DocsHeader({ title, description }: DocsHeaderProps) {
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
<div className="space-y-3">
|
||||
<h1 className="text-3xl font-semibold tracking-tight text-foreground">
|
||||
{title}
|
||||
</h1>
|
||||
@@ -61,17 +61,17 @@ export function DocsLayout({
|
||||
}: DocsLayoutProps) {
|
||||
return (
|
||||
<div className="flex gap-8">
|
||||
<div className="flex-1 min-w-0 max-w-[720px] mx-auto py-8 pb-20">
|
||||
<div className="flex-1 min-w-0 max-w-[720px] mx-auto pt-10 pb-20">
|
||||
<DocsHeader title={title} description={description} />
|
||||
|
||||
<div className="mt-10 space-y-10">{children}</div>
|
||||
<div className="mt-12 space-y-12">{children}</div>
|
||||
|
||||
{(prev || next) && (
|
||||
<div className="flex items-center justify-between gap-4 mt-12">
|
||||
<div className="flex items-center justify-between gap-4 mt-16">
|
||||
{prev ? (
|
||||
<Link
|
||||
href={prev.href}
|
||||
className="group flex flex-col items-start gap-1"
|
||||
className="group flex flex-col items-start gap-1.5"
|
||||
>
|
||||
<span className="text-xs text-muted-foreground">Previous</span>
|
||||
<span className="text-sm font-medium group-hover:underline underline-offset-4">
|
||||
@@ -84,7 +84,7 @@ export function DocsLayout({
|
||||
{next && (
|
||||
<Link
|
||||
href={next.href}
|
||||
className="group flex flex-col items-end gap-1"
|
||||
className="group flex flex-col items-end gap-1.5"
|
||||
>
|
||||
<span className="text-xs text-muted-foreground">Next</span>
|
||||
<span className="text-sm font-medium group-hover:underline underline-offset-4">
|
||||
@@ -96,7 +96,7 @@ export function DocsLayout({
|
||||
)}
|
||||
</div>
|
||||
|
||||
<aside className="hidden xl:block w-44 shrink-0">
|
||||
<aside className="hidden xl:block w-48 shrink-0">
|
||||
<nav className="sticky top-28">
|
||||
{toc.length > 0 && <DocsToc items={toc} />}
|
||||
</nav>
|
||||
@@ -114,13 +114,13 @@ interface DocsSectionProps {
|
||||
export function DocsSection({ title, children }: DocsSectionProps) {
|
||||
const id = title ? slugify(title) : undefined;
|
||||
return (
|
||||
<section className="space-y-4 scroll-mt-20" id={id}>
|
||||
<section className="space-y-5 scroll-mt-24" id={id}>
|
||||
{title && (
|
||||
<h2 className="text-xl font-semibold tracking-tight text-foreground">
|
||||
{title}
|
||||
</h2>
|
||||
)}
|
||||
<div className="text-foreground/80 text-base sm:text-[15px] leading-7 space-y-3 [&_p]:leading-7 [&_ul]:list-disc [&_ul]:pl-5 [&_ul]:space-y-1.5 [&_ol]:list-decimal [&_ol]:pl-5 [&_ol]:space-y-1.5 [&_li]:leading-7 [&_strong]:text-foreground [&_strong]:font-medium [&_em]:text-muted-foreground">
|
||||
<div className="text-foreground/80 text-[15px] leading-7 space-y-4 [&_p]:leading-7 [&_ul]:list-disc [&_ul]:pl-5 [&_ul]:space-y-2 [&_ol]:list-decimal [&_ol]:pl-5 [&_ol]:space-y-2 [&_li]:leading-7 [&_strong]:text-foreground [&_strong]:font-medium [&_em]:text-muted-foreground">
|
||||
{children}
|
||||
</div>
|
||||
</section>
|
||||
@@ -134,7 +134,7 @@ interface DocsNoteProps {
|
||||
|
||||
export function DocsNote({ children }: DocsNoteProps) {
|
||||
return (
|
||||
<div className="rounded-lg border bg-muted/40 px-4 py-3 text-[14px] leading-relaxed text-foreground/70 [&_strong]:text-foreground [&_strong]:font-medium">
|
||||
<div className="rounded-lg border bg-muted/30 px-5 py-4 text-[14px] leading-relaxed text-foreground/70 [&_strong]:text-foreground [&_strong]:font-medium">
|
||||
{children}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -22,8 +22,8 @@ export default function ClusterExample() {
|
||||
data="https://maplibre.org/maplibre-gl-js/docs/assets/earthquakes.geojson"
|
||||
clusterRadius={50}
|
||||
clusterMaxZoom={14}
|
||||
clusterColors={["#22c55e", "#eab308", "#ef4444"]}
|
||||
pointColor="#3b82f6"
|
||||
clusterColors={["#1d8cf8", "#6d5dfc", "#e23670"]}
|
||||
pointColor="#1d8cf8"
|
||||
onPointClick={(feature, coordinates) => {
|
||||
setSelectedPoint({
|
||||
coordinates,
|
||||
|
||||
41
src/app/docs/_components/examples/controlled-map-example.tsx
Normal file
41
src/app/docs/_components/examples/controlled-map-example.tsx
Normal file
@@ -0,0 +1,41 @@
|
||||
"use client";
|
||||
|
||||
import { useState } from "react";
|
||||
import { Map, type MapViewport } from "@/registry/map";
|
||||
|
||||
export function ControlledMapExample() {
|
||||
const [viewport, setViewport] = useState<MapViewport>({
|
||||
center: [-74.006, 40.7128],
|
||||
zoom: 8,
|
||||
bearing: 0,
|
||||
pitch: 0,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="h-[400px] relative w-full">
|
||||
<Map viewport={viewport} onViewportChange={setViewport} />
|
||||
<div className="absolute top-2 left-2 z-10 flex flex-wrap gap-x-3 gap-y-1 text-xs font-mono bg-background/80 backdrop-blur px-2 py-1.5 rounded border">
|
||||
<span>
|
||||
<span className="text-muted-foreground">lng:</span>{" "}
|
||||
{viewport.center[0].toFixed(3)}
|
||||
</span>
|
||||
<span>
|
||||
<span className="text-muted-foreground">lat:</span>{" "}
|
||||
{viewport.center[1].toFixed(3)}
|
||||
</span>
|
||||
<span>
|
||||
<span className="text-muted-foreground">zoom:</span>{" "}
|
||||
{viewport.zoom.toFixed(1)}
|
||||
</span>
|
||||
<span>
|
||||
<span className="text-muted-foreground">bearing:</span>{" "}
|
||||
{viewport.bearing.toFixed(1)}°
|
||||
</span>
|
||||
<span>
|
||||
<span className="text-muted-foreground">pitch:</span>{" "}
|
||||
{viewport.pitch.toFixed(1)}°
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
49
src/app/docs/_components/examples/custom-style-example.tsx
Normal file
49
src/app/docs/_components/examples/custom-style-example.tsx
Normal file
@@ -0,0 +1,49 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
import { Map, type MapRef } from "@/registry/map";
|
||||
|
||||
const styles = {
|
||||
default: undefined,
|
||||
openstreetmap: "https://tiles.openfreemap.org/styles/bright",
|
||||
openstreetmap3d: "https://tiles.openfreemap.org/styles/liberty",
|
||||
};
|
||||
|
||||
type StyleKey = keyof typeof styles;
|
||||
|
||||
export function CustomStyleExample() {
|
||||
const mapRef = useRef<MapRef>(null);
|
||||
const [style, setStyle] = useState<StyleKey>("default");
|
||||
const selectedStyle = styles[style];
|
||||
const is3D = style === "openstreetmap3d";
|
||||
|
||||
useEffect(() => {
|
||||
mapRef.current?.easeTo({ pitch: is3D ? 60 : 0, duration: 500 });
|
||||
}, [is3D]);
|
||||
|
||||
return (
|
||||
<div className="h-[400px] relative w-full">
|
||||
<Map
|
||||
ref={mapRef}
|
||||
center={[-0.1276, 51.5074]}
|
||||
zoom={15}
|
||||
styles={
|
||||
selectedStyle
|
||||
? { light: selectedStyle, dark: selectedStyle }
|
||||
: undefined
|
||||
}
|
||||
/>
|
||||
<div className="absolute top-2 right-2 z-10">
|
||||
<select
|
||||
value={style}
|
||||
onChange={(e) => setStyle(e.target.value as StyleKey)}
|
||||
className="bg-background text-foreground border rounded-md px-2 py-1 text-sm shadow"
|
||||
>
|
||||
<option value="default">Default (Carto)</option>
|
||||
<option value="openstreetmap">OpenStreetMap</option>
|
||||
<option value="openstreetmap3d">OpenStreetMap 3D</option>
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -130,23 +130,21 @@ export default function AdvancedPage() {
|
||||
map's pitch and bearing, and listen to map events to display
|
||||
real-time values.
|
||||
</p>
|
||||
<ComponentPreview code={advancedSource}>
|
||||
<AdvancedUsageExample />
|
||||
</ComponentPreview>
|
||||
</DocsSection>
|
||||
|
||||
<ComponentPreview code={advancedSource}>
|
||||
<AdvancedUsageExample />
|
||||
</ComponentPreview>
|
||||
|
||||
<DocsSection title="Example: Custom GeoJSON Layer">
|
||||
<p>
|
||||
Add custom GeoJSON data as layers with fill and outline styles. This
|
||||
example shows NYC parks with hover interactions.
|
||||
</p>
|
||||
<ComponentPreview code={customLayerSource}>
|
||||
<CustomLayerExample />
|
||||
</ComponentPreview>
|
||||
</DocsSection>
|
||||
|
||||
<ComponentPreview code={customLayerSource}>
|
||||
<CustomLayerExample />
|
||||
</ComponentPreview>
|
||||
|
||||
<DocsSection title="Example: Markers via Layers">
|
||||
<p>
|
||||
When displaying hundreds or thousands of markers, use GeoJSON layers
|
||||
@@ -154,12 +152,11 @@ export default function AdvancedPage() {
|
||||
approach renders markers on the WebGL canvas, providing significantly
|
||||
better performance.
|
||||
</p>
|
||||
<ComponentPreview code={layerMarkersSource}>
|
||||
<LayerMarkersExample />
|
||||
</ComponentPreview>
|
||||
</DocsSection>
|
||||
|
||||
<ComponentPreview code={layerMarkersSource}>
|
||||
<LayerMarkersExample />
|
||||
</ComponentPreview>
|
||||
|
||||
<DocsSection title="Extend to Build">
|
||||
<p>You can extend this to build custom features like:</p>
|
||||
<ul>
|
||||
|
||||
@@ -101,6 +101,11 @@ export default function ApiReferencePage() {
|
||||
description:
|
||||
"Child components (markers, popups, controls, routes).",
|
||||
},
|
||||
{
|
||||
name: "className",
|
||||
type: "string",
|
||||
description: "Additional CSS classes for the map container.",
|
||||
},
|
||||
{
|
||||
name: "theme",
|
||||
type: '"light" | "dark"',
|
||||
@@ -119,6 +124,18 @@ export default function ApiReferencePage() {
|
||||
description:
|
||||
'Map projection type. Use { type: "globe" } for 3D globe view.',
|
||||
},
|
||||
{
|
||||
name: "viewport",
|
||||
type: "Partial<MapViewport>",
|
||||
description:
|
||||
"Controlled viewport state. When used with onViewportChange, enables controlled mode. Can also be used alone for initial viewport.",
|
||||
},
|
||||
{
|
||||
name: "onViewportChange",
|
||||
type: "(viewport: MapViewport) => void",
|
||||
description:
|
||||
"Callback fired continuously as the viewport changes (during pan, zoom, rotate). Can be used alone to observe changes, or with viewport prop to enable controlled mode.",
|
||||
},
|
||||
]}
|
||||
/>
|
||||
</DocsSection>
|
||||
@@ -569,7 +586,7 @@ export default function ApiReferencePage() {
|
||||
{
|
||||
name: "clusterColors",
|
||||
type: "[string, string, string]",
|
||||
default: '["#51bbd6", "#f1f075", "#f28cb1"]',
|
||||
default: '["#22c55e", "#eab308", "#ef4444"]',
|
||||
description:
|
||||
"Colors for cluster circles: [small, medium, large] based on point count.",
|
||||
},
|
||||
|
||||
@@ -1,6 +1,13 @@
|
||||
import { DocsLayout, DocsSection, DocsCode } from "../_components/docs";
|
||||
import {
|
||||
DocsLayout,
|
||||
DocsSection,
|
||||
DocsCode,
|
||||
DocsLink,
|
||||
} from "../_components/docs";
|
||||
import { ComponentPreview } from "../_components/component-preview";
|
||||
import { BasicMapExample } from "../_components/examples/basic-map-example";
|
||||
import { ControlledMapExample } from "../_components/examples/controlled-map-example";
|
||||
import { CustomStyleExample } from "../_components/examples/custom-style-example";
|
||||
import { getExampleSource } from "@/lib/get-example-source";
|
||||
import { Metadata } from "next";
|
||||
|
||||
@@ -10,6 +17,8 @@ export const metadata: Metadata = {
|
||||
|
||||
export default function BasicMapPage() {
|
||||
const basicMapSource = getExampleSource("basic-map-example.tsx");
|
||||
const controlledMapSource = getExampleSource("controlled-map-example.tsx");
|
||||
const customStyleSource = getExampleSource("custom-style-example.tsx");
|
||||
|
||||
return (
|
||||
<DocsLayout
|
||||
@@ -17,17 +26,47 @@ export default function BasicMapPage() {
|
||||
description="The simplest way to add an interactive map to your application."
|
||||
prev={{ title: "API Reference", href: "/docs/api-reference" }}
|
||||
next={{ title: "Controls", href: "/docs/controls" }}
|
||||
toc={[
|
||||
{ title: "Basic Usage", slug: "basic-usage" },
|
||||
{ title: "Controlled Mode", slug: "controlled-mode" },
|
||||
{ title: "Custom Styles", slug: "custom-styles" },
|
||||
]}
|
||||
>
|
||||
<DocsSection>
|
||||
<DocsSection title="Basic Usage">
|
||||
<p>
|
||||
The <DocsCode>Map</DocsCode> component handles MapLibre GL setup,
|
||||
theming, and provides context for child components.
|
||||
</p>
|
||||
<ComponentPreview code={basicMapSource}>
|
||||
<BasicMapExample />
|
||||
</ComponentPreview>
|
||||
</DocsSection>
|
||||
|
||||
<ComponentPreview code={basicMapSource}>
|
||||
<BasicMapExample />
|
||||
</ComponentPreview>
|
||||
<DocsSection title="Controlled Mode">
|
||||
<p>
|
||||
Use the <DocsCode>viewport</DocsCode> and{" "}
|
||||
<DocsCode>onViewportChange</DocsCode> props to control the map's
|
||||
viewport externally. This is useful when you need to sync the map
|
||||
state with your application or respond to viewport changes.
|
||||
</p>
|
||||
<ComponentPreview code={controlledMapSource}>
|
||||
<ControlledMapExample />
|
||||
</ComponentPreview>
|
||||
</DocsSection>
|
||||
|
||||
<DocsSection title="Custom Styles">
|
||||
<p>
|
||||
Use the <DocsCode>styles</DocsCode> prop to provide custom map styles.
|
||||
This example uses free vector tiles from{" "}
|
||||
<DocsLink href="https://openfreemap.org" external>
|
||||
OpenFreeMap
|
||||
</DocsLink>
|
||||
, an open-source project, the data comes from OpenStreetMap.
|
||||
</p>
|
||||
<ComponentPreview code={customStyleSource}>
|
||||
<CustomStyleExample />
|
||||
</ComponentPreview>
|
||||
</DocsSection>
|
||||
</DocsLayout>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -32,11 +32,10 @@ export default function ClustersPage() {
|
||||
Click on clusters to zoom in. Click individual points to see details
|
||||
in a popup.
|
||||
</p>
|
||||
<ComponentPreview code={clusterSource}>
|
||||
<ClusterExample />
|
||||
</ComponentPreview>
|
||||
</DocsSection>
|
||||
|
||||
<ComponentPreview code={clusterSource}>
|
||||
<ClusterExample />
|
||||
</ComponentPreview>
|
||||
</DocsLayout>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -23,11 +23,10 @@ export default function ControlsPage() {
|
||||
The <DocsCode>MapControls</DocsCode> component provides a set of
|
||||
interactive controls that can be positioned on any corner of the map.
|
||||
</p>
|
||||
<ComponentPreview code={controlsSource}>
|
||||
<MapControlsExample />
|
||||
</ComponentPreview>
|
||||
</DocsSection>
|
||||
|
||||
<ComponentPreview code={controlsSource}>
|
||||
<MapControlsExample />
|
||||
</ComponentPreview>
|
||||
</DocsLayout>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -14,9 +14,11 @@ export default function DocsLayout({
|
||||
className="sticky top-0 z-50 bg-background/95 border-b backdrop-blur supports-backdrop-filter:bg-background/60"
|
||||
leftContent={<MobileSidebarTrigger />}
|
||||
/>
|
||||
<div className="flex flex-1 container">
|
||||
<DocsSidebar />
|
||||
<main className="flex-1 w-full">{children}</main>
|
||||
<div className="container flex-1">
|
||||
<div className="flex">
|
||||
<DocsSidebar />
|
||||
<main className="flex-1 w-full">{children}</main>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</SidebarProvider>
|
||||
|
||||
@@ -57,33 +57,30 @@ export default function MarkersPage() {
|
||||
<p>
|
||||
Simple markers with tooltips and popups showing location information.
|
||||
</p>
|
||||
<ComponentPreview code={markersSource}>
|
||||
<MarkersExample />
|
||||
</ComponentPreview>
|
||||
</DocsSection>
|
||||
|
||||
<ComponentPreview code={markersSource}>
|
||||
<MarkersExample />
|
||||
</ComponentPreview>
|
||||
|
||||
<DocsSection title="Rich Popups">
|
||||
<p>
|
||||
Build complex popups with images, ratings, and action buttons using
|
||||
shadcn/ui components.
|
||||
</p>
|
||||
<ComponentPreview code={popupSource} className="h-[500px]">
|
||||
<PopupExample />
|
||||
</ComponentPreview>
|
||||
</DocsSection>
|
||||
|
||||
<ComponentPreview code={popupSource} className="h-[500px]">
|
||||
<PopupExample />
|
||||
</ComponentPreview>
|
||||
|
||||
<DocsSection title="Draggable Marker">
|
||||
<p>
|
||||
Create draggable markers that users can move around the map. Click the
|
||||
marker to see its current coordinates in a popup.
|
||||
</p>
|
||||
<ComponentPreview code={draggableMarkerSource}>
|
||||
<DraggableMarkerExample />
|
||||
</ComponentPreview>
|
||||
</DocsSection>
|
||||
|
||||
<ComponentPreview code={draggableMarkerSource}>
|
||||
<DraggableMarkerExample />
|
||||
</ComponentPreview>
|
||||
</DocsLayout>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -24,11 +24,10 @@ export default function PopupsPage() {
|
||||
on the map. Unlike <DocsCode>MarkerPopup</DocsCode>, standalone popups
|
||||
are not attached to markers and can be controlled programmatically.
|
||||
</p>
|
||||
<ComponentPreview code={popupSource}>
|
||||
<StandalonePopupExample />
|
||||
</ComponentPreview>
|
||||
</DocsSection>
|
||||
|
||||
<ComponentPreview code={popupSource}>
|
||||
<StandalonePopupExample />
|
||||
</ComponentPreview>
|
||||
</DocsLayout>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -39,12 +39,11 @@ export default function RoutesPage() {
|
||||
|
||||
<DocsSection title="Basic Route">
|
||||
<p>Draw a route with numbered stop markers along the path.</p>
|
||||
<ComponentPreview code={routeSource}>
|
||||
<RouteExample />
|
||||
</ComponentPreview>
|
||||
</DocsSection>
|
||||
|
||||
<ComponentPreview code={routeSource}>
|
||||
<RouteExample />
|
||||
</ComponentPreview>
|
||||
|
||||
<DocsSection title="Route Planning">
|
||||
<p>
|
||||
Display multiple route options and let users select between them. This
|
||||
@@ -54,11 +53,10 @@ export default function RoutesPage() {
|
||||
</DocsLink>
|
||||
. Click on a route or use the buttons to switch.
|
||||
</p>
|
||||
<ComponentPreview code={osrmRouteSource} className="h-[500px]">
|
||||
<OsrmRouteExample />
|
||||
</ComponentPreview>
|
||||
</DocsSection>
|
||||
|
||||
<ComponentPreview code={osrmRouteSource} className="h-[500px]">
|
||||
<OsrmRouteExample />
|
||||
</ComponentPreview>
|
||||
</DocsLayout>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -118,7 +118,7 @@
|
||||
|
||||
@layer base {
|
||||
* {
|
||||
@apply border-border outline-ring/50 overscroll-none;
|
||||
@apply border-border outline-ring/50;
|
||||
}
|
||||
body {
|
||||
@apply bg-background text-foreground font-sans;
|
||||
@@ -136,7 +136,7 @@
|
||||
}
|
||||
|
||||
.container {
|
||||
@apply max-w-[1440px] mx-auto px-6;
|
||||
@apply max-w-[1480px] mx-auto px-6;
|
||||
}
|
||||
|
||||
/* animations for the home page */
|
||||
|
||||
@@ -2,7 +2,13 @@
|
||||
|
||||
import * as React from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { Search } from "lucide-react";
|
||||
import {
|
||||
ArrowDown,
|
||||
ArrowUp,
|
||||
CornerDownLeft,
|
||||
FileText,
|
||||
SearchIcon,
|
||||
} from "lucide-react";
|
||||
|
||||
import {
|
||||
CommandDialog,
|
||||
@@ -14,6 +20,7 @@ import {
|
||||
} from "@/components/ui/command";
|
||||
import { Kbd } from "@/components/ui/kbd";
|
||||
import { docsNavigation } from "@/lib/docs-navigation";
|
||||
import { Button } from "./ui/button";
|
||||
|
||||
export function CommandSearch() {
|
||||
const [open, setOpen] = React.useState(false);
|
||||
@@ -38,24 +45,35 @@ export function CommandSearch() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<button
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => setOpen(true)}
|
||||
aria-label="Search documentation"
|
||||
className="hidden group sm:flex items-center gap-2 h-8 px-3 rounded-full bg-secondary/60 border border-border/40 text-sm text-muted-foreground hover:text-foreground hover:border-border transition-colors"
|
||||
className="hidden group md:flex items-center w-[180px] dark:border-border/50 border-border/70 rounded-lg text-sm font-normal text-muted-foreground"
|
||||
>
|
||||
<Search className="size-3.5" />
|
||||
<span className="hidden sm:inline">Search...</span>
|
||||
<Kbd className="hidden sm:inline-flex ml-2">⌘K</Kbd>
|
||||
</button>
|
||||
<SearchIcon className="size-3.5 shrink-0" />
|
||||
<span>Search docs...</span>
|
||||
<Kbd className="ml-auto">⌘K</Kbd>
|
||||
</Button>
|
||||
<CommandDialog
|
||||
open={open}
|
||||
onOpenChange={setOpen}
|
||||
title="Search Documentation"
|
||||
description="Search for documentation pages and components"
|
||||
showCloseButton={false}
|
||||
>
|
||||
<CommandInput placeholder="Type to search..." />
|
||||
<CommandInput
|
||||
placeholder="Search documentation..."
|
||||
className="border-none text-sm h-10"
|
||||
/>
|
||||
<CommandList>
|
||||
<CommandEmpty>No results found.</CommandEmpty>
|
||||
<CommandEmpty className="py-8 text-muted-foreground text-sm">
|
||||
<div className="flex flex-col items-center gap-1.5">
|
||||
<FileText className="size-5 opacity-40" />
|
||||
<span>No results found.</span>
|
||||
</div>
|
||||
</CommandEmpty>
|
||||
{docsNavigation.map((group) => (
|
||||
<CommandGroup key={group.title} heading={group.title}>
|
||||
{group.items.map((item) => (
|
||||
@@ -71,6 +89,29 @@ export function CommandSearch() {
|
||||
</CommandGroup>
|
||||
))}
|
||||
</CommandList>
|
||||
<div className="border-t p-3 text-xs text-muted-foreground/80 flex items-center justify-between">
|
||||
<div className="flex items-center gap-2.5">
|
||||
<span className="flex items-center gap-1.5">
|
||||
<Kbd>
|
||||
<ArrowUp className="size-3" />
|
||||
</Kbd>
|
||||
<Kbd>
|
||||
<ArrowDown className="size-3" />
|
||||
</Kbd>
|
||||
<span>navigate</span>
|
||||
</span>
|
||||
<span className="flex items-center gap-1.5">
|
||||
<Kbd>
|
||||
<CornerDownLeft className="size-3" />
|
||||
</Kbd>
|
||||
<span>select</span>
|
||||
</span>
|
||||
</div>
|
||||
<span className="flex items-baseline gap-1.5">
|
||||
<Kbd>esc</Kbd>
|
||||
<span>close</span>
|
||||
</span>
|
||||
</div>
|
||||
</CommandDialog>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -2,24 +2,24 @@ import Link from "next/link";
|
||||
|
||||
export function Footer() {
|
||||
return (
|
||||
<footer className="w-full py-8 border-t border-border/40 bg-muted/20">
|
||||
<div className="container flex flex-col sm:flex-row items-center justify-between gap-4 text-sm text-muted-foreground">
|
||||
<div className="flex items-center gap-2 flex-wrap justify-center sm:justify-start">
|
||||
<span>© {new Date().getFullYear()} mapcn</span>
|
||||
<span className="text-border">•</span>
|
||||
<footer className="w-full py-6 border-t">
|
||||
<div className="w-full container flex flex-col sm:flex-row items-center justify-between gap-4 text-sm text-muted-foreground">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="font-medium text-foreground">mapcn</span>
|
||||
<span className="text-muted-foreground/80">•</span>
|
||||
<span>
|
||||
Built by{" "}
|
||||
<a
|
||||
href="https://github.com/AnmolSaini16"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="font-medium text-foreground hover:underline underline-offset-4"
|
||||
className="font-medium text-foreground underline underline-offset-4"
|
||||
>
|
||||
Anmol
|
||||
</a>
|
||||
</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-6">
|
||||
<div className="flex items-center gap-5">
|
||||
<Link
|
||||
href="/docs"
|
||||
className="hover:text-foreground transition-colors"
|
||||
|
||||
@@ -34,7 +34,7 @@ export function Header({ className, leftContent }: HeaderProps) {
|
||||
</div>
|
||||
<div className="flex items-center gap-2 h-4.5">
|
||||
<CommandSearch />
|
||||
<Separator orientation="vertical" className="hidden sm:block" />
|
||||
<Separator orientation="vertical" className="hidden md:block" />
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Button variant="ghost" size="sm" asChild>
|
||||
@@ -44,7 +44,7 @@ export function Header({ className, leftContent }: HeaderProps) {
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
<Heart className="size-4 text-pink-500" />
|
||||
<span className="hidden sm:inline">Sponsor</span>
|
||||
<span className="hidden md:inline font-normal">Sponsor</span>
|
||||
</a>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
|
||||
@@ -17,7 +17,7 @@ const buttonVariants = cva(
|
||||
secondary:
|
||||
"bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||
ghost:
|
||||
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
|
||||
"hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/60",
|
||||
link: "text-primary underline-offset-4 hover:underline",
|
||||
},
|
||||
size: {
|
||||
|
||||
@@ -5,6 +5,7 @@ import * as DialogPrimitive from "@radix-ui/react-dialog"
|
||||
import { XIcon } from "lucide-react"
|
||||
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Button } from "@/components/ui/button"
|
||||
|
||||
function Dialog({
|
||||
...props
|
||||
@@ -90,7 +91,14 @@ function DialogHeader({ className, ...props }: React.ComponentProps<"div">) {
|
||||
)
|
||||
}
|
||||
|
||||
function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
function DialogFooter({
|
||||
className,
|
||||
showCloseButton = false,
|
||||
children,
|
||||
...props
|
||||
}: React.ComponentProps<"div"> & {
|
||||
showCloseButton?: boolean
|
||||
}) {
|
||||
return (
|
||||
<div
|
||||
data-slot="dialog-footer"
|
||||
@@ -99,7 +107,14 @@ function DialogFooter({ className, ...props }: React.ComponentProps<"div">) {
|
||||
className
|
||||
)}
|
||||
{...props}
|
||||
/>
|
||||
>
|
||||
{children}
|
||||
{showCloseButton && (
|
||||
<DialogPrimitive.Close asChild>
|
||||
<Button variant="outline">Close</Button>
|
||||
</DialogPrimitive.Close>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@@ -82,6 +82,16 @@ type MapContextValue = {
|
||||
|
||||
const MapContext = createContext<MapContextValue | null>(null);
|
||||
|
||||
function getViewport(map: MapLibreGL.Map): MapViewport {
|
||||
const center = map.getCenter();
|
||||
return {
|
||||
center: [center.lng, center.lat],
|
||||
zoom: map.getZoom(),
|
||||
bearing: map.getBearing(),
|
||||
pitch: map.getPitch(),
|
||||
};
|
||||
}
|
||||
|
||||
function useMap() {
|
||||
const context = useContext(MapContext);
|
||||
if (!context) {
|
||||
@@ -99,8 +109,22 @@ type MapStyleOption = string | MapLibreGL.StyleSpecification;
|
||||
|
||||
type Theme = "light" | "dark";
|
||||
|
||||
/** Map viewport state */
|
||||
type MapViewport = {
|
||||
/** Center coordinates [longitude, latitude] */
|
||||
center: [number, number];
|
||||
/** Zoom level */
|
||||
zoom: number;
|
||||
/** Bearing (rotation) in degrees */
|
||||
bearing: number;
|
||||
/** Pitch (tilt) in degrees */
|
||||
pitch: number;
|
||||
};
|
||||
|
||||
type MapProps = {
|
||||
children?: ReactNode;
|
||||
/** Additional CSS classes for the map container */
|
||||
className?: string;
|
||||
/**
|
||||
* Theme for the map. If not provided, automatically detects system preference.
|
||||
* Pass your theme value here.
|
||||
@@ -113,6 +137,17 @@ type MapProps = {
|
||||
};
|
||||
/** Map projection type. Use `{ type: "globe" }` for 3D globe view. */
|
||||
projection?: MapLibreGL.ProjectionSpecification;
|
||||
/**
|
||||
* Controlled viewport. When provided with onViewportChange,
|
||||
* the map becomes controlled and viewport is driven by this prop.
|
||||
*/
|
||||
viewport?: Partial<MapViewport>;
|
||||
/**
|
||||
* Callback fired continuously as the viewport changes (pan, zoom, rotate, pitch).
|
||||
* Can be used standalone to observe changes, or with `viewport` prop
|
||||
* to enable controlled mode where the map viewport is driven by your state.
|
||||
*/
|
||||
onViewportChange?: (viewport: MapViewport) => void;
|
||||
} & Omit<MapLibreGL.MapOptions, "container" | "style">;
|
||||
|
||||
type MapRef = MapLibreGL.Map;
|
||||
@@ -128,7 +163,16 @@ const DefaultLoader = () => (
|
||||
);
|
||||
|
||||
const Map = forwardRef<MapRef, MapProps>(function Map(
|
||||
{ children, theme: themeProp, styles, projection, ...props },
|
||||
{
|
||||
children,
|
||||
className,
|
||||
theme: themeProp,
|
||||
styles,
|
||||
projection,
|
||||
viewport,
|
||||
onViewportChange,
|
||||
...props
|
||||
},
|
||||
ref
|
||||
) {
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
@@ -137,8 +181,14 @@ const Map = forwardRef<MapRef, MapProps>(function Map(
|
||||
const [isStyleLoaded, setIsStyleLoaded] = useState(false);
|
||||
const currentStyleRef = useRef<MapStyleOption | null>(null);
|
||||
const styleTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
||||
const internalUpdateRef = useRef(false);
|
||||
const resolvedTheme = useResolvedTheme(themeProp);
|
||||
|
||||
const isControlled = viewport !== undefined && onViewportChange !== undefined;
|
||||
|
||||
const onViewportChangeRef = useRef(onViewportChange);
|
||||
onViewportChangeRef.current = onViewportChange;
|
||||
|
||||
const mapStyles = useMemo(
|
||||
() => ({
|
||||
dark: styles?.dark ?? defaultStyles.dark,
|
||||
@@ -147,6 +197,7 @@ const Map = forwardRef<MapRef, MapProps>(function Map(
|
||||
[styles]
|
||||
);
|
||||
|
||||
// Expose the map instance to the parent component
|
||||
useImperativeHandle(ref, () => mapInstance as MapLibreGL.Map, [mapInstance]);
|
||||
|
||||
const clearStyleTimeout = useCallback(() => {
|
||||
@@ -156,6 +207,7 @@ const Map = forwardRef<MapRef, MapProps>(function Map(
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Initialize the map
|
||||
useEffect(() => {
|
||||
if (!containerRef.current) return;
|
||||
|
||||
@@ -171,6 +223,7 @@ const Map = forwardRef<MapRef, MapProps>(function Map(
|
||||
compact: true,
|
||||
},
|
||||
...props,
|
||||
...viewport,
|
||||
});
|
||||
|
||||
const styleDataHandler = () => {
|
||||
@@ -187,14 +240,22 @@ const Map = forwardRef<MapRef, MapProps>(function Map(
|
||||
};
|
||||
const loadHandler = () => setIsLoaded(true);
|
||||
|
||||
// Viewport change handler - skip if triggered by internal update
|
||||
const handleMove = () => {
|
||||
if (internalUpdateRef.current) return;
|
||||
onViewportChangeRef.current?.(getViewport(map));
|
||||
};
|
||||
|
||||
map.on("load", loadHandler);
|
||||
map.on("styledata", styleDataHandler);
|
||||
map.on("move", handleMove);
|
||||
setMapInstance(map);
|
||||
|
||||
return () => {
|
||||
clearStyleTimeout();
|
||||
map.off("load", loadHandler);
|
||||
map.off("styledata", styleDataHandler);
|
||||
map.off("move", handleMove);
|
||||
map.remove();
|
||||
setIsLoaded(false);
|
||||
setIsStyleLoaded(false);
|
||||
@@ -203,6 +264,35 @@ const Map = forwardRef<MapRef, MapProps>(function Map(
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, []);
|
||||
|
||||
// Sync controlled viewport to map
|
||||
useEffect(() => {
|
||||
if (!mapInstance || !isControlled || !viewport) return;
|
||||
if (mapInstance.isMoving()) return;
|
||||
|
||||
const current = getViewport(mapInstance);
|
||||
const next = {
|
||||
center: viewport.center ?? current.center,
|
||||
zoom: viewport.zoom ?? current.zoom,
|
||||
bearing: viewport.bearing ?? current.bearing,
|
||||
pitch: viewport.pitch ?? current.pitch,
|
||||
};
|
||||
|
||||
if (
|
||||
next.center[0] === current.center[0] &&
|
||||
next.center[1] === current.center[1] &&
|
||||
next.zoom === current.zoom &&
|
||||
next.bearing === current.bearing &&
|
||||
next.pitch === current.pitch
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
internalUpdateRef.current = true;
|
||||
mapInstance.jumpTo(next);
|
||||
internalUpdateRef.current = false;
|
||||
}, [mapInstance, isControlled, viewport]);
|
||||
|
||||
// Handle style change
|
||||
useEffect(() => {
|
||||
if (!mapInstance || !resolvedTheme) return;
|
||||
|
||||
@@ -228,7 +318,10 @@ const Map = forwardRef<MapRef, MapProps>(function Map(
|
||||
|
||||
return (
|
||||
<MapContext.Provider value={contextValue}>
|
||||
<div ref={containerRef} className="relative w-full h-full">
|
||||
<div
|
||||
ref={containerRef}
|
||||
className={cn("relative w-full h-full", className)}
|
||||
>
|
||||
{!isLoaded && <DefaultLoader />}
|
||||
{/* SSR-safe: children render only when map is loaded on client */}
|
||||
{mapInstance && children}
|
||||
@@ -288,6 +381,23 @@ function MapMarker({
|
||||
}: MapMarkerProps) {
|
||||
const { map } = useMap();
|
||||
|
||||
const callbacksRef = useRef({
|
||||
onClick,
|
||||
onMouseEnter,
|
||||
onMouseLeave,
|
||||
onDragStart,
|
||||
onDrag,
|
||||
onDragEnd,
|
||||
});
|
||||
callbacksRef.current = {
|
||||
onClick,
|
||||
onMouseEnter,
|
||||
onMouseLeave,
|
||||
onDragStart,
|
||||
onDrag,
|
||||
onDragEnd,
|
||||
};
|
||||
|
||||
const marker = useMemo(() => {
|
||||
const markerInstance = new MapLibreGL.Marker({
|
||||
...markerOptions,
|
||||
@@ -295,9 +405,11 @@ function MapMarker({
|
||||
draggable,
|
||||
}).setLngLat([longitude, latitude]);
|
||||
|
||||
const handleClick = (e: MouseEvent) => onClick?.(e);
|
||||
const handleMouseEnter = (e: MouseEvent) => onMouseEnter?.(e);
|
||||
const handleMouseLeave = (e: MouseEvent) => onMouseLeave?.(e);
|
||||
const handleClick = (e: MouseEvent) => callbacksRef.current.onClick?.(e);
|
||||
const handleMouseEnter = (e: MouseEvent) =>
|
||||
callbacksRef.current.onMouseEnter?.(e);
|
||||
const handleMouseLeave = (e: MouseEvent) =>
|
||||
callbacksRef.current.onMouseLeave?.(e);
|
||||
|
||||
markerInstance.getElement()?.addEventListener("click", handleClick);
|
||||
markerInstance
|
||||
@@ -309,15 +421,15 @@ function MapMarker({
|
||||
|
||||
const handleDragStart = () => {
|
||||
const lngLat = markerInstance.getLngLat();
|
||||
onDragStart?.({ lng: lngLat.lng, lat: lngLat.lat });
|
||||
callbacksRef.current.onDragStart?.({ lng: lngLat.lng, lat: lngLat.lat });
|
||||
};
|
||||
const handleDrag = () => {
|
||||
const lngLat = markerInstance.getLngLat();
|
||||
onDrag?.({ lng: lngLat.lng, lat: lngLat.lat });
|
||||
callbacksRef.current.onDrag?.({ lng: lngLat.lng, lat: lngLat.lat });
|
||||
};
|
||||
const handleDragEnd = () => {
|
||||
const lngLat = markerInstance.getLngLat();
|
||||
onDragEnd?.({ lng: lngLat.lng, lat: lngLat.lat });
|
||||
callbacksRef.current.onDragEnd?.({ lng: lngLat.lng, lat: lngLat.lat });
|
||||
};
|
||||
|
||||
markerInstance.on("dragstart", handleDragStart);
|
||||
@@ -827,6 +939,8 @@ function MapPopup({
|
||||
}: MapPopupProps) {
|
||||
const { map } = useMap();
|
||||
const popupOptionsRef = useRef(popupOptions);
|
||||
const onCloseRef = useRef(onClose);
|
||||
onCloseRef.current = onClose;
|
||||
const container = useMemo(() => document.createElement("div"), []);
|
||||
|
||||
const popup = useMemo(() => {
|
||||
@@ -845,7 +959,8 @@ function MapPopup({
|
||||
useEffect(() => {
|
||||
if (!map) return;
|
||||
|
||||
const onCloseProp = () => onClose?.();
|
||||
const onCloseProp = () => onCloseRef.current?.();
|
||||
|
||||
popup.on("close", onCloseProp);
|
||||
|
||||
popup.setDOMContent(container);
|
||||
@@ -881,7 +996,6 @@ function MapPopup({
|
||||
|
||||
const handleClose = () => {
|
||||
popup.remove();
|
||||
onClose?.();
|
||||
};
|
||||
|
||||
return createPortal(
|
||||
@@ -1058,7 +1172,7 @@ type MapClusterLayerProps<
|
||||
clusterMaxZoom?: number;
|
||||
/** Radius of each cluster when clustering points in pixels (default: 50) */
|
||||
clusterRadius?: number;
|
||||
/** Colors for cluster circles: [small, medium, large] based on point count (default: ["#51bbd6", "#f1f075", "#f28cb1"]) */
|
||||
/** Colors for cluster circles: [small, medium, large] based on point count (default: ["#22c55e", "#eab308", "#ef4444"]) */
|
||||
clusterColors?: [string, string, string];
|
||||
/** Point count thresholds for color/size steps: [medium, large] (default: [100, 750]) */
|
||||
clusterThresholds?: [number, number];
|
||||
@@ -1083,7 +1197,7 @@ function MapClusterLayer<
|
||||
data,
|
||||
clusterMaxZoom = 14,
|
||||
clusterRadius = 50,
|
||||
clusterColors = ["#51bbd6", "#f1f075", "#f28cb1"],
|
||||
clusterColors = ["#22c55e", "#eab308", "#ef4444"],
|
||||
clusterThresholds = [100, 750],
|
||||
pointColor = "#3b82f6",
|
||||
onPointClick,
|
||||
@@ -1140,6 +1254,9 @@ function MapClusterLayer<
|
||||
clusterThresholds[1],
|
||||
40,
|
||||
],
|
||||
"circle-stroke-width": 1,
|
||||
"circle-stroke-color": "#fff",
|
||||
"circle-opacity": 0.85,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1166,7 +1283,9 @@ function MapClusterLayer<
|
||||
filter: ["!", ["has", "point_count"]],
|
||||
paint: {
|
||||
"circle-color": pointColor,
|
||||
"circle-radius": 6,
|
||||
"circle-radius": 5,
|
||||
"circle-stroke-width": 2,
|
||||
"circle-stroke-color": "#fff",
|
||||
},
|
||||
});
|
||||
|
||||
@@ -1360,4 +1479,4 @@ export {
|
||||
MapClusterLayer,
|
||||
};
|
||||
|
||||
export type { MapRef };
|
||||
export type { MapRef, MapViewport };
|
||||
|
||||
Reference in New Issue
Block a user