Customization Guide
Atlas is designed for extensive customization without modifying core code. This guide covers branding, theming, and custom component rendering.
For complete configuration options, see Configuration Reference. For real-world examples, see Examples & Recipes.
Branding
Basic Branding
Configure your instance identity in map.config.ts:
branding: {
name: 'My Organization Map',
description: 'Tracking events in our region',
primaryColor: '#3b82f6',
}Logo and Favicon
Add your logo to the public/ directory:
branding: {
logo: '/logo.png', // Header logo
favicon: '/favicon.ico', // Browser tab icon
ogImage: '/og-image.png', // Social sharing (1200x630px)
}For optimal social sharing, also set environment variables:
NEXT_PUBLIC_FAVICON=/favicon.icoNEXT_PUBLIC_OG_IMAGE=/og-image.png
Social Links
Display links to your organization:
branding: {
social: {
github: 'https://github.com/your-org',
twitter: 'https://twitter.com/your-handle',
website: 'https://your-website.com',
},
}Theming
Theme Configuration
Control light/dark mode behavior:
theme: {
defaultTheme: 'system', // 'light' | 'dark' | 'system'
allowToggle: true, // Let users change theme
showToggle: true, // Show toggle button
togglePosition: 'top-right',
}Custom Map Styles
Use custom Mapbox styles for different themes:
map: {
lightStyle: 'mapbox://styles/your-username/light-custom',
darkStyle: 'mapbox://styles/your-username/dark-custom',
}CSS Variables
Atlas uses CSS variables for theming. Override in globals.css:
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%;
--primary: 221.2 83.2% 53.3%;
--primary-foreground: 210 40% 98%;
/* ... more variables */
}
.dark {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
/* ... dark mode overrides */
}Custom Renderers
Override how events appear throughout the UI using render props.
Available Render Props
The MapEngine component accepts these render props:
| Prop | Component | Purpose |
|---|---|---|
renderMarker | Custom map markers | Individual event pins |
renderCluster | Custom clusters | Grouped event indicators (React mode only) |
renderPopup | Custom popups | Event detail cards |
The MapEventFeed component accepts:
| Prop | Component | Purpose |
|---|---|---|
renderFeedItem | Custom feed items | Sidebar list items |
Using Custom Renderers
import { MapEngine } from '@/core';
import { MapEventFeed } from '@/controls';
import { PulseMarker, StandardPopup, CompactFeedItem } from '@/renderers';
// Map renderers
<MapEngine
mapEvents={mapEvents}
renderMarker={(props) => <PulseMarker {...props} />}
renderPopup={(props) => <StandardPopup {...props} />}
/>
// Feed renderers (separate component)
<MapEventFeed
mapEvents={filteredMapEvents}
renderFeedItem={(props) => <CompactFeedItem {...props} />}
/>Built-in Renderers
Atlas includes several pre-built renderers:
Markers:
DefaultMarker- Simple colored circlePulseMarker- Animated pulsing marker
Popups:
CompactPopup- Minimal info displayStandardPopup- Full event detailsFullViewModal- Expanded modal view
Feed Items:
DefaultFeedItem- Standard list itemCompactFeedItem- Minimal heightMediaPreviewFeedItem- With thumbnail
Creating Custom Renderers
Custom Marker
import type { MarkerRendererProps } from '@disclosureos/mapping';
export function MyMarker({ mapEvents, isSelected, onClick }: MarkerRendererProps) {
const mapEvent = mapEvents[0]; // Use primary event
const sensitivity = mapEvent.location?.locationSensitivity || 'standard';
const colors = {
critical: 'bg-red-500',
high: 'bg-orange-500',
moderate: 'bg-yellow-500',
standard: 'bg-blue-500',
};
return (
<button
onClick={onClick}
className={`
w-4 h-4 rounded-full border-2 border-white shadow-lg
${colors[sensitivity]}
${isSelected ? 'ring-2 ring-white scale-125' : ''}
transition-transform hover:scale-110
`}
/>
);
}Custom Popup
import type { PopupRendererProps } from '@disclosureos/mapping';
export function MyPopup({ mapEvent, onClose, dataSource }: PopupRendererProps) {
return (
<div className="bg-card p-4 rounded-lg shadow-xl max-w-sm">
<div className="flex justify-between items-start">
<h3 className="font-semibold">{mapEvent.location?.name}</h3>
<button onClick={onClose} className="text-muted-foreground">
<X className="w-4 h-4" />
</button>
</div>
<p className="text-sm text-muted-foreground mt-1">
{mapEvent.temporal?.date}
</p>
{mapEvent.summary && (
<p className="mt-2 text-sm">{mapEvent.summary}</p>
)}
{mapEvent.media?.length > 0 && (
<div className="mt-3 grid grid-cols-2 gap-2">
{mapEvent.media.slice(0, 4).map((m) => (
<img
key={m.url}
src={m.thumbnailUrl || m.url}
alt={m.caption}
className="rounded aspect-video object-cover"
/>
))}
</div>
)}
</div>
);
}Custom Feed Item
import type { FeedItemRendererProps } from '@disclosureos/mapping';
export function MyFeedItem({ mapEvent, isSelected, onClick }: FeedItemRendererProps) {
return (
<button
onClick={onClick}
className={`
w-full text-left p-3 rounded-lg border transition-all
${isSelected ? 'bg-primary/10 border-primary' : 'border-border hover:bg-muted/50'}
`}
>
<div className="flex items-center gap-2">
<MapPin className="w-4 h-4 text-muted-foreground" />
<span className="font-medium">{mapEvent.location?.name}</span>
</div>
<div className="text-sm text-muted-foreground mt-1">
{new Date(mapEvent.temporal?.date).toLocaleDateString()}
</div>
</button>
);
}Renderer Props Reference
All renderers receive typed props:
interface MarkerRendererProps {
mapEvents: MapEventDisplay[]; // May be multiple at same location
isSelected: boolean;
isNew: boolean; // For animation
onClick: () => void;
size: number; // Computed size
color: string; // Primary color
}
interface PopupRendererProps {
mapEvent: MapEventDisplay;
onClose: () => void;
dataSource?: DataSourceConfig;
onViewDetails?: (mapEvent: MapEventDisplay) => void;
}
interface FeedItemRendererProps {
mapEvent: MapEventDisplay;
isSelected: boolean;
isHighlighted: boolean; // For newly appeared items
onClick: () => void;
dataSource?: DataSourceConfig;
}
interface ClusterRendererProps {
pointCount: number;
count: number; // Alias for pointCount
pointCountAbbreviated: string;
mapEvents?: MapEventDisplay[];
coordinates: [number, number];
onExpand: () => void;
onClick: () => void;
size: number;
color: string;
}Component Customization
Replacing Built-in Components
For deeper customization, you can replace entire components in page.tsx:
// Replace the filter sidebar
import { MyCustomFilterSidebar } from '@/components/my-filter-sidebar';
export default function Page() {
return (
<DataProvider>
<div className="flex h-screen">
<MyCustomFilterSidebar />
<MapEngine ... />
</div>
</DataProvider>
);
}Using Hooks
Access data and state via hooks for custom components:
import { useData, useTheme } from '@/core';
function MyComponent() {
const {
mapEvents,
filteredMapEvents,
filters,
updateFilter,
selectedMapEvent,
setSelectedMapEvent,
} = useData();
const { theme, setTheme, toggleTheme } = useTheme();
// Build your custom UI
}Advanced Customization
Custom Controls
Add new controls to the map:
- Create your control component
- Use hooks to access data
- Position using CSS or config
'use client';
import { useData } from '@/core';
export function MyControl() {
const { filteredMapEvents } = useData();
return (
<div className="absolute top-4 left-4 bg-card/90 backdrop-blur-md p-3 rounded-lg">
<h3>My Custom Control</h3>
<p>{filteredMapEvents.length} events visible</p>
</div>
);
}Custom Filters
Add filtering logic in the data context:
// Custom filter in useData hook
const customFilteredMapEvents = useMemo(() => {
return filteredMapEvents.filter(mapEvent => {
// Your custom filter logic
return mapEvent.someField === someValue;
});
}, [filteredMapEvents, someValue]);Related
- Configuration Reference — All config options
- Component Reference — Component props and APIs
- Examples & Recipes — Real-world customization examples
- Types Reference — TypeScript types for renderers
- Accessibility Guide — Making customizations accessible
- Performance Guide — Optimizing custom components