Customization Guide
Sightline 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 incidents 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
Sightline 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 incidents 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 incident pins |
renderCluster | Custom clusters | Grouped incident indicators (React mode only) |
renderPopup | Custom popups | Incident detail cards |
The IncidentFeed component accepts:
| Prop | Component | Purpose |
|---|---|---|
renderFeedItem | Custom feed items | Sidebar list items |
Using Custom Renderers
import { MapEngine } from '@/core';
import { IncidentFeed } from '@/controls';
import { PulseMarker, StandardPopup, CompactFeedItem } from '@/examples/renderers';
// Map renderers
<MapEngine
incidents={incidents}
renderMarker={(props) => <PulseMarker {...props} />}
renderPopup={(props) => <StandardPopup {...props} />}
/>
// Feed renderers (separate component)
<IncidentFeed
incidents={filteredIncidents}
renderFeedItem={(props) => <CompactFeedItem {...props} />}
/>Built-in Renderers
Sightline includes several pre-built renderers:
Markers:
DefaultMarker- Simple colored circlePulseMarker- Animated pulsing marker
Popups:
CompactPopup- Minimal info displayStandardPopup- Full incident detailsFullViewModal- Expanded modal view
Feed Items:
DefaultFeedItem- Standard list itemCompactFeedItem- Minimal heightMediaPreviewFeedItem- With thumbnail
Creating Custom Renderers
Custom Marker
import type { MarkerRendererProps } from '@disclosureos/sightline-core';
export function MyMarker({ incidents, isSelected, onClick }: MarkerRendererProps) {
const incident = incidents[0]; // Use primary incident
const sensitivity = incident.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/sightline-core';
export function MyPopup({ incident, 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">{incident.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">
{incident.temporal?.date}
</p>
{incident.summary && (
<p className="mt-2 text-sm">{incident.summary}</p>
)}
{incident.media?.length > 0 && (
<div className="mt-3 grid grid-cols-2 gap-2">
{incident.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/sightline-core';
export function MyFeedItem({ incident, 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">{incident.location?.name}</span>
</div>
<div className="text-sm text-muted-foreground mt-1">
{new Date(incident.temporal?.date).toLocaleDateString()}
</div>
</button>
);
}Renderer Props Reference
All renderers receive typed props:
interface MarkerRendererProps {
incidents: IncidentDisplay[]; // May be multiple at same location
isSelected: boolean;
isNew: boolean; // For animation
onClick: () => void;
size: number; // Computed size
color: string; // Primary color
}
interface PopupRendererProps {
incident: IncidentDisplay;
onClose: () => void;
dataSource?: DataSourceConfig;
onViewDetails?: (incident: IncidentDisplay) => void;
}
interface FeedItemRendererProps {
incident: IncidentDisplay;
isSelected: boolean;
isHighlighted: boolean; // For newly appeared items
onClick: () => void;
dataSource?: DataSourceConfig;
}
interface ClusterRendererProps {
pointCount: number;
count: number; // Alias for pointCount
pointCountAbbreviated: string;
incidents?: IncidentDisplay[];
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 {
incidents,
filteredIncidents,
filters,
updateFilter,
selectedIncident,
setSelectedIncident,
} = 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 { filteredIncidents } = 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>{filteredIncidents.length} incidents visible</p>
</div>
);
}Custom Filters
Add filtering logic in the data context:
// Custom filter in useData hook
const customFilteredIncidents = useMemo(() => {
return filteredIncidents.filter(incident => {
// Your custom filter logic
return incident.someField === someValue;
});
}, [filteredIncidents, 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