import type { Meta, StoryObj } from '@storybook/react'; import React from 'react'; import { Map } from './map'; import { MapLegend } from '../map-legend'; import { MapNotice } from '../map-notice'; import { MapViewControl } from '../map-view-control'; import type { MapScale } from './map.types'; function MapStoryFrame({ children }: { children: React.ReactNode }) { return (
{children}
); } const meta: Meta = { title: 'UI/Map', component: Map, render: args => ( ), parameters: { layout: 'centered', }, argTypes: { zoom: { control: { type: 'range', min: 1, max: 20 } }, height: { control: 'text' }, fitTo: { control: 'inline-radio', options: ['none', 'markers', 'shapes', 'all'] }, fitPadding: { control: { type: 'range', min: 0, max: 120 } }, maxZoomOnFit: { control: { type: 'range', min: 1, max: 20 } }, loading: { control: 'boolean' }, isEmpty: { control: 'boolean' }, }, }; export default meta; type Story = StoryObj; /* -------------------------------------------------------------------------- */ /* Sample data — municipalities of Rio Grande do Sul with service volumes. */ /* -------------------------------------------------------------------------- */ const ATTENDANCE_SCALE: MapScale = { value: 0, domain: [0, 900], range: [6, 24], method: 'sqrt', }; const MUNICIPALITIES = [ { id: 'porto-alegre', label: 'Porto Alegre', position: { lat: -30.0346, lng: -51.2177 }, volume: 874, }, { id: 'caxias-do-sul', label: 'Caxias do Sul', position: { lat: -29.1685, lng: -51.1796 }, volume: 412, }, { id: 'pelotas', label: 'Pelotas', position: { lat: -31.7654, lng: -52.3376 }, volume: 233 }, { id: 'canoas', label: 'Canoas', position: { lat: -29.9177, lng: -51.1836 }, volume: 188 }, { id: 'gravatai', label: 'Gravataí', position: { lat: -29.9444, lng: -50.9919 }, volume: 96 }, { id: 'santa-maria', label: 'Santa Maria', position: { lat: -29.6842, lng: -53.8069 }, volume: 41, }, ]; const VIEWS = [ { id: 'estado', label: 'Estado', center: { lat: -30.0, lng: -52.5 }, zoom: 6 }, { id: 'metro', label: 'Região metropolitana', center: { lat: -29.98, lng: -51.15 }, zoom: 10 }, ]; export const Default: Story = { args: { center: { lat: -23.5505, lng: -46.6333 }, zoom: 12, height: '400px', }, }; export const WithMarkers: Story = { args: { center: { lat: -23.5505, lng: -46.6333 }, zoom: 13, height: '500px', markers: [ { position: { lat: -23.5505, lng: -46.6333 }, title: 'São Paulo Center', info: 'The heart of the city', }, { position: { lat: -23.5605, lng: -46.6433 }, title: 'Avenida Paulista', info: 'Financial and cultural hub', colorToken: '--destructive', }, ], }, }; /** * Proportional symbology: the disc area encodes the value, and the size key in * the legend states the scale. Both read from the same `MapScale` object. */ export const ProportionalMarkers: Story = { args: { zoom: 7, center: { lat: -30.0, lng: -52.0 }, height: '520px', fitTo: 'markers', maxZoomOnFit: 9, markers: MUNICIPALITIES.map(municipality => ({ id: municipality.id, position: municipality.position, title: municipality.label, info: `${municipality.volume} atendimentos no mês`, shape: 'circle' as const, scale: { ...ATTENDANCE_SCALE, value: municipality.volume }, colorToken: '--chart-1' as const, fillOpacity: 0.45, })), }, render: args => (
), }; /** * Click or tab to a marker to select it. This is the gesture six of the surveyed * applications had to rebuild as a parallel list. */ export const SelectableFeatures: Story = { render: function SelectableFeaturesStory(args) { const [selectedId, setSelectedId] = React.useState(null); return ( setSelectedId(id)} markers={MUNICIPALITIES.map(municipality => ({ id: municipality.id, position: municipality.position, title: municipality.label, shape: 'circle' as const, radiusPx: 9, colorToken: '--chart-2' as const, }))} />

Selecionado:{' '} {MUNICIPALITIES.find(m => m.id === selectedId)?.label ?? 'nenhum'}

); }, args: { zoom: 7, center: { lat: -30.0, lng: -52.0 }, height: '480px', fitTo: 'markers', maxZoomOnFit: 9, }, }; /** Instant labels on hover and on keyboard focus, without opening an InfoWindow. */ export const MarkerTooltips: Story = { args: { zoom: 7, center: { lat: -30.0, lng: -52.0 }, height: '460px', fitTo: 'markers', maxZoomOnFit: 9, markers: MUNICIPALITIES.map(municipality => ({ id: municipality.id, position: municipality.position, title: municipality.label, colorToken: '--chart-3' as const, tooltip: ( {municipality.label} — {municipality.volume} atendimentos ), tooltipDirection: 'top' as const, })), }, }; /** Named camera positions plus the reframe action. */ export const NamedViews: Story = { render: function NamedViewsStory(args) { const [activeViewId, setActiveViewId] = React.useState('estado'); return (
setActiveViewId(undefined)} />
); }, args: { height: '480px', fitTo: 'markers', maxZoomOnFit: 9, markers: MUNICIPALITIES.map(municipality => ({ id: municipality.id, position: municipality.position, title: municipality.label, colorToken: '--chart-1' as const, })), }, }; /** An empty filter result is a normal outcome, not an error. */ export const EmptyState: Story = { args: { height: '400px', isEmpty: true, }, }; /** A data error, distinct from a failure to load the Maps script. */ export const ErrorState: Story = { args: { height: '400px', errorState: (

Falha ao carregar os atendimentos

Tente novamente em alguns instantes

), }, }; /** Applications with per-profile confidentiality need an explicit denial. */ export const DeniedState: Story = { args: { height: '400px', deniedState: (

Acesso restrito

Seu perfil não permite visualizar a localização destes registros

), }, }; /** Cartographic attribution and a provenance seal, both theme-aware. */ export const NoticesAndAttribution: Story = { render: args => (
Dados demonstrativos
), args: { zoom: 7, center: { lat: -30.0, lng: -52.0 }, height: '440px', attribution: '© OpenStreetMap contributors', markers: MUNICIPALITIES.slice(0, 3).map(municipality => ({ id: municipality.id, position: municipality.position, title: municipality.label, colorToken: '--chart-4' as const, })), }, }; /* * A coarse outline of Rio Grande do Sul. Real boundary files have thousands of * vertices; a dozen is enough to show that the outline and the points share one * projection. */ const RS_OUTLINE = { type: 'FeatureCollection' as const, features: [ { type: 'Feature' as const, properties: { nome: 'Rio Grande do Sul' }, geometry: { type: 'Polygon', coordinates: [ [ [-53.11, -27.08], [-51.55, -26.25], [-49.72, -28.6], [-49.69, -29.35], [-50.7, -30.4], [-52.0, -31.9], [-52.65, -33.15], [-53.4, -33.75], [-53.51, -32.4], [-55.6, -30.85], [-57.65, -30.19], [-56.4, -28.9], [-54.9, -27.5], [-53.11, -27.08], ], ], }, }, ], }; /** * No API key, no tiles — and the data is still there. This is what a blocked * corporate network, an exhausted quota or a lapsed billing account now looks * like, instead of a panel replaced by a setup prompt. * * The outline is what makes it answer "where" and not just "how many", so pass * `baseGeojson` whenever the geography matters. */ export const WithoutBasemap: Story = { args: { basemap: 'none', height: '520px', baseGeojson: RS_OUTLINE, attribution: '© IBGE — malha municipal', markers: MUNICIPALITIES.map(municipality => ({ id: municipality.id, position: municipality.position, title: municipality.label, shape: 'circle' as const, scale: { ...ATTENDANCE_SCALE, value: municipality.volume }, colorToken: '--chart-1' as const, fillOpacity: 0.55, tooltip: ( {municipality.label} — {municipality.volume} atendimentos ), })), }, render: args => (
), }; /** Selection and keyboard navigation work with no basemap behind them. */ export const WithoutBasemapSelectable: Story = { render: function WithoutBasemapSelectableStory(args) { const [selectedId, setSelectedId] = React.useState(null); return ( setSelectedId(id)} markers={MUNICIPALITIES.map(municipality => ({ id: municipality.id, position: municipality.position, title: municipality.label, shape: 'circle' as const, radiusPx: 9, colorToken: '--chart-2' as const, }))} />

Selecionado:{' '} {MUNICIPALITIES.find(m => m.id === selectedId)?.label ?? 'nenhum'}

); }, args: { basemap: 'none', height: '480px', baseGeojson: RS_OUTLINE, }, }; /** * Opting out of the fallback (`basemapFallback={false}`) restores the original * setup prompt for consumers who prefer it. */ export const NoKey: Story = { args: { apiKey: '', basemapFallback: false, }, };