tempest-react-sdk 0.38.2 → 0.38.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/bin/lib/design/collect.mjs +19 -0
- package/bin/lib/design/functions.mjs +5 -0
- package/bin/lib/design/index.mjs +8 -3
- package/bin/lib/design/index.test.mjs +31 -0
- package/bin/lib/design/scan.mjs +34 -2
- package/bin/lib/design/scan.test.mjs +78 -0
- package/bin/lib/doctor/doctor.e2e.test.mjs +85 -0
- package/bin/tempest.mjs +90 -6
- package/dist/audio/level-meter.cjs.map +1 -1
- package/dist/audio/level-meter.js.map +1 -1
- package/dist/audio/use-audio-recorder.cjs.map +1 -1
- package/dist/audio/use-audio-recorder.js.map +1 -1
- package/dist/auth/passkey.cjs.map +1 -1
- package/dist/auth/passkey.js.map +1 -1
- package/dist/auth/use-passkey.cjs.map +1 -1
- package/dist/auth/use-passkey.js.map +1 -1
- package/dist/br/BrazilMap.cjs.map +1 -1
- package/dist/br/BrazilMap.js.map +1 -1
- package/dist/br/BrazilStateCitySelect.cjs.map +1 -1
- package/dist/br/BrazilStateCitySelect.js.map +1 -1
- package/dist/br/BrazilStateMap.cjs.map +1 -1
- package/dist/br/BrazilStateMap.js.map +1 -1
- package/dist/br/PixQRCode.cjs.map +1 -1
- package/dist/br/PixQRCode.js.map +1 -1
- package/dist/br/boleto.cjs.map +1 -1
- package/dist/br/boleto.js.map +1 -1
- package/dist/br/pix.cjs.map +1 -1
- package/dist/br/pix.js.map +1 -1
- package/dist/br.d.ts +8 -0
- package/dist/capture/media-recorder.cjs.map +1 -1
- package/dist/capture/media-recorder.js.map +1 -1
- package/dist/capture/use-barcode-scanner.cjs.map +1 -1
- package/dist/capture/use-barcode-scanner.js.map +1 -1
- package/dist/capture/use-screen-capture.cjs.map +1 -1
- package/dist/capture/use-screen-capture.js.map +1 -1
- package/dist/capture/use-speech-recognition.cjs.map +1 -1
- package/dist/capture/use-speech-recognition.js.map +1 -1
- package/dist/capture/use-video-recorder.cjs.map +1 -1
- package/dist/capture/use-video-recorder.js.map +1 -1
- package/dist/charts/types.cjs.map +1 -1
- package/dist/charts/types.js.map +1 -1
- package/dist/charts.d.ts +8 -0
- package/dist/components/AIChat/AIChat.cjs.map +1 -1
- package/dist/components/AIChat/AIChat.js.map +1 -1
- package/dist/components/AIChat/AIChatComposer.cjs.map +1 -1
- package/dist/components/AIChat/AIChatComposer.js.map +1 -1
- package/dist/components/AIChat/AIChatTurn.cjs.map +1 -1
- package/dist/components/AIChat/AIChatTurn.js.map +1 -1
- package/dist/components/AppBar/AppBar.cjs.map +1 -1
- package/dist/components/AppBar/AppBar.js.map +1 -1
- package/dist/components/AudioPlayer/AudioPlayer.cjs.map +1 -1
- package/dist/components/AudioPlayer/AudioPlayer.js.map +1 -1
- package/dist/components/AudioRecorder/AudioRecorder.cjs.map +1 -1
- package/dist/components/AudioRecorder/AudioRecorder.js.map +1 -1
- package/dist/components/BarcodeScanner/BarcodeScanner.cjs.map +1 -1
- package/dist/components/BarcodeScanner/BarcodeScanner.js.map +1 -1
- package/dist/components/Button/Button.cjs.map +1 -1
- package/dist/components/Button/Button.js.map +1 -1
- package/dist/components/Calendar/Calendar.cjs.map +1 -1
- package/dist/components/Calendar/Calendar.js.map +1 -1
- package/dist/components/Carousel/Carousel.cjs.map +1 -1
- package/dist/components/Carousel/Carousel.js.map +1 -1
- package/dist/components/Chat/Chat.cjs.map +1 -1
- package/dist/components/Chat/Chat.js.map +1 -1
- package/dist/components/ChipInput/ChipInput.cjs.map +1 -1
- package/dist/components/ChipInput/ChipInput.js.map +1 -1
- package/dist/components/CodeBlock/CodeBlock.cjs.map +1 -1
- package/dist/components/CodeBlock/CodeBlock.js.map +1 -1
- package/dist/components/Combobox/Combobox.cjs.map +1 -1
- package/dist/components/Combobox/Combobox.js.map +1 -1
- package/dist/components/Command/Command.cjs.map +1 -1
- package/dist/components/Command/Command.js.map +1 -1
- package/dist/components/ConfirmDialog/ConfirmDialog.cjs.map +1 -1
- package/dist/components/ConfirmDialog/ConfirmDialog.js.map +1 -1
- package/dist/components/ContextMenu/ContextMenu.cjs.map +1 -1
- package/dist/components/ContextMenu/ContextMenu.js.map +1 -1
- package/dist/components/CopyButton/CopyButton.cjs.map +1 -1
- package/dist/components/CopyButton/CopyButton.js.map +1 -1
- package/dist/components/DataTable/DataTable.cjs.map +1 -1
- package/dist/components/DataTable/DataTable.js.map +1 -1
- package/dist/components/DataTable/DataTable.module.cjs.map +1 -1
- package/dist/components/DataTable/DataTable.module.js.map +1 -1
- package/dist/components/DataTable/EditableCell.cjs.map +1 -1
- package/dist/components/DataTable/EditableCell.js.map +1 -1
- package/dist/components/DatePicker/DatePicker.cjs.map +1 -1
- package/dist/components/DatePicker/DatePicker.js.map +1 -1
- package/dist/components/DateRangePicker/DateRangePicker.cjs.map +1 -1
- package/dist/components/DateRangePicker/DateRangePicker.js.map +1 -1
- package/dist/components/Drawer/Drawer.cjs.map +1 -1
- package/dist/components/Drawer/Drawer.js.map +1 -1
- package/dist/components/DropdownMenu/DropdownMenu.cjs.map +1 -1
- package/dist/components/DropdownMenu/DropdownMenu.js.map +1 -1
- package/dist/components/Dropzone/Dropzone.cjs.map +1 -1
- package/dist/components/Dropzone/Dropzone.js.map +1 -1
- package/dist/components/FileUpload/FileUpload.cjs.map +1 -1
- package/dist/components/FileUpload/FileUpload.js.map +1 -1
- package/dist/components/FilterBar/FilterBar.cjs.map +1 -1
- package/dist/components/FilterBar/FilterBar.js.map +1 -1
- package/dist/components/ImageCropper/ImageCropper.cjs.map +1 -1
- package/dist/components/ImageCropper/ImageCropper.js.map +1 -1
- package/dist/components/InstallBanner/InstallBanner.cjs.map +1 -1
- package/dist/components/InstallBanner/InstallBanner.js.map +1 -1
- package/dist/components/Kanban/Kanban.cjs.map +1 -1
- package/dist/components/Kanban/Kanban.js.map +1 -1
- package/dist/components/Lightbox/Lightbox.cjs.map +1 -1
- package/dist/components/Lightbox/Lightbox.js.map +1 -1
- package/dist/components/ListTile/ListTile.cjs.map +1 -1
- package/dist/components/ListTile/ListTile.js.map +1 -1
- package/dist/components/Markdown/Markdown.cjs.map +1 -1
- package/dist/components/Markdown/Markdown.js.map +1 -1
- package/dist/components/Markdown/markdown-parse.cjs.map +1 -1
- package/dist/components/Markdown/markdown-parse.js.map +1 -1
- package/dist/components/Menubar/Menubar.cjs.map +1 -1
- package/dist/components/Menubar/Menubar.js.map +1 -1
- package/dist/components/Modal/Modal.cjs.map +1 -1
- package/dist/components/Modal/Modal.js.map +1 -1
- package/dist/components/ModalsManager/ModalsManager.cjs.map +1 -1
- package/dist/components/ModalsManager/ModalsManager.js.map +1 -1
- package/dist/components/MultiSelect/MultiSelect.cjs.map +1 -1
- package/dist/components/MultiSelect/MultiSelect.js.map +1 -1
- package/dist/components/NavigationMenu/NavigationMenu.cjs.map +1 -1
- package/dist/components/NavigationMenu/NavigationMenu.js.map +1 -1
- package/dist/components/NotificationCenter/NotificationCenter.cjs.map +1 -1
- package/dist/components/NotificationCenter/NotificationCenter.js.map +1 -1
- package/dist/components/Page/Page.cjs.map +1 -1
- package/dist/components/Page/Page.js.map +1 -1
- package/dist/components/Pagination/Pagination.cjs.map +1 -1
- package/dist/components/Pagination/Pagination.js.map +1 -1
- package/dist/components/PasswordInput/PasswordInput.cjs.map +1 -1
- package/dist/components/PasswordInput/PasswordInput.js.map +1 -1
- package/dist/components/PinInput/PinInput.cjs.map +1 -1
- package/dist/components/PinInput/PinInput.js.map +1 -1
- package/dist/components/Popover/Popover.cjs.map +1 -1
- package/dist/components/Popover/Popover.js.map +1 -1
- package/dist/components/Progress/Progress.cjs.map +1 -1
- package/dist/components/Progress/Progress.js.map +1 -1
- package/dist/components/QRCode/qr-encode.cjs.map +1 -1
- package/dist/components/QRCode/qr-encode.js.map +1 -1
- package/dist/components/Radio/Radio.cjs.map +1 -1
- package/dist/components/Radio/Radio.js.map +1 -1
- package/dist/components/RangeSlider/RangeSlider.cjs.map +1 -1
- package/dist/components/RangeSlider/RangeSlider.js.map +1 -1
- package/dist/components/RatingStars/RatingStars.cjs.map +1 -1
- package/dist/components/RatingStars/RatingStars.js.map +1 -1
- package/dist/components/RefreshIndicator/RefreshIndicator.cjs.map +1 -1
- package/dist/components/RefreshIndicator/RefreshIndicator.js.map +1 -1
- package/dist/components/Resizable/Resizable.cjs.map +1 -1
- package/dist/components/Resizable/Resizable.js.map +1 -1
- package/dist/components/Scheduler/Scheduler.cjs.map +1 -1
- package/dist/components/Scheduler/Scheduler.js.map +1 -1
- package/dist/components/Sidebar/Sidebar.cjs.map +1 -1
- package/dist/components/Sidebar/Sidebar.js.map +1 -1
- package/dist/components/SignaturePad/SignaturePad.cjs.map +1 -1
- package/dist/components/SignaturePad/SignaturePad.js.map +1 -1
- package/dist/components/Slider/Slider.cjs.map +1 -1
- package/dist/components/Slider/Slider.js.map +1 -1
- package/dist/components/Sparkline/Sparkline.cjs.map +1 -1
- package/dist/components/Sparkline/Sparkline.js.map +1 -1
- package/dist/components/StepperInput/StepperInput.cjs.map +1 -1
- package/dist/components/StepperInput/StepperInput.js.map +1 -1
- package/dist/components/Table/Table.cjs.map +1 -1
- package/dist/components/Table/Table.js.map +1 -1
- package/dist/components/TimePicker/TimePicker.cjs.map +1 -1
- package/dist/components/TimePicker/TimePicker.js.map +1 -1
- package/dist/components/Toast/ToastProvider.cjs.map +1 -1
- package/dist/components/Toast/ToastProvider.js.map +1 -1
- package/dist/components/Tour/Tour.cjs.map +1 -1
- package/dist/components/Tour/Tour.js.map +1 -1
- package/dist/components/Transfer/Transfer.cjs.map +1 -1
- package/dist/components/Transfer/Transfer.js.map +1 -1
- package/dist/components/TreeView/TreeView.cjs.map +1 -1
- package/dist/components/TreeView/TreeView.js.map +1 -1
- package/dist/components/VirtualList/VirtualList.cjs.map +1 -1
- package/dist/components/VirtualList/VirtualList.js.map +1 -1
- package/dist/components/VirtualTable/VirtualTable.cjs.map +1 -1
- package/dist/components/VirtualTable/VirtualTable.js.map +1 -1
- package/dist/components/Wizard/Wizard.cjs.map +1 -1
- package/dist/components/Wizard/Wizard.js.map +1 -1
- package/dist/editor/RichTextEditor.cjs.map +1 -1
- package/dist/editor/RichTextEditor.js.map +1 -1
- package/dist/forms/FormField.cjs.map +1 -1
- package/dist/forms/FormField.js.map +1 -1
- package/dist/geo/TrajectoryMap.cjs.map +1 -1
- package/dist/geo/TrajectoryMap.js.map +1 -1
- package/dist/geo/estimate.cjs.map +1 -1
- package/dist/geo/estimate.js.map +1 -1
- package/dist/geo/projection.cjs.map +1 -1
- package/dist/geo/projection.js.map +1 -1
- package/dist/hooks/use-event-listener.cjs.map +1 -1
- package/dist/hooks/use-event-listener.js.map +1 -1
- package/dist/hooks/use-local-storage.cjs.map +1 -1
- package/dist/hooks/use-local-storage.js.map +1 -1
- package/dist/hooks/use-sortable.cjs.map +1 -1
- package/dist/hooks/use-sortable.js.map +1 -1
- package/dist/http/api-client.cjs.map +1 -1
- package/dist/http/api-client.js.map +1 -1
- package/dist/http/errors.cjs.map +1 -1
- package/dist/http/errors.js.map +1 -1
- package/dist/http/resumable-upload.cjs.map +1 -1
- package/dist/http/resumable-upload.js.map +1 -1
- package/dist/http/upload-with-progress.cjs.map +1 -1
- package/dist/http/upload-with-progress.js.map +1 -1
- package/dist/i18n/I18nProvider.cjs.map +1 -1
- package/dist/i18n/I18nProvider.js.map +1 -1
- package/dist/imaging/canvas.cjs.map +1 -1
- package/dist/imaging/canvas.js.map +1 -1
- package/dist/imaging.d.ts +5 -0
- package/dist/logger/logger.cjs.map +1 -1
- package/dist/logger/logger.js.map +1 -1
- package/dist/oauth/GoogleSignIn.cjs.map +1 -1
- package/dist/oauth/GoogleSignIn.js.map +1 -1
- package/dist/offline/create-offline-sync.cjs.map +1 -1
- package/dist/offline/create-offline-sync.js.map +1 -1
- package/dist/perf/cache-size.cjs +1 -1
- package/dist/perf/cache-size.cjs.map +1 -1
- package/dist/perf/cache-size.js +27 -6
- package/dist/perf/cache-size.js.map +1 -1
- package/dist/sse/create-event-stream.cjs.map +1 -1
- package/dist/sse/create-event-stream.js.map +1 -1
- package/dist/styles.css +1 -1
- package/dist/sw/background-sync.cjs.map +1 -1
- package/dist/sw/background-sync.js.map +1 -1
- package/dist/sw/cache.cjs.map +1 -1
- package/dist/sw/cache.js.map +1 -1
- package/dist/sw.d.ts +12 -0
- package/dist/tabular/compact.cjs.map +1 -1
- package/dist/tabular/compact.js.map +1 -1
- package/dist/tabular/predictor.cjs.map +1 -1
- package/dist/tabular/predictor.js.map +1 -1
- package/dist/tabular.d.ts +7 -2
- package/dist/tempest-react-sdk.d.ts +133 -6
- package/dist/theme/ThemeProvider.cjs.map +1 -1
- package/dist/theme/ThemeProvider.js.map +1 -1
- package/dist/theme/color.cjs.map +1 -1
- package/dist/theme/color.js.map +1 -1
- package/dist/theme/create-theme.cjs.map +1 -1
- package/dist/theme/create-theme.js.map +1 -1
- package/dist/theme/data-viz-ramps.cjs +1 -1
- package/dist/theme/data-viz-ramps.cjs.map +1 -1
- package/dist/theme/data-viz-ramps.js +1 -1
- package/dist/theme/data-viz-ramps.js.map +1 -1
- package/dist/utils/storage.cjs.map +1 -1
- package/dist/utils/storage.js.map +1 -1
- package/dist/vision/core/canvas.cjs.map +1 -1
- package/dist/vision/core/canvas.js.map +1 -1
- package/dist/vision/core/exceptions.cjs.map +1 -1
- package/dist/vision/core/exceptions.js.map +1 -1
- package/dist/vision/core/graph.cjs.map +1 -1
- package/dist/vision/core/graph.js.map +1 -1
- package/dist/vision/core/metadata.cjs.map +1 -1
- package/dist/vision/core/metadata.js.map +1 -1
- package/dist/vision/core/providers.cjs.map +1 -1
- package/dist/vision/core/providers.js.map +1 -1
- package/dist/vision/core/session.cjs.map +1 -1
- package/dist/vision/core/session.js.map +1 -1
- package/dist/vision/core/timing.cjs.map +1 -1
- package/dist/vision/core/timing.js.map +1 -1
- package/dist/vision/io/image.cjs.map +1 -1
- package/dist/vision/io/image.js.map +1 -1
- package/dist/vision/labels.cjs.map +1 -1
- package/dist/vision/labels.js.map +1 -1
- package/dist/vision/luminance.cjs.map +1 -1
- package/dist/vision/luminance.js.map +1 -1
- package/dist/vision/postprocess/classification.cjs.map +1 -1
- package/dist/vision/postprocess/classification.js.map +1 -1
- package/dist/vision/postprocess/detection.cjs.map +1 -1
- package/dist/vision/postprocess/detection.js.map +1 -1
- package/dist/vision/postprocess/segmentation.cjs.map +1 -1
- package/dist/vision/postprocess/segmentation.js.map +1 -1
- package/dist/vision/preprocess/image.cjs.map +1 -1
- package/dist/vision/preprocess/image.js.map +1 -1
- package/dist/vision/results.cjs.map +1 -1
- package/dist/vision/results.js.map +1 -1
- package/dist/vision/tasks/base.cjs.map +1 -1
- package/dist/vision/tasks/base.js.map +1 -1
- package/dist/vision/tasks/classifier.cjs.map +1 -1
- package/dist/vision/tasks/classifier.js.map +1 -1
- package/dist/vision/tasks/detector.cjs.map +1 -1
- package/dist/vision/tasks/detector.js.map +1 -1
- package/dist/vision/tasks/segmenter.cjs.map +1 -1
- package/dist/vision/tasks/segmenter.js.map +1 -1
- package/dist/vision/types.cjs.map +1 -1
- package/dist/vision/types.js.map +1 -1
- package/dist/vision.d.ts +6 -0
- package/dist/vite/tempest-icons.cjs.map +1 -1
- package/dist/vite/tempest-icons.js.map +1 -1
- package/dist/vite/tempest-pwa-icons.cjs.map +1 -1
- package/dist/vite/tempest-pwa-icons.js.map +1 -1
- package/dist/ws/create-web-socket.cjs.map +1 -1
- package/dist/ws/create-web-socket.js.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"BrazilMap.cjs","names":[],"sources":["../../src/br/BrazilMap.tsx"],"sourcesContent":["import { useEffect, useMemo, useRef, useState, type HTMLAttributes, type ReactNode } from \"react\";\nimport { cn } from \"@/utils/cn\";\nimport { fitProjection } from \"@/geo/projection\";\nimport type { GeoMarker } from \"@/geo/types\";\nimport type { BrUfFeatureCollection } from \"./br-geo\";\nimport { getState, type UF } from \"./locations\";\nimport { MapMarkers } from \"./MapMarkers\";\nimport { MapTooltip } from \"./MapTooltip\";\nimport { REGION_COLORS } from \"./regions\";\nimport type { ColorScale } from \"./scales\";\nimport { useMapZoom } from \"./use-map-zoom\";\nimport { geometriesBounds, geometryCentroid, geometryPath, lerpColor } from \"./svg-utils\";\nimport { useMapHover } from \"./use-map-hover\";\nimport styles from \"./BrazilMap.module.css\";\n\n/** Data passed to a {@link BrazilMapProps.renderTooltip} callback. */\nexport interface BrazilMapTooltipData {\n uf: UF;\n name: string;\n /** Choropleth value for this UF, if `values` was provided. */\n value?: number;\n}\n\nexport interface BrazilMapProps extends Omit<HTMLAttributes<HTMLDivElement>, \"onSelect\"> {\n /** Currently selected UF(s) — highlighted. Accepts one or many. */\n selected?: UF | readonly UF[] | null;\n /** Fired when a state is clicked. */\n onSelect?: (uf: UF) => void;\n /**\n * Optional choropleth values per UF. When set, each state is tinted between\n * `minColor` and `maxColor` by its value (linear). States without a value\n * use the base surface color.\n */\n values?: Partial<Record<UF, number>>;\n /** Choropleth low-end color (2-color linear ramp). Default: a light primary tint. */\n minColor?: string;\n /** Choropleth high-end color (2-color linear ramp). Default: the primary token. */\n maxColor?: string;\n /**\n * Custom value→color scale (e.g. from `sequentialScale`/`quantizeScale`).\n * Takes precedence over `minColor`/`maxColor`. Pair with a `<MapLegend>`.\n */\n colorScale?: ColorScale;\n /**\n * Tint each state by its macro-region (categorical). Overrides\n * `values`/`colorScale`. Pair with `<MapLegend items={regionLegendItems()} />`.\n */\n colorByRegion?: boolean;\n /** Viewport height in pixels. Default: `440`. */\n height?: number;\n /** Inner padding in pixels. Default: `12`. */\n padding?: number;\n /** Render the UF acronym at each state centroid. Default: `true`. */\n showLabels?: boolean;\n /** Accessible label for the map region. Default: `\"Mapa do Brasil por estado\"`. */\n label?: string;\n /** Custom content when the geometry is still loading. */\n loadingContent?: ReactNode;\n /** Show a floating tooltip (name + region + city count + value) on hover. Default: `true`. */\n showTooltip?: boolean;\n /** Override the default tooltip content. */\n renderTooltip?: (data: BrazilMapTooltipData) => ReactNode;\n /** Point markers to overlay on the map (e.g. capitals, stores). */\n markers?: readonly GeoMarker[];\n /** Fired when a marker is clicked. */\n onMarkerClick?: (marker: GeoMarker, index: number) => void;\n /** Enable wheel-zoom + drag-pan (double-click resets). Default: `false`. */\n zoomable?: boolean;\n}\n\n/**\n * Clickable choropleth map of Brazil's 27 federative units. Renders the bundled\n * simplified UF GeoJSON as SVG paths — **no external tiles or paid API**. Click\n * a state to fire `onSelect(uf)`; pass `selected` to highlight and `values` to\n * tint states by a metric.\n *\n * The GeoJSON (~36 KB gzip) is loaded lazily, so importing this component does\n * not pull the geometry until it actually mounts.\n *\n * @example\n * const [uf, setUf] = useState<UF | null>(null);\n * <BrazilMap selected={uf} onSelect={setUf} />\n *\n * @example\n * // Choropleth by a metric per state\n * <BrazilMap values={{ SP: 120, RJ: 90, MG: 60 }} />\n */\nexport function BrazilMap({\n selected,\n onSelect,\n values,\n minColor = \"#dbeafe\",\n maxColor = \"#2563eb\",\n colorScale,\n colorByRegion = false,\n height = 440,\n padding = 12,\n showLabels = true,\n label = \"Mapa do Brasil por estado\",\n loadingContent,\n showTooltip = true,\n renderTooltip,\n markers,\n onMarkerClick,\n zoomable = false,\n className,\n style,\n ...rest\n}: BrazilMapProps) {\n const containerRef = useRef<HTMLDivElement>(null);\n const [width, setWidth] = useState<number>(600);\n const [collection, setCollection] = useState<BrUfFeatureCollection | null>(null);\n const { hover, onMove, onLeave } = useMapHover<{ uf: UF; name: string }>(containerRef);\n const mapZoom = useMapZoom(zoomable);\n\n useEffect(() => {\n let active = true;\n void import(\"./br-geo\").then((m) => {\n if (active) setCollection(m.BR_UF_GEOJSON);\n });\n return () => {\n active = false;\n };\n }, []);\n\n useEffect(() => {\n const node = containerRef.current;\n if (!node || typeof ResizeObserver === \"undefined\") return;\n const observer = new ResizeObserver((entries) => {\n const measured = entries[0]?.contentRect.width;\n if (measured && measured > 0) setWidth(measured);\n });\n observer.observe(node);\n return () => observer.disconnect();\n }, []);\n\n const selectedSet = useMemo<Set<UF>>(() => {\n if (!selected) return new Set();\n return new Set(Array.isArray(selected) ? selected : [selected as UF]);\n }, [selected]);\n\n const valueRange = useMemo<[number, number] | null>(() => {\n if (!values) return null;\n const nums = Object.values(values).filter((v): v is number => typeof v === \"number\");\n if (nums.length === 0) return null;\n return [Math.min(...nums), Math.max(...nums)];\n }, [values]);\n\n const shapes = useMemo(() => {\n if (!collection) return null;\n const projection = fitProjection(\n geometriesBounds(collection.features.map((f) => f.geometry)),\n width,\n height,\n { padding },\n );\n const items = collection.features.map((feature) => ({\n uf: feature.properties.uf,\n name: feature.properties.name,\n region: feature.properties.region,\n d: geometryPath(feature.geometry, projection),\n centroid: geometryCentroid(feature.geometry, projection),\n }));\n return { projection, items };\n }, [collection, width, height, padding]);\n\n function fillFor(uf: UF): string | undefined {\n const v = values?.[uf];\n if (typeof v !== \"number\") return undefined;\n if (colorScale) return colorScale(v);\n if (!valueRange) return undefined;\n const [min, max] = valueRange;\n const t = max === min ? 1 : (v - min) / (max - min);\n return lerpColor(minColor, maxColor, t);\n }\n\n // Memoized so frequent re-renders (hover tooltip) don't rebuild all 27 paths.\n const stateEls = useMemo(() => {\n if (!shapes) return null;\n return shapes.items.map((shape) => {\n const isSelected = selectedSet.has(shape.uf);\n const fillColor = isSelected\n ? undefined\n : colorByRegion\n ? REGION_COLORS[shape.region]\n : fillFor(shape.uf);\n return (\n <path\n key={shape.uf}\n className={cn(styles.state, isSelected && styles.selected)}\n d={shape.d}\n fillRule=\"evenodd\"\n style={fillColor ? { fill: fillColor } : undefined}\n data-uf={shape.uf}\n tabIndex={onSelect ? 0 : undefined}\n role={onSelect ? \"button\" : undefined}\n aria-label={shape.name}\n aria-pressed={onSelect ? isSelected : undefined}\n onClick={onSelect ? () => onSelect(shape.uf) : undefined}\n onMouseMove={\n showTooltip\n ? (e) => onMove({ uf: shape.uf, name: shape.name }, e)\n : undefined\n }\n onMouseLeave={showTooltip ? onLeave : undefined}\n onKeyDown={\n onSelect\n ? (e) => {\n if (e.key === \"Enter\" || e.key === \" \") {\n e.preventDefault();\n onSelect(shape.uf);\n }\n }\n : undefined\n }\n />\n );\n });\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [\n shapes,\n selectedSet,\n values,\n colorScale,\n colorByRegion,\n minColor,\n maxColor,\n onSelect,\n showTooltip,\n onMove,\n onLeave,\n ]);\n\n return (\n <div\n ref={containerRef}\n className={cn(styles.map, className)}\n style={{ height, ...style }}\n role=\"group\"\n aria-label={label}\n {...rest}\n >\n {!shapes ? (\n <div className={styles.loading}>{loadingContent ?? \"Carregando mapa…\"}</div>\n ) : (\n <svg\n className={styles.svg}\n width=\"100%\"\n height={height}\n viewBox={`0 0 ${width} ${height}`}\n preserveAspectRatio=\"xMidYMid meet\"\n {...mapZoom.handlers}\n >\n <g transform={mapZoom.transform || undefined}>\n {stateEls}\n\n {showLabels &&\n shapes.items.map((shape) => (\n <text\n key={`label-${shape.uf}`}\n className={styles.label}\n x={shape.centroid.x}\n y={shape.centroid.y}\n textAnchor=\"middle\"\n dominantBaseline=\"central\"\n >\n {shape.uf}\n </text>\n ))}\n\n {markers && markers.length > 0 && (\n <MapMarkers\n projection={shapes.projection}\n markers={markers}\n onMarkerClick={onMarkerClick}\n />\n )}\n </g>\n </svg>\n )}\n\n {mapZoom.isTransformed && (\n <button type=\"button\" className={styles.zoomReset} onClick={mapZoom.reset}>\n Reset\n </button>\n )}\n\n {showTooltip && hover && (\n <MapTooltip x={hover.x} y={hover.y}>\n {renderTooltip ? (\n renderTooltip({\n uf: hover.item.uf,\n name: hover.item.name,\n value: values?.[hover.item.uf],\n })\n ) : (\n <DefaultUfTooltip\n uf={hover.item.uf}\n name={hover.item.name}\n value={values?.[hover.item.uf]}\n />\n )}\n </MapTooltip>\n )}\n </div>\n );\n}\n\n/** Default UF tooltip: name (UF) + region + city count + optional value. */\nfunction DefaultUfTooltip({ uf, name, value }: { uf: UF; name: string; value?: number }) {\n const state = getState(uf);\n return (\n <>\n <div className={styles.tooltipTitle}>\n {name} ({uf})\n </div>\n <div className={styles.tooltipMeta}>\n {state ? `${state.region} · ${state.cities.length} cidades` : uf}\n {typeof value === \"number\" ? ` · ${value}` : \"\"}\n </div>\n </>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"BrazilMap.cjs","names":[],"sources":["../../src/br/BrazilMap.tsx"],"sourcesContent":["/**\n * @tempest-limits file-lines, props-count, function-lines — the 17 props are five\n * independent axes of one map — choropleth (values, minColor, maxColor, colorScale,\n * colorByRegion), selection (selected, onSelect), overlay (markers, onMarkerClick,\n * zoomable), framing (height, padding, showLabels, label, loadingContent) and\n * tooltip (showTooltip, renderTooltip). None of them is mutually exclusive with\n * another, so there is no second component hiding here; the body is long for the\n * same reason it cannot be split, since the projected paths, the hit-testing and the\n * zoom transform all read the same viewBox state.\n */\nimport { useEffect, useMemo, useRef, useState, type HTMLAttributes, type ReactNode } from \"react\";\nimport { cn } from \"@/utils/cn\";\nimport { fitProjection } from \"@/geo/projection\";\nimport type { GeoMarker } from \"@/geo/types\";\nimport type { BrUfFeatureCollection } from \"./br-geo\";\nimport { getState, type UF } from \"./locations\";\nimport { MapMarkers } from \"./MapMarkers\";\nimport { MapTooltip } from \"./MapTooltip\";\nimport { REGION_COLORS } from \"./regions\";\nimport type { ColorScale } from \"./scales\";\nimport { useMapZoom } from \"./use-map-zoom\";\nimport { geometriesBounds, geometryCentroid, geometryPath, lerpColor } from \"./svg-utils\";\nimport { useMapHover } from \"./use-map-hover\";\nimport styles from \"./BrazilMap.module.css\";\n\n/** Data passed to a {@link BrazilMapProps.renderTooltip} callback. */\nexport interface BrazilMapTooltipData {\n uf: UF;\n name: string;\n /** Choropleth value for this UF, if `values` was provided. */\n value?: number;\n}\n\nexport interface BrazilMapProps extends Omit<HTMLAttributes<HTMLDivElement>, \"onSelect\"> {\n /** Currently selected UF(s) — highlighted. Accepts one or many. */\n selected?: UF | readonly UF[] | null;\n /** Fired when a state is clicked. */\n onSelect?: (uf: UF) => void;\n /**\n * Optional choropleth values per UF. When set, each state is tinted between\n * `minColor` and `maxColor` by its value (linear). States without a value\n * use the base surface color.\n */\n values?: Partial<Record<UF, number>>;\n /** Choropleth low-end color (2-color linear ramp). Default: a light primary tint. */\n minColor?: string;\n /** Choropleth high-end color (2-color linear ramp). Default: the primary token. */\n maxColor?: string;\n /**\n * Custom value→color scale (e.g. from `sequentialScale`/`quantizeScale`).\n * Takes precedence over `minColor`/`maxColor`. Pair with a `<MapLegend>`.\n */\n colorScale?: ColorScale;\n /**\n * Tint each state by its macro-region (categorical). Overrides\n * `values`/`colorScale`. Pair with `<MapLegend items={regionLegendItems()} />`.\n */\n colorByRegion?: boolean;\n /** Viewport height in pixels. Default: `440`. */\n height?: number;\n /** Inner padding in pixels. Default: `12`. */\n padding?: number;\n /** Render the UF acronym at each state centroid. Default: `true`. */\n showLabels?: boolean;\n /** Accessible label for the map region. Default: `\"Mapa do Brasil por estado\"`. */\n label?: string;\n /** Custom content when the geometry is still loading. */\n loadingContent?: ReactNode;\n /** Show a floating tooltip (name + region + city count + value) on hover. Default: `true`. */\n showTooltip?: boolean;\n /** Override the default tooltip content. */\n renderTooltip?: (data: BrazilMapTooltipData) => ReactNode;\n /** Point markers to overlay on the map (e.g. capitals, stores). */\n markers?: readonly GeoMarker[];\n /** Fired when a marker is clicked. */\n onMarkerClick?: (marker: GeoMarker, index: number) => void;\n /** Enable wheel-zoom + drag-pan (double-click resets). Default: `false`. */\n zoomable?: boolean;\n}\n\n/**\n * Clickable choropleth map of Brazil's 27 federative units. Renders the bundled\n * simplified UF GeoJSON as SVG paths — **no external tiles or paid API**. Click\n * a state to fire `onSelect(uf)`; pass `selected` to highlight and `values` to\n * tint states by a metric.\n *\n * The GeoJSON (~36 KB gzip) is loaded lazily, so importing this component does\n * not pull the geometry until it actually mounts.\n *\n * @example\n * const [uf, setUf] = useState<UF | null>(null);\n * <BrazilMap selected={uf} onSelect={setUf} />\n *\n * @example\n * // Choropleth by a metric per state\n * <BrazilMap values={{ SP: 120, RJ: 90, MG: 60 }} />\n */\nexport function BrazilMap({\n selected,\n onSelect,\n values,\n minColor = \"#dbeafe\",\n maxColor = \"#2563eb\",\n colorScale,\n colorByRegion = false,\n height = 440,\n padding = 12,\n showLabels = true,\n label = \"Mapa do Brasil por estado\",\n loadingContent,\n showTooltip = true,\n renderTooltip,\n markers,\n onMarkerClick,\n zoomable = false,\n className,\n style,\n ...rest\n}: BrazilMapProps) {\n const containerRef = useRef<HTMLDivElement>(null);\n const [width, setWidth] = useState<number>(600);\n const [collection, setCollection] = useState<BrUfFeatureCollection | null>(null);\n const { hover, onMove, onLeave } = useMapHover<{ uf: UF; name: string }>(containerRef);\n const mapZoom = useMapZoom(zoomable);\n\n useEffect(() => {\n let active = true;\n void import(\"./br-geo\").then((m) => {\n if (active) setCollection(m.BR_UF_GEOJSON);\n });\n return () => {\n active = false;\n };\n }, []);\n\n useEffect(() => {\n const node = containerRef.current;\n if (!node || typeof ResizeObserver === \"undefined\") return;\n const observer = new ResizeObserver((entries) => {\n const measured = entries[0]?.contentRect.width;\n if (measured && measured > 0) setWidth(measured);\n });\n observer.observe(node);\n return () => observer.disconnect();\n }, []);\n\n const selectedSet = useMemo<Set<UF>>(() => {\n if (!selected) return new Set();\n return new Set(Array.isArray(selected) ? selected : [selected as UF]);\n }, [selected]);\n\n const valueRange = useMemo<[number, number] | null>(() => {\n if (!values) return null;\n const nums = Object.values(values).filter((v): v is number => typeof v === \"number\");\n if (nums.length === 0) return null;\n return [Math.min(...nums), Math.max(...nums)];\n }, [values]);\n\n const shapes = useMemo(() => {\n if (!collection) return null;\n const projection = fitProjection(\n geometriesBounds(collection.features.map((f) => f.geometry)),\n width,\n height,\n { padding },\n );\n const items = collection.features.map((feature) => ({\n uf: feature.properties.uf,\n name: feature.properties.name,\n region: feature.properties.region,\n d: geometryPath(feature.geometry, projection),\n centroid: geometryCentroid(feature.geometry, projection),\n }));\n return { projection, items };\n }, [collection, width, height, padding]);\n\n function fillFor(uf: UF): string | undefined {\n const v = values?.[uf];\n if (typeof v !== \"number\") return undefined;\n if (colorScale) return colorScale(v);\n if (!valueRange) return undefined;\n const [min, max] = valueRange;\n const t = max === min ? 1 : (v - min) / (max - min);\n return lerpColor(minColor, maxColor, t);\n }\n\n // Memoized so frequent re-renders (hover tooltip) don't rebuild all 27 paths.\n const stateEls = useMemo(() => {\n if (!shapes) return null;\n return shapes.items.map((shape) => {\n const isSelected = selectedSet.has(shape.uf);\n const fillColor = isSelected\n ? undefined\n : colorByRegion\n ? REGION_COLORS[shape.region]\n : fillFor(shape.uf);\n return (\n <path\n key={shape.uf}\n className={cn(styles.state, isSelected && styles.selected)}\n d={shape.d}\n fillRule=\"evenodd\"\n style={fillColor ? { fill: fillColor } : undefined}\n data-uf={shape.uf}\n tabIndex={onSelect ? 0 : undefined}\n role={onSelect ? \"button\" : undefined}\n aria-label={shape.name}\n aria-pressed={onSelect ? isSelected : undefined}\n onClick={onSelect ? () => onSelect(shape.uf) : undefined}\n onMouseMove={\n showTooltip\n ? (e) => onMove({ uf: shape.uf, name: shape.name }, e)\n : undefined\n }\n onMouseLeave={showTooltip ? onLeave : undefined}\n onKeyDown={\n onSelect\n ? (e) => {\n if (e.key === \"Enter\" || e.key === \" \") {\n e.preventDefault();\n onSelect(shape.uf);\n }\n }\n : undefined\n }\n />\n );\n });\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [\n shapes,\n selectedSet,\n values,\n colorScale,\n colorByRegion,\n minColor,\n maxColor,\n onSelect,\n showTooltip,\n onMove,\n onLeave,\n ]);\n\n return (\n <div\n ref={containerRef}\n className={cn(styles.map, className)}\n style={{ height, ...style }}\n role=\"group\"\n aria-label={label}\n {...rest}\n >\n {!shapes ? (\n <div className={styles.loading}>{loadingContent ?? \"Carregando mapa…\"}</div>\n ) : (\n <svg\n className={styles.svg}\n width=\"100%\"\n height={height}\n viewBox={`0 0 ${width} ${height}`}\n preserveAspectRatio=\"xMidYMid meet\"\n {...mapZoom.handlers}\n >\n <g transform={mapZoom.transform || undefined}>\n {stateEls}\n\n {showLabels &&\n shapes.items.map((shape) => (\n <text\n key={`label-${shape.uf}`}\n className={styles.label}\n x={shape.centroid.x}\n y={shape.centroid.y}\n textAnchor=\"middle\"\n dominantBaseline=\"central\"\n >\n {shape.uf}\n </text>\n ))}\n\n {markers && markers.length > 0 && (\n <MapMarkers\n projection={shapes.projection}\n markers={markers}\n onMarkerClick={onMarkerClick}\n />\n )}\n </g>\n </svg>\n )}\n\n {mapZoom.isTransformed && (\n <button type=\"button\" className={styles.zoomReset} onClick={mapZoom.reset}>\n Reset\n </button>\n )}\n\n {showTooltip && hover && (\n <MapTooltip x={hover.x} y={hover.y}>\n {renderTooltip ? (\n renderTooltip({\n uf: hover.item.uf,\n name: hover.item.name,\n value: values?.[hover.item.uf],\n })\n ) : (\n <DefaultUfTooltip\n uf={hover.item.uf}\n name={hover.item.name}\n value={values?.[hover.item.uf]}\n />\n )}\n </MapTooltip>\n )}\n </div>\n );\n}\n\n/** Default UF tooltip: name (UF) + region + city count + optional value. */\nfunction DefaultUfTooltip({ uf, name, value }: { uf: UF; name: string; value?: number }) {\n const state = getState(uf);\n return (\n <>\n <div className={styles.tooltipTitle}>\n {name} ({uf})\n </div>\n <div className={styles.tooltipMeta}>\n {state ? `${state.region} · ${state.cities.length} cidades` : uf}\n {typeof value === \"number\" ? ` · ${value}` : \"\"}\n </div>\n </>\n );\n}\n"],"mappings":"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"}
|
package/dist/br/BrazilMap.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"BrazilMap.js","names":[],"sources":["../../src/br/BrazilMap.tsx"],"sourcesContent":["import { useEffect, useMemo, useRef, useState, type HTMLAttributes, type ReactNode } from \"react\";\nimport { cn } from \"@/utils/cn\";\nimport { fitProjection } from \"@/geo/projection\";\nimport type { GeoMarker } from \"@/geo/types\";\nimport type { BrUfFeatureCollection } from \"./br-geo\";\nimport { getState, type UF } from \"./locations\";\nimport { MapMarkers } from \"./MapMarkers\";\nimport { MapTooltip } from \"./MapTooltip\";\nimport { REGION_COLORS } from \"./regions\";\nimport type { ColorScale } from \"./scales\";\nimport { useMapZoom } from \"./use-map-zoom\";\nimport { geometriesBounds, geometryCentroid, geometryPath, lerpColor } from \"./svg-utils\";\nimport { useMapHover } from \"./use-map-hover\";\nimport styles from \"./BrazilMap.module.css\";\n\n/** Data passed to a {@link BrazilMapProps.renderTooltip} callback. */\nexport interface BrazilMapTooltipData {\n uf: UF;\n name: string;\n /** Choropleth value for this UF, if `values` was provided. */\n value?: number;\n}\n\nexport interface BrazilMapProps extends Omit<HTMLAttributes<HTMLDivElement>, \"onSelect\"> {\n /** Currently selected UF(s) — highlighted. Accepts one or many. */\n selected?: UF | readonly UF[] | null;\n /** Fired when a state is clicked. */\n onSelect?: (uf: UF) => void;\n /**\n * Optional choropleth values per UF. When set, each state is tinted between\n * `minColor` and `maxColor` by its value (linear). States without a value\n * use the base surface color.\n */\n values?: Partial<Record<UF, number>>;\n /** Choropleth low-end color (2-color linear ramp). Default: a light primary tint. */\n minColor?: string;\n /** Choropleth high-end color (2-color linear ramp). Default: the primary token. */\n maxColor?: string;\n /**\n * Custom value→color scale (e.g. from `sequentialScale`/`quantizeScale`).\n * Takes precedence over `minColor`/`maxColor`. Pair with a `<MapLegend>`.\n */\n colorScale?: ColorScale;\n /**\n * Tint each state by its macro-region (categorical). Overrides\n * `values`/`colorScale`. Pair with `<MapLegend items={regionLegendItems()} />`.\n */\n colorByRegion?: boolean;\n /** Viewport height in pixels. Default: `440`. */\n height?: number;\n /** Inner padding in pixels. Default: `12`. */\n padding?: number;\n /** Render the UF acronym at each state centroid. Default: `true`. */\n showLabels?: boolean;\n /** Accessible label for the map region. Default: `\"Mapa do Brasil por estado\"`. */\n label?: string;\n /** Custom content when the geometry is still loading. */\n loadingContent?: ReactNode;\n /** Show a floating tooltip (name + region + city count + value) on hover. Default: `true`. */\n showTooltip?: boolean;\n /** Override the default tooltip content. */\n renderTooltip?: (data: BrazilMapTooltipData) => ReactNode;\n /** Point markers to overlay on the map (e.g. capitals, stores). */\n markers?: readonly GeoMarker[];\n /** Fired when a marker is clicked. */\n onMarkerClick?: (marker: GeoMarker, index: number) => void;\n /** Enable wheel-zoom + drag-pan (double-click resets). Default: `false`. */\n zoomable?: boolean;\n}\n\n/**\n * Clickable choropleth map of Brazil's 27 federative units. Renders the bundled\n * simplified UF GeoJSON as SVG paths — **no external tiles or paid API**. Click\n * a state to fire `onSelect(uf)`; pass `selected` to highlight and `values` to\n * tint states by a metric.\n *\n * The GeoJSON (~36 KB gzip) is loaded lazily, so importing this component does\n * not pull the geometry until it actually mounts.\n *\n * @example\n * const [uf, setUf] = useState<UF | null>(null);\n * <BrazilMap selected={uf} onSelect={setUf} />\n *\n * @example\n * // Choropleth by a metric per state\n * <BrazilMap values={{ SP: 120, RJ: 90, MG: 60 }} />\n */\nexport function BrazilMap({\n selected,\n onSelect,\n values,\n minColor = \"#dbeafe\",\n maxColor = \"#2563eb\",\n colorScale,\n colorByRegion = false,\n height = 440,\n padding = 12,\n showLabels = true,\n label = \"Mapa do Brasil por estado\",\n loadingContent,\n showTooltip = true,\n renderTooltip,\n markers,\n onMarkerClick,\n zoomable = false,\n className,\n style,\n ...rest\n}: BrazilMapProps) {\n const containerRef = useRef<HTMLDivElement>(null);\n const [width, setWidth] = useState<number>(600);\n const [collection, setCollection] = useState<BrUfFeatureCollection | null>(null);\n const { hover, onMove, onLeave } = useMapHover<{ uf: UF; name: string }>(containerRef);\n const mapZoom = useMapZoom(zoomable);\n\n useEffect(() => {\n let active = true;\n void import(\"./br-geo\").then((m) => {\n if (active) setCollection(m.BR_UF_GEOJSON);\n });\n return () => {\n active = false;\n };\n }, []);\n\n useEffect(() => {\n const node = containerRef.current;\n if (!node || typeof ResizeObserver === \"undefined\") return;\n const observer = new ResizeObserver((entries) => {\n const measured = entries[0]?.contentRect.width;\n if (measured && measured > 0) setWidth(measured);\n });\n observer.observe(node);\n return () => observer.disconnect();\n }, []);\n\n const selectedSet = useMemo<Set<UF>>(() => {\n if (!selected) return new Set();\n return new Set(Array.isArray(selected) ? selected : [selected as UF]);\n }, [selected]);\n\n const valueRange = useMemo<[number, number] | null>(() => {\n if (!values) return null;\n const nums = Object.values(values).filter((v): v is number => typeof v === \"number\");\n if (nums.length === 0) return null;\n return [Math.min(...nums), Math.max(...nums)];\n }, [values]);\n\n const shapes = useMemo(() => {\n if (!collection) return null;\n const projection = fitProjection(\n geometriesBounds(collection.features.map((f) => f.geometry)),\n width,\n height,\n { padding },\n );\n const items = collection.features.map((feature) => ({\n uf: feature.properties.uf,\n name: feature.properties.name,\n region: feature.properties.region,\n d: geometryPath(feature.geometry, projection),\n centroid: geometryCentroid(feature.geometry, projection),\n }));\n return { projection, items };\n }, [collection, width, height, padding]);\n\n function fillFor(uf: UF): string | undefined {\n const v = values?.[uf];\n if (typeof v !== \"number\") return undefined;\n if (colorScale) return colorScale(v);\n if (!valueRange) return undefined;\n const [min, max] = valueRange;\n const t = max === min ? 1 : (v - min) / (max - min);\n return lerpColor(minColor, maxColor, t);\n }\n\n // Memoized so frequent re-renders (hover tooltip) don't rebuild all 27 paths.\n const stateEls = useMemo(() => {\n if (!shapes) return null;\n return shapes.items.map((shape) => {\n const isSelected = selectedSet.has(shape.uf);\n const fillColor = isSelected\n ? undefined\n : colorByRegion\n ? REGION_COLORS[shape.region]\n : fillFor(shape.uf);\n return (\n <path\n key={shape.uf}\n className={cn(styles.state, isSelected && styles.selected)}\n d={shape.d}\n fillRule=\"evenodd\"\n style={fillColor ? { fill: fillColor } : undefined}\n data-uf={shape.uf}\n tabIndex={onSelect ? 0 : undefined}\n role={onSelect ? \"button\" : undefined}\n aria-label={shape.name}\n aria-pressed={onSelect ? isSelected : undefined}\n onClick={onSelect ? () => onSelect(shape.uf) : undefined}\n onMouseMove={\n showTooltip\n ? (e) => onMove({ uf: shape.uf, name: shape.name }, e)\n : undefined\n }\n onMouseLeave={showTooltip ? onLeave : undefined}\n onKeyDown={\n onSelect\n ? (e) => {\n if (e.key === \"Enter\" || e.key === \" \") {\n e.preventDefault();\n onSelect(shape.uf);\n }\n }\n : undefined\n }\n />\n );\n });\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [\n shapes,\n selectedSet,\n values,\n colorScale,\n colorByRegion,\n minColor,\n maxColor,\n onSelect,\n showTooltip,\n onMove,\n onLeave,\n ]);\n\n return (\n <div\n ref={containerRef}\n className={cn(styles.map, className)}\n style={{ height, ...style }}\n role=\"group\"\n aria-label={label}\n {...rest}\n >\n {!shapes ? (\n <div className={styles.loading}>{loadingContent ?? \"Carregando mapa…\"}</div>\n ) : (\n <svg\n className={styles.svg}\n width=\"100%\"\n height={height}\n viewBox={`0 0 ${width} ${height}`}\n preserveAspectRatio=\"xMidYMid meet\"\n {...mapZoom.handlers}\n >\n <g transform={mapZoom.transform || undefined}>\n {stateEls}\n\n {showLabels &&\n shapes.items.map((shape) => (\n <text\n key={`label-${shape.uf}`}\n className={styles.label}\n x={shape.centroid.x}\n y={shape.centroid.y}\n textAnchor=\"middle\"\n dominantBaseline=\"central\"\n >\n {shape.uf}\n </text>\n ))}\n\n {markers && markers.length > 0 && (\n <MapMarkers\n projection={shapes.projection}\n markers={markers}\n onMarkerClick={onMarkerClick}\n />\n )}\n </g>\n </svg>\n )}\n\n {mapZoom.isTransformed && (\n <button type=\"button\" className={styles.zoomReset} onClick={mapZoom.reset}>\n Reset\n </button>\n )}\n\n {showTooltip && hover && (\n <MapTooltip x={hover.x} y={hover.y}>\n {renderTooltip ? (\n renderTooltip({\n uf: hover.item.uf,\n name: hover.item.name,\n value: values?.[hover.item.uf],\n })\n ) : (\n <DefaultUfTooltip\n uf={hover.item.uf}\n name={hover.item.name}\n value={values?.[hover.item.uf]}\n />\n )}\n </MapTooltip>\n )}\n </div>\n );\n}\n\n/** Default UF tooltip: name (UF) + region + city count + optional value. */\nfunction DefaultUfTooltip({ uf, name, value }: { uf: UF; name: string; value?: number }) {\n const state = getState(uf);\n return (\n <>\n <div className={styles.tooltipTitle}>\n {name} ({uf})\n </div>\n <div className={styles.tooltipMeta}>\n {state ? `${state.region} · ${state.cities.length} cidades` : uf}\n {typeof value === \"number\" ? ` · ${value}` : \"\"}\n </div>\n </>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"BrazilMap.js","names":[],"sources":["../../src/br/BrazilMap.tsx"],"sourcesContent":["/**\n * @tempest-limits file-lines, props-count, function-lines — the 17 props are five\n * independent axes of one map — choropleth (values, minColor, maxColor, colorScale,\n * colorByRegion), selection (selected, onSelect), overlay (markers, onMarkerClick,\n * zoomable), framing (height, padding, showLabels, label, loadingContent) and\n * tooltip (showTooltip, renderTooltip). None of them is mutually exclusive with\n * another, so there is no second component hiding here; the body is long for the\n * same reason it cannot be split, since the projected paths, the hit-testing and the\n * zoom transform all read the same viewBox state.\n */\nimport { useEffect, useMemo, useRef, useState, type HTMLAttributes, type ReactNode } from \"react\";\nimport { cn } from \"@/utils/cn\";\nimport { fitProjection } from \"@/geo/projection\";\nimport type { GeoMarker } from \"@/geo/types\";\nimport type { BrUfFeatureCollection } from \"./br-geo\";\nimport { getState, type UF } from \"./locations\";\nimport { MapMarkers } from \"./MapMarkers\";\nimport { MapTooltip } from \"./MapTooltip\";\nimport { REGION_COLORS } from \"./regions\";\nimport type { ColorScale } from \"./scales\";\nimport { useMapZoom } from \"./use-map-zoom\";\nimport { geometriesBounds, geometryCentroid, geometryPath, lerpColor } from \"./svg-utils\";\nimport { useMapHover } from \"./use-map-hover\";\nimport styles from \"./BrazilMap.module.css\";\n\n/** Data passed to a {@link BrazilMapProps.renderTooltip} callback. */\nexport interface BrazilMapTooltipData {\n uf: UF;\n name: string;\n /** Choropleth value for this UF, if `values` was provided. */\n value?: number;\n}\n\nexport interface BrazilMapProps extends Omit<HTMLAttributes<HTMLDivElement>, \"onSelect\"> {\n /** Currently selected UF(s) — highlighted. Accepts one or many. */\n selected?: UF | readonly UF[] | null;\n /** Fired when a state is clicked. */\n onSelect?: (uf: UF) => void;\n /**\n * Optional choropleth values per UF. When set, each state is tinted between\n * `minColor` and `maxColor` by its value (linear). States without a value\n * use the base surface color.\n */\n values?: Partial<Record<UF, number>>;\n /** Choropleth low-end color (2-color linear ramp). Default: a light primary tint. */\n minColor?: string;\n /** Choropleth high-end color (2-color linear ramp). Default: the primary token. */\n maxColor?: string;\n /**\n * Custom value→color scale (e.g. from `sequentialScale`/`quantizeScale`).\n * Takes precedence over `minColor`/`maxColor`. Pair with a `<MapLegend>`.\n */\n colorScale?: ColorScale;\n /**\n * Tint each state by its macro-region (categorical). Overrides\n * `values`/`colorScale`. Pair with `<MapLegend items={regionLegendItems()} />`.\n */\n colorByRegion?: boolean;\n /** Viewport height in pixels. Default: `440`. */\n height?: number;\n /** Inner padding in pixels. Default: `12`. */\n padding?: number;\n /** Render the UF acronym at each state centroid. Default: `true`. */\n showLabels?: boolean;\n /** Accessible label for the map region. Default: `\"Mapa do Brasil por estado\"`. */\n label?: string;\n /** Custom content when the geometry is still loading. */\n loadingContent?: ReactNode;\n /** Show a floating tooltip (name + region + city count + value) on hover. Default: `true`. */\n showTooltip?: boolean;\n /** Override the default tooltip content. */\n renderTooltip?: (data: BrazilMapTooltipData) => ReactNode;\n /** Point markers to overlay on the map (e.g. capitals, stores). */\n markers?: readonly GeoMarker[];\n /** Fired when a marker is clicked. */\n onMarkerClick?: (marker: GeoMarker, index: number) => void;\n /** Enable wheel-zoom + drag-pan (double-click resets). Default: `false`. */\n zoomable?: boolean;\n}\n\n/**\n * Clickable choropleth map of Brazil's 27 federative units. Renders the bundled\n * simplified UF GeoJSON as SVG paths — **no external tiles or paid API**. Click\n * a state to fire `onSelect(uf)`; pass `selected` to highlight and `values` to\n * tint states by a metric.\n *\n * The GeoJSON (~36 KB gzip) is loaded lazily, so importing this component does\n * not pull the geometry until it actually mounts.\n *\n * @example\n * const [uf, setUf] = useState<UF | null>(null);\n * <BrazilMap selected={uf} onSelect={setUf} />\n *\n * @example\n * // Choropleth by a metric per state\n * <BrazilMap values={{ SP: 120, RJ: 90, MG: 60 }} />\n */\nexport function BrazilMap({\n selected,\n onSelect,\n values,\n minColor = \"#dbeafe\",\n maxColor = \"#2563eb\",\n colorScale,\n colorByRegion = false,\n height = 440,\n padding = 12,\n showLabels = true,\n label = \"Mapa do Brasil por estado\",\n loadingContent,\n showTooltip = true,\n renderTooltip,\n markers,\n onMarkerClick,\n zoomable = false,\n className,\n style,\n ...rest\n}: BrazilMapProps) {\n const containerRef = useRef<HTMLDivElement>(null);\n const [width, setWidth] = useState<number>(600);\n const [collection, setCollection] = useState<BrUfFeatureCollection | null>(null);\n const { hover, onMove, onLeave } = useMapHover<{ uf: UF; name: string }>(containerRef);\n const mapZoom = useMapZoom(zoomable);\n\n useEffect(() => {\n let active = true;\n void import(\"./br-geo\").then((m) => {\n if (active) setCollection(m.BR_UF_GEOJSON);\n });\n return () => {\n active = false;\n };\n }, []);\n\n useEffect(() => {\n const node = containerRef.current;\n if (!node || typeof ResizeObserver === \"undefined\") return;\n const observer = new ResizeObserver((entries) => {\n const measured = entries[0]?.contentRect.width;\n if (measured && measured > 0) setWidth(measured);\n });\n observer.observe(node);\n return () => observer.disconnect();\n }, []);\n\n const selectedSet = useMemo<Set<UF>>(() => {\n if (!selected) return new Set();\n return new Set(Array.isArray(selected) ? selected : [selected as UF]);\n }, [selected]);\n\n const valueRange = useMemo<[number, number] | null>(() => {\n if (!values) return null;\n const nums = Object.values(values).filter((v): v is number => typeof v === \"number\");\n if (nums.length === 0) return null;\n return [Math.min(...nums), Math.max(...nums)];\n }, [values]);\n\n const shapes = useMemo(() => {\n if (!collection) return null;\n const projection = fitProjection(\n geometriesBounds(collection.features.map((f) => f.geometry)),\n width,\n height,\n { padding },\n );\n const items = collection.features.map((feature) => ({\n uf: feature.properties.uf,\n name: feature.properties.name,\n region: feature.properties.region,\n d: geometryPath(feature.geometry, projection),\n centroid: geometryCentroid(feature.geometry, projection),\n }));\n return { projection, items };\n }, [collection, width, height, padding]);\n\n function fillFor(uf: UF): string | undefined {\n const v = values?.[uf];\n if (typeof v !== \"number\") return undefined;\n if (colorScale) return colorScale(v);\n if (!valueRange) return undefined;\n const [min, max] = valueRange;\n const t = max === min ? 1 : (v - min) / (max - min);\n return lerpColor(minColor, maxColor, t);\n }\n\n // Memoized so frequent re-renders (hover tooltip) don't rebuild all 27 paths.\n const stateEls = useMemo(() => {\n if (!shapes) return null;\n return shapes.items.map((shape) => {\n const isSelected = selectedSet.has(shape.uf);\n const fillColor = isSelected\n ? undefined\n : colorByRegion\n ? REGION_COLORS[shape.region]\n : fillFor(shape.uf);\n return (\n <path\n key={shape.uf}\n className={cn(styles.state, isSelected && styles.selected)}\n d={shape.d}\n fillRule=\"evenodd\"\n style={fillColor ? { fill: fillColor } : undefined}\n data-uf={shape.uf}\n tabIndex={onSelect ? 0 : undefined}\n role={onSelect ? \"button\" : undefined}\n aria-label={shape.name}\n aria-pressed={onSelect ? isSelected : undefined}\n onClick={onSelect ? () => onSelect(shape.uf) : undefined}\n onMouseMove={\n showTooltip\n ? (e) => onMove({ uf: shape.uf, name: shape.name }, e)\n : undefined\n }\n onMouseLeave={showTooltip ? onLeave : undefined}\n onKeyDown={\n onSelect\n ? (e) => {\n if (e.key === \"Enter\" || e.key === \" \") {\n e.preventDefault();\n onSelect(shape.uf);\n }\n }\n : undefined\n }\n />\n );\n });\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [\n shapes,\n selectedSet,\n values,\n colorScale,\n colorByRegion,\n minColor,\n maxColor,\n onSelect,\n showTooltip,\n onMove,\n onLeave,\n ]);\n\n return (\n <div\n ref={containerRef}\n className={cn(styles.map, className)}\n style={{ height, ...style }}\n role=\"group\"\n aria-label={label}\n {...rest}\n >\n {!shapes ? (\n <div className={styles.loading}>{loadingContent ?? \"Carregando mapa…\"}</div>\n ) : (\n <svg\n className={styles.svg}\n width=\"100%\"\n height={height}\n viewBox={`0 0 ${width} ${height}`}\n preserveAspectRatio=\"xMidYMid meet\"\n {...mapZoom.handlers}\n >\n <g transform={mapZoom.transform || undefined}>\n {stateEls}\n\n {showLabels &&\n shapes.items.map((shape) => (\n <text\n key={`label-${shape.uf}`}\n className={styles.label}\n x={shape.centroid.x}\n y={shape.centroid.y}\n textAnchor=\"middle\"\n dominantBaseline=\"central\"\n >\n {shape.uf}\n </text>\n ))}\n\n {markers && markers.length > 0 && (\n <MapMarkers\n projection={shapes.projection}\n markers={markers}\n onMarkerClick={onMarkerClick}\n />\n )}\n </g>\n </svg>\n )}\n\n {mapZoom.isTransformed && (\n <button type=\"button\" className={styles.zoomReset} onClick={mapZoom.reset}>\n Reset\n </button>\n )}\n\n {showTooltip && hover && (\n <MapTooltip x={hover.x} y={hover.y}>\n {renderTooltip ? (\n renderTooltip({\n uf: hover.item.uf,\n name: hover.item.name,\n value: values?.[hover.item.uf],\n })\n ) : (\n <DefaultUfTooltip\n uf={hover.item.uf}\n name={hover.item.name}\n value={values?.[hover.item.uf]}\n />\n )}\n </MapTooltip>\n )}\n </div>\n );\n}\n\n/** Default UF tooltip: name (UF) + region + city count + optional value. */\nfunction DefaultUfTooltip({ uf, name, value }: { uf: UF; name: string; value?: number }) {\n const state = getState(uf);\n return (\n <>\n <div className={styles.tooltipTitle}>\n {name} ({uf})\n </div>\n <div className={styles.tooltipMeta}>\n {state ? `${state.region} · ${state.cities.length} cidades` : uf}\n {typeof value === \"number\" ? ` · ${value}` : \"\"}\n </div>\n </>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"BrazilStateCitySelect.cjs","names":[],"sources":["../../src/br/BrazilStateCitySelect.tsx"],"sourcesContent":["
|
|
1
|
+
{"version":3,"file":"BrazilStateCitySelect.cjs","names":[],"sources":["../../src/br/BrazilStateCitySelect.tsx"],"sourcesContent":["/**\n * @tempest-limits props-count — two dependent selects in one control, so every knob\n * comes in pairs: stateLabel and cityLabel, statePlaceholder and cityPlaceholder,\n * defaultUf and defaultCity. The pairing is the feature — the city list is derived\n * from the chosen UF, which is exactly what a caller wiring two separate selects has\n * to reimplement.\n */\nimport { useMemo, useState, type ReactElement } from \"react\";\nimport { Select } from \"@/components/Select\";\nimport { cityChoices, listStates, normalizeUf, type UF } from \"./locations\";\n\n/** Current selection emitted by {@link BrazilStateCitySelect}. */\nexport interface BrazilStateCitySelection {\n /** Selected federative unit, or `null` when none. */\n uf: UF | null;\n /** Selected city, or `null` when none. */\n city: string | null;\n}\n\nexport interface BrazilStateCitySelectProps {\n /** Pre-selected UF (uncontrolled initial value). */\n defaultUf?: UF;\n /** Pre-selected city (uncontrolled initial value). */\n defaultCity?: string;\n /** Fired whenever the state or city changes. */\n onChange?: (selection: BrazilStateCitySelection) => void;\n /** Label for the state select. Default: `\"Estado\"`. */\n stateLabel?: string;\n /** Label for the city select. Default: `\"Cidade\"`. */\n cityLabel?: string;\n /** Placeholder for the state select. Default: `\"Selecione o estado\"`. */\n statePlaceholder?: string;\n /** Placeholder for the city select. Default: `\"Selecione a cidade\"`. */\n cityPlaceholder?: string;\n /** Disable both selects. */\n disabled?: boolean;\n /** Layout of the two selects. Default: `\"row\"`. */\n layout?: \"row\" | \"column\";\n}\n\n/**\n * Cascading Estado → Cidade selector backed by the bundled BR locations data —\n * pick a state and the city list narrows to that UF's municipalities. No\n * network, no external API. Complements {@link BrazilMap} (wire the map's\n * `onSelect` to drive the same UF).\n *\n * @example\n * <BrazilStateCitySelect onChange={({ uf, city }) => console.log(uf, city)} />\n */\nexport function BrazilStateCitySelect({\n defaultUf,\n defaultCity,\n onChange,\n stateLabel = \"Estado\",\n cityLabel = \"Cidade\",\n statePlaceholder = \"Selecione o estado\",\n cityPlaceholder = \"Selecione a cidade\",\n disabled = false,\n layout = \"row\",\n}: BrazilStateCitySelectProps): ReactElement {\n const [uf, setUf] = useState<UF | null>(defaultUf ?? null);\n const [city, setCity] = useState<string | null>(defaultCity ?? null);\n\n const stateOptions = useMemo(\n () => listStates().map((s) => ({ value: s.uf, label: s.name })),\n [],\n );\n const cityOptions = useMemo(() => (uf ? cityChoices(uf) : []), [uf]);\n\n function handleUf(next: string): void {\n const nextUf = normalizeUf(next);\n setUf(nextUf);\n setCity(null);\n onChange?.({ uf: nextUf, city: null });\n }\n\n function handleCity(next: string): void {\n const nextCity = next || null;\n setCity(nextCity);\n onChange?.({ uf, city: nextCity });\n }\n\n return (\n <div\n style={{\n display: \"flex\",\n flexDirection: layout === \"row\" ? \"row\" : \"column\",\n gap: \"var(--tempest-space-3, 12px)\",\n flexWrap: \"wrap\",\n }}\n >\n <Select\n label={stateLabel}\n placeholder={statePlaceholder}\n options={stateOptions}\n value={uf ?? \"\"}\n disabled={disabled}\n onChange={(e) => handleUf(e.target.value)}\n style={{ minWidth: 0 }}\n />\n <Select\n label={cityLabel}\n placeholder={cityPlaceholder}\n options={cityOptions}\n value={city ?? \"\"}\n disabled={disabled || !uf}\n onChange={(e) => handleCity(e.target.value)}\n style={{ minWidth: 0 }}\n />\n </div>\n );\n}\n"],"mappings":"sIAiDA,SAAgB,EAAsB,CAClC,YACA,cACA,WACA,aAAa,SACb,YAAY,SACZ,mBAAmB,qBACnB,kBAAkB,qBAClB,WAAW,GACX,SAAS,OACgC,CACzC,GAAM,CAAC,EAAI,IAAA,EAAA,EAAA,SAAA,CAA6B,GAAa,IAAI,EACnD,CAAC,EAAM,IAAA,EAAA,EAAA,SAAA,CAAmC,GAAe,IAAI,EAE7D,GAAA,EAAA,EAAA,QAAA,KACI,EAAA,WAAW,CAAC,CAAC,IAAK,IAAO,CAAE,MAAO,EAAE,GAAI,MAAO,EAAE,IAAK,EAAE,EAC9D,CAAC,CACL,EACM,GAAA,EAAA,EAAA,QAAA,KAA6B,EAAK,EAAA,YAAY,CAAE,EAAI,CAAC,EAAI,CAAC,CAAE,CAAC,EAEnE,SAAS,EAAS,EAAoB,CAClC,IAAM,EAAS,EAAA,YAAY,CAAI,EAC/B,EAAM,CAAM,EACZ,EAAQ,IAAI,EACZ,IAAW,CAAE,GAAI,EAAQ,KAAM,IAAK,CAAC,CACzC,CAEA,SAAS,EAAW,EAAoB,CACpC,IAAM,EAAW,GAAQ,KACzB,EAAQ,CAAQ,EAChB,IAAW,CAAE,KAAI,KAAM,CAAS,CAAC,CACrC,CAEA,OACI,EAAA,EAAA,KAAA,CAAC,MAAD,CACI,MAAO,CACH,QAAS,OACT,cAAe,IAAW,MAAQ,MAAQ,SAC1C,IAAK,+BACL,SAAU,MACd,WANJ,EAQI,EAAA,EAAA,IAAA,CAAC,EAAA,OAAD,CACI,MAAO,EACP,YAAa,EACb,QAAS,EACT,MAAO,GAAM,GACH,WACV,SAAW,GAAM,EAAS,EAAE,OAAO,KAAK,EACxC,MAAO,CAAE,SAAU,CAAE,CACxB,CAAA,GACD,EAAA,EAAA,IAAA,CAAC,EAAA,OAAD,CACI,MAAO,EACP,YAAa,EACb,QAAS,EACT,MAAO,GAAQ,GACf,SAAU,GAAY,CAAC,EACvB,SAAW,GAAM,EAAW,EAAE,OAAO,KAAK,EAC1C,MAAO,CAAE,SAAU,CAAE,CACxB,CAAA,CACA,GAEb"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"BrazilStateCitySelect.js","names":[],"sources":["../../src/br/BrazilStateCitySelect.tsx"],"sourcesContent":["
|
|
1
|
+
{"version":3,"file":"BrazilStateCitySelect.js","names":[],"sources":["../../src/br/BrazilStateCitySelect.tsx"],"sourcesContent":["/**\n * @tempest-limits props-count — two dependent selects in one control, so every knob\n * comes in pairs: stateLabel and cityLabel, statePlaceholder and cityPlaceholder,\n * defaultUf and defaultCity. The pairing is the feature — the city list is derived\n * from the chosen UF, which is exactly what a caller wiring two separate selects has\n * to reimplement.\n */\nimport { useMemo, useState, type ReactElement } from \"react\";\nimport { Select } from \"@/components/Select\";\nimport { cityChoices, listStates, normalizeUf, type UF } from \"./locations\";\n\n/** Current selection emitted by {@link BrazilStateCitySelect}. */\nexport interface BrazilStateCitySelection {\n /** Selected federative unit, or `null` when none. */\n uf: UF | null;\n /** Selected city, or `null` when none. */\n city: string | null;\n}\n\nexport interface BrazilStateCitySelectProps {\n /** Pre-selected UF (uncontrolled initial value). */\n defaultUf?: UF;\n /** Pre-selected city (uncontrolled initial value). */\n defaultCity?: string;\n /** Fired whenever the state or city changes. */\n onChange?: (selection: BrazilStateCitySelection) => void;\n /** Label for the state select. Default: `\"Estado\"`. */\n stateLabel?: string;\n /** Label for the city select. Default: `\"Cidade\"`. */\n cityLabel?: string;\n /** Placeholder for the state select. Default: `\"Selecione o estado\"`. */\n statePlaceholder?: string;\n /** Placeholder for the city select. Default: `\"Selecione a cidade\"`. */\n cityPlaceholder?: string;\n /** Disable both selects. */\n disabled?: boolean;\n /** Layout of the two selects. Default: `\"row\"`. */\n layout?: \"row\" | \"column\";\n}\n\n/**\n * Cascading Estado → Cidade selector backed by the bundled BR locations data —\n * pick a state and the city list narrows to that UF's municipalities. No\n * network, no external API. Complements {@link BrazilMap} (wire the map's\n * `onSelect` to drive the same UF).\n *\n * @example\n * <BrazilStateCitySelect onChange={({ uf, city }) => console.log(uf, city)} />\n */\nexport function BrazilStateCitySelect({\n defaultUf,\n defaultCity,\n onChange,\n stateLabel = \"Estado\",\n cityLabel = \"Cidade\",\n statePlaceholder = \"Selecione o estado\",\n cityPlaceholder = \"Selecione a cidade\",\n disabled = false,\n layout = \"row\",\n}: BrazilStateCitySelectProps): ReactElement {\n const [uf, setUf] = useState<UF | null>(defaultUf ?? null);\n const [city, setCity] = useState<string | null>(defaultCity ?? null);\n\n const stateOptions = useMemo(\n () => listStates().map((s) => ({ value: s.uf, label: s.name })),\n [],\n );\n const cityOptions = useMemo(() => (uf ? cityChoices(uf) : []), [uf]);\n\n function handleUf(next: string): void {\n const nextUf = normalizeUf(next);\n setUf(nextUf);\n setCity(null);\n onChange?.({ uf: nextUf, city: null });\n }\n\n function handleCity(next: string): void {\n const nextCity = next || null;\n setCity(nextCity);\n onChange?.({ uf, city: nextCity });\n }\n\n return (\n <div\n style={{\n display: \"flex\",\n flexDirection: layout === \"row\" ? \"row\" : \"column\",\n gap: \"var(--tempest-space-3, 12px)\",\n flexWrap: \"wrap\",\n }}\n >\n <Select\n label={stateLabel}\n placeholder={statePlaceholder}\n options={stateOptions}\n value={uf ?? \"\"}\n disabled={disabled}\n onChange={(e) => handleUf(e.target.value)}\n style={{ minWidth: 0 }}\n />\n <Select\n label={cityLabel}\n placeholder={cityPlaceholder}\n options={cityOptions}\n value={city ?? \"\"}\n disabled={disabled || !uf}\n onChange={(e) => handleCity(e.target.value)}\n style={{ minWidth: 0 }}\n />\n </div>\n );\n}\n"],"mappings":";;;;;AAiDA,SAAgB,EAAsB,EAClC,cACA,gBACA,aACA,gBAAa,UACb,eAAY,UACZ,sBAAmB,sBACnB,qBAAkB,sBAClB,cAAW,IACX,YAAS,SACgC;CACzC,IAAM,CAAC,GAAI,KAAS,EAAoB,KAAa,IAAI,GACnD,CAAC,GAAM,KAAW,EAAwB,KAAe,IAAI,GAE7D,IAAe,QACX,EAAW,CAAC,CAAC,KAAK,OAAO;EAAE,OAAO,EAAE;EAAI,OAAO,EAAE;CAAK,EAAE,GAC9D,CAAC,CACL,GACM,IAAc,QAAe,IAAK,EAAY,CAAE,IAAI,CAAC,GAAI,CAAC,CAAE,CAAC;CAEnE,SAAS,EAAS,GAAoB;EAClC,IAAM,IAAS,EAAY,CAAI;EAG/B,AAFA,EAAM,CAAM,GACZ,EAAQ,IAAI,GACZ,IAAW;GAAE,IAAI;GAAQ,MAAM;EAAK,CAAC;CACzC;CAEA,SAAS,EAAW,GAAoB;EACpC,IAAM,IAAW,KAAQ;EAEzB,AADA,EAAQ,CAAQ,GAChB,IAAW;GAAE;GAAI,MAAM;EAAS,CAAC;CACrC;CAEA,OACI,kBAAC,OAAD;EACI,OAAO;GACH,SAAS;GACT,eAAe,MAAW,QAAQ,QAAQ;GAC1C,KAAK;GACL,UAAU;EACd;YANJ,CAQI,kBAAC,GAAD;GACI,OAAO;GACP,aAAa;GACb,SAAS;GACT,OAAO,KAAM;GACH;GACV,WAAW,MAAM,EAAS,EAAE,OAAO,KAAK;GACxC,OAAO,EAAE,UAAU,EAAE;EACxB,CAAA,GACD,kBAAC,GAAD;GACI,OAAO;GACP,aAAa;GACb,SAAS;GACT,OAAO,KAAQ;GACf,UAAU,KAAY,CAAC;GACvB,WAAW,MAAM,EAAW,EAAE,OAAO,KAAK;GAC1C,OAAO,EAAE,UAAU,EAAE;EACxB,CAAA,CACA;;AAEb"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"BrazilStateMap.cjs","names":[],"sources":["../../src/br/BrazilStateMap.tsx"],"sourcesContent":["import { useEffect, useMemo, useRef, useState, type HTMLAttributes, type ReactNode } from \"react\";\nimport { cn } from \"@/utils/cn\";\nimport { fitProjection } from \"@/geo/projection\";\nimport type { GeoMarker } from \"@/geo/types\";\nimport { getState, type UF } from \"./locations\";\nimport { MapMarkers } from \"./MapMarkers\";\nimport { MapTooltip } from \"./MapTooltip\";\nimport type { ColorScale } from \"./scales\";\nimport { useMapZoom } from \"./use-map-zoom\";\nimport { loadStateMunicipalities, type StateMunicipalities } from \"./state-geo\";\nimport { geometriesBounds, geometryCentroid, geometryPath, lerpColor } from \"./svg-utils\";\nimport { useMapHover } from \"./use-map-hover\";\nimport styles from \"./BrazilMap.module.css\";\n\n/** A municipality identified for selection callbacks. */\nexport interface Municipality {\n /** 7-digit IBGE code. */\n id: string;\n /** Municipality name. */\n name: string;\n}\n\n/** Data passed to a {@link BrazilStateMapProps.renderTooltip} callback. */\nexport interface BrazilStateMapTooltipData extends Municipality {\n /** Choropleth value for this municipality, if `values` was provided. */\n value?: number;\n}\n\nexport interface BrazilStateMapProps extends Omit<HTMLAttributes<HTMLDivElement>, \"onSelect\"> {\n /** Federative unit to draw (required). */\n uf: UF;\n /** Selected municipality — matched by IBGE `id` or by `name`. Accepts many. */\n selected?: string | readonly string[] | null;\n /** Fired when a municipality is clicked. */\n onSelect?: (municipality: Municipality) => void;\n /**\n * Choropleth values keyed by municipality `id` **or** `name`. When set, each\n * municipality is tinted between `minColor` and `maxColor`.\n */\n values?: Record<string, number>;\n /** Choropleth low-end color (2-color linear ramp). Default: a light primary tint. */\n minColor?: string;\n /** Choropleth high-end color (2-color linear ramp). Default: the primary token. */\n maxColor?: string;\n /**\n * Custom value→color scale (from `sequentialScale`/`quantizeScale`). Takes\n * precedence over `minColor`/`maxColor`. Pair with a `<MapLegend>`.\n */\n colorScale?: ColorScale;\n /** Viewport height in pixels. Default: `440`. */\n height?: number;\n /** Inner padding in pixels. Default: `12`. */\n padding?: number;\n /**\n * Render each municipality name at its centroid. Off by default — a state\n * can have hundreds of municipalities and labels overlap badly.\n */\n showLabels?: boolean;\n /** Accessible label. Default: derived from the state name. */\n label?: string;\n /** Content while the state geometry is loading. */\n loadingContent?: ReactNode;\n /** Show a floating tooltip (name + IBGE code + value) on hover. Default: `true`. */\n showTooltip?: boolean;\n /** Override the default tooltip content. */\n renderTooltip?: (data: BrazilStateMapTooltipData) => ReactNode;\n /** Point markers to overlay on the state (e.g. addresses, POIs). */\n markers?: readonly GeoMarker[];\n /** Fired when a marker is clicked. */\n onMarkerClick?: (marker: GeoMarker, index: number) => void;\n /** Enable wheel-zoom + drag-pan (double-click resets). Default: `false`. */\n zoomable?: boolean;\n}\n\n/**\n * Clickable submap of a single Brazilian state showing **all its\n * municipalities** as SVG paths — no external tiles or paid API. The state's\n * geometry (~40-70 KB gzip) is loaded lazily per UF, so switching states fetches\n * only what is shown.\n *\n * @example\n * const [city, setCity] = useState<string | null>(null);\n * <BrazilStateMap uf=\"SP\" selected={city} onSelect={(m) => setCity(m.name)} />\n *\n * @example\n * // Choropleth of a metric by municipality name\n * <BrazilStateMap uf=\"RJ\" values={{ \"Rio de Janeiro\": 100, Niterói: 40 }} />\n */\nexport function BrazilStateMap({\n uf,\n selected,\n onSelect,\n values,\n minColor = \"#dbeafe\",\n maxColor = \"#2563eb\",\n colorScale,\n height = 440,\n padding = 12,\n showLabels = false,\n label,\n loadingContent,\n showTooltip = true,\n renderTooltip,\n markers,\n onMarkerClick,\n zoomable = false,\n className,\n style,\n ...rest\n}: BrazilStateMapProps) {\n const containerRef = useRef<HTMLDivElement>(null);\n const [width, setWidth] = useState<number>(600);\n const [collection, setCollection] = useState<StateMunicipalities | null>(null);\n const mapZoom = useMapZoom(zoomable);\n const { hover, onMove, onLeave } = useMapHover<Municipality>(containerRef);\n\n useEffect(() => {\n let active = true;\n setCollection(null);\n void loadStateMunicipalities(uf).then((data) => {\n if (active) setCollection(data);\n });\n return () => {\n active = false;\n };\n }, [uf]);\n\n useEffect(() => {\n const node = containerRef.current;\n if (!node || typeof ResizeObserver === \"undefined\") return;\n const observer = new ResizeObserver((entries) => {\n const measured = entries[0]?.contentRect.width;\n if (measured && measured > 0) setWidth(measured);\n });\n observer.observe(node);\n return () => observer.disconnect();\n }, []);\n\n const selectedSet = useMemo<Set<string>>(() => {\n if (!selected) return new Set();\n return new Set(Array.isArray(selected) ? selected : [selected as string]);\n }, [selected]);\n\n const valueRange = useMemo<[number, number] | null>(() => {\n if (!values) return null;\n const nums = Object.values(values).filter((v): v is number => typeof v === \"number\");\n if (nums.length === 0) return null;\n return [Math.min(...nums), Math.max(...nums)];\n }, [values]);\n\n const shapes = useMemo(() => {\n if (!collection) return null;\n const projection = fitProjection(\n geometriesBounds(collection.features.map((f) => f.geometry)),\n width,\n height,\n { padding },\n );\n const items = collection.features.map((feature) => ({\n id: feature.properties.id,\n name: feature.properties.name,\n d: geometryPath(feature.geometry, projection),\n centroid: geometryCentroid(feature.geometry, projection),\n }));\n return { projection, items };\n }, [collection, width, height, padding]);\n\n function fillFor(id: string, name: string): string | undefined {\n const v = values?.[id] ?? values?.[name];\n if (typeof v !== \"number\") return undefined;\n if (colorScale) return colorScale(v);\n if (!valueRange) return undefined;\n const [min, max] = valueRange;\n const t = max === min ? 1 : (v - min) / (max - min);\n return lerpColor(minColor, maxColor, t);\n }\n\n // Memoized so hover re-renders don't rebuild hundreds of municipality paths.\n const cityEls = useMemo(() => {\n if (!shapes) return null;\n return shapes.items.map((shape) => {\n const isSelected = selectedSet.has(shape.id) || selectedSet.has(shape.name);\n const fillColor = isSelected ? undefined : fillFor(shape.id, shape.name);\n return (\n <path\n key={shape.id}\n className={cn(styles.state, isSelected && styles.selected)}\n d={shape.d}\n fillRule=\"evenodd\"\n style={fillColor ? { fill: fillColor } : undefined}\n data-city-id={shape.id}\n data-city={shape.name}\n tabIndex={onSelect ? 0 : undefined}\n role={onSelect ? \"button\" : undefined}\n aria-label={shape.name}\n aria-pressed={onSelect ? isSelected : undefined}\n onClick={\n onSelect ? () => onSelect({ id: shape.id, name: shape.name }) : undefined\n }\n onMouseMove={\n showTooltip\n ? (e) => onMove({ id: shape.id, name: shape.name }, e)\n : undefined\n }\n onMouseLeave={showTooltip ? onLeave : undefined}\n onKeyDown={\n onSelect\n ? (e) => {\n if (e.key === \"Enter\" || e.key === \" \") {\n e.preventDefault();\n onSelect({ id: shape.id, name: shape.name });\n }\n }\n : undefined\n }\n />\n );\n });\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [\n shapes,\n selectedSet,\n values,\n colorScale,\n minColor,\n maxColor,\n onSelect,\n showTooltip,\n onMove,\n onLeave,\n ]);\n\n const resolvedLabel = label ?? `Municípios de ${getState(uf)?.name ?? uf}`;\n\n return (\n <div\n ref={containerRef}\n className={cn(styles.map, className)}\n style={{ height, ...style }}\n role=\"group\"\n aria-label={resolvedLabel}\n {...rest}\n >\n {!shapes ? (\n <div className={styles.loading}>{loadingContent ?? \"Carregando municípios…\"}</div>\n ) : (\n <svg\n className={styles.svg}\n width=\"100%\"\n height={height}\n viewBox={`0 0 ${width} ${height}`}\n preserveAspectRatio=\"xMidYMid meet\"\n {...mapZoom.handlers}\n >\n <g transform={mapZoom.transform || undefined}>\n {cityEls}\n\n {showLabels &&\n shapes.items.map((shape) => (\n <text\n key={`label-${shape.id}`}\n className={styles.label}\n x={shape.centroid.x}\n y={shape.centroid.y}\n textAnchor=\"middle\"\n dominantBaseline=\"central\"\n >\n {shape.name}\n </text>\n ))}\n\n {markers && markers.length > 0 && (\n <MapMarkers\n projection={shapes.projection}\n markers={markers}\n onMarkerClick={onMarkerClick}\n />\n )}\n </g>\n </svg>\n )}\n\n {mapZoom.isTransformed && (\n <button type=\"button\" className={styles.zoomReset} onClick={mapZoom.reset}>\n Reset\n </button>\n )}\n\n {showTooltip && hover && (\n <MapTooltip x={hover.x} y={hover.y}>\n {renderTooltip ? (\n renderTooltip({\n ...hover.item,\n value: values?.[hover.item.id] ?? values?.[hover.item.name],\n })\n ) : (\n <>\n <div className={styles.tooltipTitle}>{hover.item.name}</div>\n <div className={styles.tooltipMeta}>\n IBGE {hover.item.id}\n {(() => {\n const v = values?.[hover.item.id] ?? values?.[hover.item.name];\n return typeof v === \"number\" ? ` · ${v}` : \"\";\n })()}\n </div>\n </>\n )}\n </MapTooltip>\n )}\n </div>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"BrazilStateMap.cjs","names":[],"sources":["../../src/br/BrazilStateMap.tsx"],"sourcesContent":["/**\n * @tempest-limits file-lines, props-count, function-lines — the municipality-level\n * twin of BrazilMap, prop for prop, plus the `uf` that selects which state's\n * geometry to load. Splitting it differently from BrazilMap would mean two maps with\n * two shapes for the same job.\n */\nimport { useEffect, useMemo, useRef, useState, type HTMLAttributes, type ReactNode } from \"react\";\nimport { cn } from \"@/utils/cn\";\nimport { fitProjection } from \"@/geo/projection\";\nimport type { GeoMarker } from \"@/geo/types\";\nimport { getState, type UF } from \"./locations\";\nimport { MapMarkers } from \"./MapMarkers\";\nimport { MapTooltip } from \"./MapTooltip\";\nimport type { ColorScale } from \"./scales\";\nimport { useMapZoom } from \"./use-map-zoom\";\nimport { loadStateMunicipalities, type StateMunicipalities } from \"./state-geo\";\nimport { geometriesBounds, geometryCentroid, geometryPath, lerpColor } from \"./svg-utils\";\nimport { useMapHover } from \"./use-map-hover\";\nimport styles from \"./BrazilMap.module.css\";\n\n/** A municipality identified for selection callbacks. */\nexport interface Municipality {\n /** 7-digit IBGE code. */\n id: string;\n /** Municipality name. */\n name: string;\n}\n\n/** Data passed to a {@link BrazilStateMapProps.renderTooltip} callback. */\nexport interface BrazilStateMapTooltipData extends Municipality {\n /** Choropleth value for this municipality, if `values` was provided. */\n value?: number;\n}\n\nexport interface BrazilStateMapProps extends Omit<HTMLAttributes<HTMLDivElement>, \"onSelect\"> {\n /** Federative unit to draw (required). */\n uf: UF;\n /** Selected municipality — matched by IBGE `id` or by `name`. Accepts many. */\n selected?: string | readonly string[] | null;\n /** Fired when a municipality is clicked. */\n onSelect?: (municipality: Municipality) => void;\n /**\n * Choropleth values keyed by municipality `id` **or** `name`. When set, each\n * municipality is tinted between `minColor` and `maxColor`.\n */\n values?: Record<string, number>;\n /** Choropleth low-end color (2-color linear ramp). Default: a light primary tint. */\n minColor?: string;\n /** Choropleth high-end color (2-color linear ramp). Default: the primary token. */\n maxColor?: string;\n /**\n * Custom value→color scale (from `sequentialScale`/`quantizeScale`). Takes\n * precedence over `minColor`/`maxColor`. Pair with a `<MapLegend>`.\n */\n colorScale?: ColorScale;\n /** Viewport height in pixels. Default: `440`. */\n height?: number;\n /** Inner padding in pixels. Default: `12`. */\n padding?: number;\n /**\n * Render each municipality name at its centroid. Off by default — a state\n * can have hundreds of municipalities and labels overlap badly.\n */\n showLabels?: boolean;\n /** Accessible label. Default: derived from the state name. */\n label?: string;\n /** Content while the state geometry is loading. */\n loadingContent?: ReactNode;\n /** Show a floating tooltip (name + IBGE code + value) on hover. Default: `true`. */\n showTooltip?: boolean;\n /** Override the default tooltip content. */\n renderTooltip?: (data: BrazilStateMapTooltipData) => ReactNode;\n /** Point markers to overlay on the state (e.g. addresses, POIs). */\n markers?: readonly GeoMarker[];\n /** Fired when a marker is clicked. */\n onMarkerClick?: (marker: GeoMarker, index: number) => void;\n /** Enable wheel-zoom + drag-pan (double-click resets). Default: `false`. */\n zoomable?: boolean;\n}\n\n/**\n * Clickable submap of a single Brazilian state showing **all its\n * municipalities** as SVG paths — no external tiles or paid API. The state's\n * geometry (~40-70 KB gzip) is loaded lazily per UF, so switching states fetches\n * only what is shown.\n *\n * @example\n * const [city, setCity] = useState<string | null>(null);\n * <BrazilStateMap uf=\"SP\" selected={city} onSelect={(m) => setCity(m.name)} />\n *\n * @example\n * // Choropleth of a metric by municipality name\n * <BrazilStateMap uf=\"RJ\" values={{ \"Rio de Janeiro\": 100, Niterói: 40 }} />\n */\nexport function BrazilStateMap({\n uf,\n selected,\n onSelect,\n values,\n minColor = \"#dbeafe\",\n maxColor = \"#2563eb\",\n colorScale,\n height = 440,\n padding = 12,\n showLabels = false,\n label,\n loadingContent,\n showTooltip = true,\n renderTooltip,\n markers,\n onMarkerClick,\n zoomable = false,\n className,\n style,\n ...rest\n}: BrazilStateMapProps) {\n const containerRef = useRef<HTMLDivElement>(null);\n const [width, setWidth] = useState<number>(600);\n const [collection, setCollection] = useState<StateMunicipalities | null>(null);\n const mapZoom = useMapZoom(zoomable);\n const { hover, onMove, onLeave } = useMapHover<Municipality>(containerRef);\n\n useEffect(() => {\n let active = true;\n setCollection(null);\n void loadStateMunicipalities(uf).then((data) => {\n if (active) setCollection(data);\n });\n return () => {\n active = false;\n };\n }, [uf]);\n\n useEffect(() => {\n const node = containerRef.current;\n if (!node || typeof ResizeObserver === \"undefined\") return;\n const observer = new ResizeObserver((entries) => {\n const measured = entries[0]?.contentRect.width;\n if (measured && measured > 0) setWidth(measured);\n });\n observer.observe(node);\n return () => observer.disconnect();\n }, []);\n\n const selectedSet = useMemo<Set<string>>(() => {\n if (!selected) return new Set();\n return new Set(Array.isArray(selected) ? selected : [selected as string]);\n }, [selected]);\n\n const valueRange = useMemo<[number, number] | null>(() => {\n if (!values) return null;\n const nums = Object.values(values).filter((v): v is number => typeof v === \"number\");\n if (nums.length === 0) return null;\n return [Math.min(...nums), Math.max(...nums)];\n }, [values]);\n\n const shapes = useMemo(() => {\n if (!collection) return null;\n const projection = fitProjection(\n geometriesBounds(collection.features.map((f) => f.geometry)),\n width,\n height,\n { padding },\n );\n const items = collection.features.map((feature) => ({\n id: feature.properties.id,\n name: feature.properties.name,\n d: geometryPath(feature.geometry, projection),\n centroid: geometryCentroid(feature.geometry, projection),\n }));\n return { projection, items };\n }, [collection, width, height, padding]);\n\n function fillFor(id: string, name: string): string | undefined {\n const v = values?.[id] ?? values?.[name];\n if (typeof v !== \"number\") return undefined;\n if (colorScale) return colorScale(v);\n if (!valueRange) return undefined;\n const [min, max] = valueRange;\n const t = max === min ? 1 : (v - min) / (max - min);\n return lerpColor(minColor, maxColor, t);\n }\n\n // Memoized so hover re-renders don't rebuild hundreds of municipality paths.\n const cityEls = useMemo(() => {\n if (!shapes) return null;\n return shapes.items.map((shape) => {\n const isSelected = selectedSet.has(shape.id) || selectedSet.has(shape.name);\n const fillColor = isSelected ? undefined : fillFor(shape.id, shape.name);\n return (\n <path\n key={shape.id}\n className={cn(styles.state, isSelected && styles.selected)}\n d={shape.d}\n fillRule=\"evenodd\"\n style={fillColor ? { fill: fillColor } : undefined}\n data-city-id={shape.id}\n data-city={shape.name}\n tabIndex={onSelect ? 0 : undefined}\n role={onSelect ? \"button\" : undefined}\n aria-label={shape.name}\n aria-pressed={onSelect ? isSelected : undefined}\n onClick={\n onSelect ? () => onSelect({ id: shape.id, name: shape.name }) : undefined\n }\n onMouseMove={\n showTooltip\n ? (e) => onMove({ id: shape.id, name: shape.name }, e)\n : undefined\n }\n onMouseLeave={showTooltip ? onLeave : undefined}\n onKeyDown={\n onSelect\n ? (e) => {\n if (e.key === \"Enter\" || e.key === \" \") {\n e.preventDefault();\n onSelect({ id: shape.id, name: shape.name });\n }\n }\n : undefined\n }\n />\n );\n });\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [\n shapes,\n selectedSet,\n values,\n colorScale,\n minColor,\n maxColor,\n onSelect,\n showTooltip,\n onMove,\n onLeave,\n ]);\n\n const resolvedLabel = label ?? `Municípios de ${getState(uf)?.name ?? uf}`;\n\n return (\n <div\n ref={containerRef}\n className={cn(styles.map, className)}\n style={{ height, ...style }}\n role=\"group\"\n aria-label={resolvedLabel}\n {...rest}\n >\n {!shapes ? (\n <div className={styles.loading}>{loadingContent ?? \"Carregando municípios…\"}</div>\n ) : (\n <svg\n className={styles.svg}\n width=\"100%\"\n height={height}\n viewBox={`0 0 ${width} ${height}`}\n preserveAspectRatio=\"xMidYMid meet\"\n {...mapZoom.handlers}\n >\n <g transform={mapZoom.transform || undefined}>\n {cityEls}\n\n {showLabels &&\n shapes.items.map((shape) => (\n <text\n key={`label-${shape.id}`}\n className={styles.label}\n x={shape.centroid.x}\n y={shape.centroid.y}\n textAnchor=\"middle\"\n dominantBaseline=\"central\"\n >\n {shape.name}\n </text>\n ))}\n\n {markers && markers.length > 0 && (\n <MapMarkers\n projection={shapes.projection}\n markers={markers}\n onMarkerClick={onMarkerClick}\n />\n )}\n </g>\n </svg>\n )}\n\n {mapZoom.isTransformed && (\n <button type=\"button\" className={styles.zoomReset} onClick={mapZoom.reset}>\n Reset\n </button>\n )}\n\n {showTooltip && hover && (\n <MapTooltip x={hover.x} y={hover.y}>\n {renderTooltip ? (\n renderTooltip({\n ...hover.item,\n value: values?.[hover.item.id] ?? values?.[hover.item.name],\n })\n ) : (\n <>\n <div className={styles.tooltipTitle}>{hover.item.name}</div>\n <div className={styles.tooltipMeta}>\n IBGE {hover.item.id}\n {(() => {\n const v = values?.[hover.item.id] ?? values?.[hover.item.name];\n return typeof v === \"number\" ? ` · ${v}` : \"\";\n })()}\n </div>\n </>\n )}\n </MapTooltip>\n )}\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"BrazilStateMap.js","names":[],"sources":["../../src/br/BrazilStateMap.tsx"],"sourcesContent":["import { useEffect, useMemo, useRef, useState, type HTMLAttributes, type ReactNode } from \"react\";\nimport { cn } from \"@/utils/cn\";\nimport { fitProjection } from \"@/geo/projection\";\nimport type { GeoMarker } from \"@/geo/types\";\nimport { getState, type UF } from \"./locations\";\nimport { MapMarkers } from \"./MapMarkers\";\nimport { MapTooltip } from \"./MapTooltip\";\nimport type { ColorScale } from \"./scales\";\nimport { useMapZoom } from \"./use-map-zoom\";\nimport { loadStateMunicipalities, type StateMunicipalities } from \"./state-geo\";\nimport { geometriesBounds, geometryCentroid, geometryPath, lerpColor } from \"./svg-utils\";\nimport { useMapHover } from \"./use-map-hover\";\nimport styles from \"./BrazilMap.module.css\";\n\n/** A municipality identified for selection callbacks. */\nexport interface Municipality {\n /** 7-digit IBGE code. */\n id: string;\n /** Municipality name. */\n name: string;\n}\n\n/** Data passed to a {@link BrazilStateMapProps.renderTooltip} callback. */\nexport interface BrazilStateMapTooltipData extends Municipality {\n /** Choropleth value for this municipality, if `values` was provided. */\n value?: number;\n}\n\nexport interface BrazilStateMapProps extends Omit<HTMLAttributes<HTMLDivElement>, \"onSelect\"> {\n /** Federative unit to draw (required). */\n uf: UF;\n /** Selected municipality — matched by IBGE `id` or by `name`. Accepts many. */\n selected?: string | readonly string[] | null;\n /** Fired when a municipality is clicked. */\n onSelect?: (municipality: Municipality) => void;\n /**\n * Choropleth values keyed by municipality `id` **or** `name`. When set, each\n * municipality is tinted between `minColor` and `maxColor`.\n */\n values?: Record<string, number>;\n /** Choropleth low-end color (2-color linear ramp). Default: a light primary tint. */\n minColor?: string;\n /** Choropleth high-end color (2-color linear ramp). Default: the primary token. */\n maxColor?: string;\n /**\n * Custom value→color scale (from `sequentialScale`/`quantizeScale`). Takes\n * precedence over `minColor`/`maxColor`. Pair with a `<MapLegend>`.\n */\n colorScale?: ColorScale;\n /** Viewport height in pixels. Default: `440`. */\n height?: number;\n /** Inner padding in pixels. Default: `12`. */\n padding?: number;\n /**\n * Render each municipality name at its centroid. Off by default — a state\n * can have hundreds of municipalities and labels overlap badly.\n */\n showLabels?: boolean;\n /** Accessible label. Default: derived from the state name. */\n label?: string;\n /** Content while the state geometry is loading. */\n loadingContent?: ReactNode;\n /** Show a floating tooltip (name + IBGE code + value) on hover. Default: `true`. */\n showTooltip?: boolean;\n /** Override the default tooltip content. */\n renderTooltip?: (data: BrazilStateMapTooltipData) => ReactNode;\n /** Point markers to overlay on the state (e.g. addresses, POIs). */\n markers?: readonly GeoMarker[];\n /** Fired when a marker is clicked. */\n onMarkerClick?: (marker: GeoMarker, index: number) => void;\n /** Enable wheel-zoom + drag-pan (double-click resets). Default: `false`. */\n zoomable?: boolean;\n}\n\n/**\n * Clickable submap of a single Brazilian state showing **all its\n * municipalities** as SVG paths — no external tiles or paid API. The state's\n * geometry (~40-70 KB gzip) is loaded lazily per UF, so switching states fetches\n * only what is shown.\n *\n * @example\n * const [city, setCity] = useState<string | null>(null);\n * <BrazilStateMap uf=\"SP\" selected={city} onSelect={(m) => setCity(m.name)} />\n *\n * @example\n * // Choropleth of a metric by municipality name\n * <BrazilStateMap uf=\"RJ\" values={{ \"Rio de Janeiro\": 100, Niterói: 40 }} />\n */\nexport function BrazilStateMap({\n uf,\n selected,\n onSelect,\n values,\n minColor = \"#dbeafe\",\n maxColor = \"#2563eb\",\n colorScale,\n height = 440,\n padding = 12,\n showLabels = false,\n label,\n loadingContent,\n showTooltip = true,\n renderTooltip,\n markers,\n onMarkerClick,\n zoomable = false,\n className,\n style,\n ...rest\n}: BrazilStateMapProps) {\n const containerRef = useRef<HTMLDivElement>(null);\n const [width, setWidth] = useState<number>(600);\n const [collection, setCollection] = useState<StateMunicipalities | null>(null);\n const mapZoom = useMapZoom(zoomable);\n const { hover, onMove, onLeave } = useMapHover<Municipality>(containerRef);\n\n useEffect(() => {\n let active = true;\n setCollection(null);\n void loadStateMunicipalities(uf).then((data) => {\n if (active) setCollection(data);\n });\n return () => {\n active = false;\n };\n }, [uf]);\n\n useEffect(() => {\n const node = containerRef.current;\n if (!node || typeof ResizeObserver === \"undefined\") return;\n const observer = new ResizeObserver((entries) => {\n const measured = entries[0]?.contentRect.width;\n if (measured && measured > 0) setWidth(measured);\n });\n observer.observe(node);\n return () => observer.disconnect();\n }, []);\n\n const selectedSet = useMemo<Set<string>>(() => {\n if (!selected) return new Set();\n return new Set(Array.isArray(selected) ? selected : [selected as string]);\n }, [selected]);\n\n const valueRange = useMemo<[number, number] | null>(() => {\n if (!values) return null;\n const nums = Object.values(values).filter((v): v is number => typeof v === \"number\");\n if (nums.length === 0) return null;\n return [Math.min(...nums), Math.max(...nums)];\n }, [values]);\n\n const shapes = useMemo(() => {\n if (!collection) return null;\n const projection = fitProjection(\n geometriesBounds(collection.features.map((f) => f.geometry)),\n width,\n height,\n { padding },\n );\n const items = collection.features.map((feature) => ({\n id: feature.properties.id,\n name: feature.properties.name,\n d: geometryPath(feature.geometry, projection),\n centroid: geometryCentroid(feature.geometry, projection),\n }));\n return { projection, items };\n }, [collection, width, height, padding]);\n\n function fillFor(id: string, name: string): string | undefined {\n const v = values?.[id] ?? values?.[name];\n if (typeof v !== \"number\") return undefined;\n if (colorScale) return colorScale(v);\n if (!valueRange) return undefined;\n const [min, max] = valueRange;\n const t = max === min ? 1 : (v - min) / (max - min);\n return lerpColor(minColor, maxColor, t);\n }\n\n // Memoized so hover re-renders don't rebuild hundreds of municipality paths.\n const cityEls = useMemo(() => {\n if (!shapes) return null;\n return shapes.items.map((shape) => {\n const isSelected = selectedSet.has(shape.id) || selectedSet.has(shape.name);\n const fillColor = isSelected ? undefined : fillFor(shape.id, shape.name);\n return (\n <path\n key={shape.id}\n className={cn(styles.state, isSelected && styles.selected)}\n d={shape.d}\n fillRule=\"evenodd\"\n style={fillColor ? { fill: fillColor } : undefined}\n data-city-id={shape.id}\n data-city={shape.name}\n tabIndex={onSelect ? 0 : undefined}\n role={onSelect ? \"button\" : undefined}\n aria-label={shape.name}\n aria-pressed={onSelect ? isSelected : undefined}\n onClick={\n onSelect ? () => onSelect({ id: shape.id, name: shape.name }) : undefined\n }\n onMouseMove={\n showTooltip\n ? (e) => onMove({ id: shape.id, name: shape.name }, e)\n : undefined\n }\n onMouseLeave={showTooltip ? onLeave : undefined}\n onKeyDown={\n onSelect\n ? (e) => {\n if (e.key === \"Enter\" || e.key === \" \") {\n e.preventDefault();\n onSelect({ id: shape.id, name: shape.name });\n }\n }\n : undefined\n }\n />\n );\n });\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [\n shapes,\n selectedSet,\n values,\n colorScale,\n minColor,\n maxColor,\n onSelect,\n showTooltip,\n onMove,\n onLeave,\n ]);\n\n const resolvedLabel = label ?? `Municípios de ${getState(uf)?.name ?? uf}`;\n\n return (\n <div\n ref={containerRef}\n className={cn(styles.map, className)}\n style={{ height, ...style }}\n role=\"group\"\n aria-label={resolvedLabel}\n {...rest}\n >\n {!shapes ? (\n <div className={styles.loading}>{loadingContent ?? \"Carregando municípios…\"}</div>\n ) : (\n <svg\n className={styles.svg}\n width=\"100%\"\n height={height}\n viewBox={`0 0 ${width} ${height}`}\n preserveAspectRatio=\"xMidYMid meet\"\n {...mapZoom.handlers}\n >\n <g transform={mapZoom.transform || undefined}>\n {cityEls}\n\n {showLabels &&\n shapes.items.map((shape) => (\n <text\n key={`label-${shape.id}`}\n className={styles.label}\n x={shape.centroid.x}\n y={shape.centroid.y}\n textAnchor=\"middle\"\n dominantBaseline=\"central\"\n >\n {shape.name}\n </text>\n ))}\n\n {markers && markers.length > 0 && (\n <MapMarkers\n projection={shapes.projection}\n markers={markers}\n onMarkerClick={onMarkerClick}\n />\n )}\n </g>\n </svg>\n )}\n\n {mapZoom.isTransformed && (\n <button type=\"button\" className={styles.zoomReset} onClick={mapZoom.reset}>\n Reset\n </button>\n )}\n\n {showTooltip && hover && (\n <MapTooltip x={hover.x} y={hover.y}>\n {renderTooltip ? (\n renderTooltip({\n ...hover.item,\n value: values?.[hover.item.id] ?? values?.[hover.item.name],\n })\n ) : (\n <>\n <div className={styles.tooltipTitle}>{hover.item.name}</div>\n <div className={styles.tooltipMeta}>\n IBGE {hover.item.id}\n {(() => {\n const v = values?.[hover.item.id] ?? values?.[hover.item.name];\n return typeof v === \"number\" ? ` · ${v}` : \"\";\n })()}\n </div>\n </>\n )}\n </MapTooltip>\n )}\n </div>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"BrazilStateMap.js","names":[],"sources":["../../src/br/BrazilStateMap.tsx"],"sourcesContent":["/**\n * @tempest-limits file-lines, props-count, function-lines — the municipality-level\n * twin of BrazilMap, prop for prop, plus the `uf` that selects which state's\n * geometry to load. Splitting it differently from BrazilMap would mean two maps with\n * two shapes for the same job.\n */\nimport { useEffect, useMemo, useRef, useState, type HTMLAttributes, type ReactNode } from \"react\";\nimport { cn } from \"@/utils/cn\";\nimport { fitProjection } from \"@/geo/projection\";\nimport type { GeoMarker } from \"@/geo/types\";\nimport { getState, type UF } from \"./locations\";\nimport { MapMarkers } from \"./MapMarkers\";\nimport { MapTooltip } from \"./MapTooltip\";\nimport type { ColorScale } from \"./scales\";\nimport { useMapZoom } from \"./use-map-zoom\";\nimport { loadStateMunicipalities, type StateMunicipalities } from \"./state-geo\";\nimport { geometriesBounds, geometryCentroid, geometryPath, lerpColor } from \"./svg-utils\";\nimport { useMapHover } from \"./use-map-hover\";\nimport styles from \"./BrazilMap.module.css\";\n\n/** A municipality identified for selection callbacks. */\nexport interface Municipality {\n /** 7-digit IBGE code. */\n id: string;\n /** Municipality name. */\n name: string;\n}\n\n/** Data passed to a {@link BrazilStateMapProps.renderTooltip} callback. */\nexport interface BrazilStateMapTooltipData extends Municipality {\n /** Choropleth value for this municipality, if `values` was provided. */\n value?: number;\n}\n\nexport interface BrazilStateMapProps extends Omit<HTMLAttributes<HTMLDivElement>, \"onSelect\"> {\n /** Federative unit to draw (required). */\n uf: UF;\n /** Selected municipality — matched by IBGE `id` or by `name`. Accepts many. */\n selected?: string | readonly string[] | null;\n /** Fired when a municipality is clicked. */\n onSelect?: (municipality: Municipality) => void;\n /**\n * Choropleth values keyed by municipality `id` **or** `name`. When set, each\n * municipality is tinted between `minColor` and `maxColor`.\n */\n values?: Record<string, number>;\n /** Choropleth low-end color (2-color linear ramp). Default: a light primary tint. */\n minColor?: string;\n /** Choropleth high-end color (2-color linear ramp). Default: the primary token. */\n maxColor?: string;\n /**\n * Custom value→color scale (from `sequentialScale`/`quantizeScale`). Takes\n * precedence over `minColor`/`maxColor`. Pair with a `<MapLegend>`.\n */\n colorScale?: ColorScale;\n /** Viewport height in pixels. Default: `440`. */\n height?: number;\n /** Inner padding in pixels. Default: `12`. */\n padding?: number;\n /**\n * Render each municipality name at its centroid. Off by default — a state\n * can have hundreds of municipalities and labels overlap badly.\n */\n showLabels?: boolean;\n /** Accessible label. Default: derived from the state name. */\n label?: string;\n /** Content while the state geometry is loading. */\n loadingContent?: ReactNode;\n /** Show a floating tooltip (name + IBGE code + value) on hover. Default: `true`. */\n showTooltip?: boolean;\n /** Override the default tooltip content. */\n renderTooltip?: (data: BrazilStateMapTooltipData) => ReactNode;\n /** Point markers to overlay on the state (e.g. addresses, POIs). */\n markers?: readonly GeoMarker[];\n /** Fired when a marker is clicked. */\n onMarkerClick?: (marker: GeoMarker, index: number) => void;\n /** Enable wheel-zoom + drag-pan (double-click resets). Default: `false`. */\n zoomable?: boolean;\n}\n\n/**\n * Clickable submap of a single Brazilian state showing **all its\n * municipalities** as SVG paths — no external tiles or paid API. The state's\n * geometry (~40-70 KB gzip) is loaded lazily per UF, so switching states fetches\n * only what is shown.\n *\n * @example\n * const [city, setCity] = useState<string | null>(null);\n * <BrazilStateMap uf=\"SP\" selected={city} onSelect={(m) => setCity(m.name)} />\n *\n * @example\n * // Choropleth of a metric by municipality name\n * <BrazilStateMap uf=\"RJ\" values={{ \"Rio de Janeiro\": 100, Niterói: 40 }} />\n */\nexport function BrazilStateMap({\n uf,\n selected,\n onSelect,\n values,\n minColor = \"#dbeafe\",\n maxColor = \"#2563eb\",\n colorScale,\n height = 440,\n padding = 12,\n showLabels = false,\n label,\n loadingContent,\n showTooltip = true,\n renderTooltip,\n markers,\n onMarkerClick,\n zoomable = false,\n className,\n style,\n ...rest\n}: BrazilStateMapProps) {\n const containerRef = useRef<HTMLDivElement>(null);\n const [width, setWidth] = useState<number>(600);\n const [collection, setCollection] = useState<StateMunicipalities | null>(null);\n const mapZoom = useMapZoom(zoomable);\n const { hover, onMove, onLeave } = useMapHover<Municipality>(containerRef);\n\n useEffect(() => {\n let active = true;\n setCollection(null);\n void loadStateMunicipalities(uf).then((data) => {\n if (active) setCollection(data);\n });\n return () => {\n active = false;\n };\n }, [uf]);\n\n useEffect(() => {\n const node = containerRef.current;\n if (!node || typeof ResizeObserver === \"undefined\") return;\n const observer = new ResizeObserver((entries) => {\n const measured = entries[0]?.contentRect.width;\n if (measured && measured > 0) setWidth(measured);\n });\n observer.observe(node);\n return () => observer.disconnect();\n }, []);\n\n const selectedSet = useMemo<Set<string>>(() => {\n if (!selected) return new Set();\n return new Set(Array.isArray(selected) ? selected : [selected as string]);\n }, [selected]);\n\n const valueRange = useMemo<[number, number] | null>(() => {\n if (!values) return null;\n const nums = Object.values(values).filter((v): v is number => typeof v === \"number\");\n if (nums.length === 0) return null;\n return [Math.min(...nums), Math.max(...nums)];\n }, [values]);\n\n const shapes = useMemo(() => {\n if (!collection) return null;\n const projection = fitProjection(\n geometriesBounds(collection.features.map((f) => f.geometry)),\n width,\n height,\n { padding },\n );\n const items = collection.features.map((feature) => ({\n id: feature.properties.id,\n name: feature.properties.name,\n d: geometryPath(feature.geometry, projection),\n centroid: geometryCentroid(feature.geometry, projection),\n }));\n return { projection, items };\n }, [collection, width, height, padding]);\n\n function fillFor(id: string, name: string): string | undefined {\n const v = values?.[id] ?? values?.[name];\n if (typeof v !== \"number\") return undefined;\n if (colorScale) return colorScale(v);\n if (!valueRange) return undefined;\n const [min, max] = valueRange;\n const t = max === min ? 1 : (v - min) / (max - min);\n return lerpColor(minColor, maxColor, t);\n }\n\n // Memoized so hover re-renders don't rebuild hundreds of municipality paths.\n const cityEls = useMemo(() => {\n if (!shapes) return null;\n return shapes.items.map((shape) => {\n const isSelected = selectedSet.has(shape.id) || selectedSet.has(shape.name);\n const fillColor = isSelected ? undefined : fillFor(shape.id, shape.name);\n return (\n <path\n key={shape.id}\n className={cn(styles.state, isSelected && styles.selected)}\n d={shape.d}\n fillRule=\"evenodd\"\n style={fillColor ? { fill: fillColor } : undefined}\n data-city-id={shape.id}\n data-city={shape.name}\n tabIndex={onSelect ? 0 : undefined}\n role={onSelect ? \"button\" : undefined}\n aria-label={shape.name}\n aria-pressed={onSelect ? isSelected : undefined}\n onClick={\n onSelect ? () => onSelect({ id: shape.id, name: shape.name }) : undefined\n }\n onMouseMove={\n showTooltip\n ? (e) => onMove({ id: shape.id, name: shape.name }, e)\n : undefined\n }\n onMouseLeave={showTooltip ? onLeave : undefined}\n onKeyDown={\n onSelect\n ? (e) => {\n if (e.key === \"Enter\" || e.key === \" \") {\n e.preventDefault();\n onSelect({ id: shape.id, name: shape.name });\n }\n }\n : undefined\n }\n />\n );\n });\n // eslint-disable-next-line react-hooks/exhaustive-deps\n }, [\n shapes,\n selectedSet,\n values,\n colorScale,\n minColor,\n maxColor,\n onSelect,\n showTooltip,\n onMove,\n onLeave,\n ]);\n\n const resolvedLabel = label ?? `Municípios de ${getState(uf)?.name ?? uf}`;\n\n return (\n <div\n ref={containerRef}\n className={cn(styles.map, className)}\n style={{ height, ...style }}\n role=\"group\"\n aria-label={resolvedLabel}\n {...rest}\n >\n {!shapes ? (\n <div className={styles.loading}>{loadingContent ?? \"Carregando municípios…\"}</div>\n ) : (\n <svg\n className={styles.svg}\n width=\"100%\"\n height={height}\n viewBox={`0 0 ${width} ${height}`}\n preserveAspectRatio=\"xMidYMid meet\"\n {...mapZoom.handlers}\n >\n <g transform={mapZoom.transform || undefined}>\n {cityEls}\n\n {showLabels &&\n shapes.items.map((shape) => (\n <text\n key={`label-${shape.id}`}\n className={styles.label}\n x={shape.centroid.x}\n y={shape.centroid.y}\n textAnchor=\"middle\"\n dominantBaseline=\"central\"\n >\n {shape.name}\n </text>\n ))}\n\n {markers && markers.length > 0 && (\n <MapMarkers\n projection={shapes.projection}\n markers={markers}\n onMarkerClick={onMarkerClick}\n />\n )}\n </g>\n </svg>\n )}\n\n {mapZoom.isTransformed && (\n <button type=\"button\" className={styles.zoomReset} onClick={mapZoom.reset}>\n Reset\n </button>\n )}\n\n {showTooltip && hover && (\n <MapTooltip x={hover.x} y={hover.y}>\n {renderTooltip ? (\n renderTooltip({\n ...hover.item,\n value: values?.[hover.item.id] ?? values?.[hover.item.name],\n })\n ) : (\n <>\n <div className={styles.tooltipTitle}>{hover.item.name}</div>\n <div className={styles.tooltipMeta}>\n IBGE {hover.item.id}\n {(() => {\n const v = values?.[hover.item.id] ?? values?.[hover.item.name];\n return typeof v === \"number\" ? ` · ${v}` : \"\";\n })()}\n </div>\n </>\n )}\n </MapTooltip>\n )}\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PixQRCode.cjs","names":[],"sources":["../../src/br/PixQRCode.tsx"],"sourcesContent":["
|
|
1
|
+
{"version":3,"file":"PixQRCode.cjs","names":[],"sources":["../../src/br/PixQRCode.tsx"],"sourcesContent":["/**\n * @tempest-limits props-count — `pix` and `payload` are the two ways to supply the\n * same code (build it, or bring one already built), `size`/`level` are the symbol,\n * and amountLabel/payeeLabel/labels exist so the surrounding copy can be translated\n * without wrapping the component.\n */\nimport type { HTMLAttributes } from \"react\";\nimport { useMemo } from \"react\";\n\nimport { CopyButton } from \"@/components/CopyButton\";\nimport { QRCode } from \"@/components/QRCode\";\nimport type { QRErrorCorrection } from \"@/components/QRCode\";\nimport { cn } from \"@/utils/cn\";\n\nimport { PixError, pixPayload } from \"./pix\";\nimport type { PixInput } from \"./pix\";\nimport styles from \"./PixQRCode.module.css\";\n\n/** Strings the component renders, so an app can translate them. */\nexport interface PixQRCodeLabels {\n /** Accessible name of the symbol. */\n qr: string;\n /** Copy button, idle. */\n copy: string;\n /** Copy button, just clicked. */\n copied: string;\n}\n\nconst DEFAULT_LABELS: PixQRCodeLabels = {\n qr: \"QR code do Pix\",\n copy: \"Copiar código\",\n copied: \"Copiado\",\n};\n\nexport interface PixQRCodeProps extends Omit<HTMLAttributes<HTMLDivElement>, \"children\"> {\n /**\n * Build the payload from its parts. Mutually exclusive with {@link payload}.\n */\n pix?: PixInput;\n /**\n * A payload your PSP already produced, used verbatim.\n *\n * Prefer this for a dynamic charge: the PSP signed that string, and rebuilding\n * it here from parsed parts would only add a way to get it wrong.\n */\n payload?: string;\n /** Rendered side of the symbol in px, quiet zone included. Default `192`. */\n size?: number;\n /**\n * Error-correction level. Default `\"M\"`.\n *\n * A printed QR that will be photographed at an angle scans better at `\"Q\"` or\n * `\"H\"`; the cost is a denser symbol, so raise the size with it.\n */\n level?: QRErrorCorrection;\n /** Render the copia-e-cola line and its copy button. Default `true`. */\n showCopy?: boolean;\n /** Amount caption, e.g. `\"R$ 25,50\"`. Rendered above the symbol when set. */\n amountLabel?: string;\n /** Payee caption. Rendered under the amount when set. */\n payeeLabel?: string;\n labels?: Partial<PixQRCodeLabels>;\n /** Called after the payload reaches the clipboard. */\n onCopied?: () => void;\n}\n\n/**\n * A Pix QR code with the copia-e-cola string next to it.\n *\n * The two affordances belong together: a QR is unusable on the device that is\n * *showing* it, which is exactly where a mobile checkout puts it, so every real\n * Pix screen ends up needing the copyable string as well. Rendering only the\n * symbol is the single most common mistake in a Pix flow.\n *\n * Everything is computed locally — {@link pixPayload} builds the string and the\n * SDK's own encoder draws the symbol — so the key, the amount and the txid never\n * leave the page. A QR-image service would receive all three.\n *\n * @throws {PixError} When neither `pix` nor `payload` is given, when both are, or\n * when `pix` fails validation. A bad key is a programming or data error rather\n * than a state to render, so it surfaces; wrap the screen in `ErrorBoundary` when\n * the values come from user input.\n *\n * @example\n * <PixQRCode\n * pix={{\n * key: \"loja@tempest.dev\",\n * merchantName: \"Loja Tempest\",\n * merchantCity: \"São Paulo\",\n * amount: 25.5,\n * txid: \"PEDIDO123\",\n * }}\n * amountLabel=\"R$ 25,50\"\n * payeeLabel=\"Loja Tempest\"\n * />\n *\n * @example\n * <PixQRCode payload={charge.brcode} level=\"Q\" size={220} />\n */\nexport function PixQRCode({\n pix,\n payload,\n size = 192,\n level = \"M\",\n showCopy = true,\n amountLabel,\n payeeLabel,\n labels,\n onCopied,\n className,\n ...rest\n}: PixQRCodeProps) {\n const value = useMemo(() => {\n if (payload !== undefined && pix !== undefined) {\n throw new PixError(\"Pass either `pix` or `payload` to PixQRCode, not both.\");\n }\n if (payload !== undefined) return payload.trim();\n if (pix !== undefined) return pixPayload(pix);\n throw new PixError(\"PixQRCode needs either `pix` or `payload`.\");\n }, [pix, payload]);\n\n const text = { ...DEFAULT_LABELS, ...labels };\n\n return (\n <div className={cn(styles.pix, className)} {...rest}>\n {amountLabel !== undefined && <span className={styles.amount}>{amountLabel}</span>}\n {payeeLabel !== undefined && <span className={styles.payee}>{payeeLabel}</span>}\n <div className={styles.symbol}>\n <QRCode value={value} size={size} level={level} label={text.qr} margin={2} />\n </div>\n {showCopy && (\n <div className={styles.copy}>\n <code className={styles.payload} data-testid=\"pix-payload\">\n {value}\n </code>\n <CopyButton value={value} onCopied={onCopied}>\n <span className={styles.copyIdle}>{text.copy}</span>\n <span className={styles.copyDone}>{text.copied}</span>\n </CopyButton>\n </div>\n )}\n </div>\n );\n}\n"],"mappings":"sPA4BA,IAAM,EAAkC,CACpC,GAAI,iBACJ,KAAM,gBACN,OAAQ,SACZ,EAmEA,SAAgB,EAAU,CACtB,MACA,UACA,OAAO,IACP,QAAQ,IACR,WAAW,GACX,cACA,aACA,SACA,WACA,YACA,GAAG,GACY,CACf,IAAM,GAAA,EAAA,EAAA,QAAA,KAAsB,CACxB,GAAI,IAAY,IAAA,IAAa,IAAQ,IAAA,GACjC,MAAM,IAAI,EAAA,SAAS,wDAAwD,EAE/E,GAAI,IAAY,IAAA,GAAW,OAAO,EAAQ,KAAK,EAC/C,GAAI,IAAQ,IAAA,GAAW,OAAO,EAAA,WAAW,CAAG,EAC5C,MAAM,IAAI,EAAA,SAAS,4CAA4C,CACnE,EAAG,CAAC,EAAK,CAAO,CAAC,EAEX,EAAO,CAAE,GAAG,EAAgB,GAAG,CAAO,EAE5C,OACI,EAAA,EAAA,KAAA,CAAC,MAAD,CAAK,UAAW,EAAA,GAAG,EAAA,QAAO,IAAK,CAAS,EAAG,GAAI,WAA/C,CACK,IAAgB,IAAA,KAAa,EAAA,EAAA,IAAA,CAAC,OAAD,CAAM,UAAW,EAAA,QAAO,gBAAS,CAAkB,CAAA,EAChF,IAAe,IAAA,KAAa,EAAA,EAAA,IAAA,CAAC,OAAD,CAAM,UAAW,EAAA,QAAO,eAAQ,CAAiB,CAAA,GAC9E,EAAA,EAAA,IAAA,CAAC,MAAD,CAAK,UAAW,EAAA,QAAO,iBACnB,EAAA,EAAA,IAAA,CAAC,EAAA,OAAD,CAAe,QAAa,OAAa,QAAO,MAAO,EAAK,GAAI,OAAQ,CAAI,CAAA,CAC3E,CAAA,EACJ,IACG,EAAA,EAAA,KAAA,CAAC,MAAD,CAAK,UAAW,EAAA,QAAO,cAAvB,EACI,EAAA,EAAA,IAAA,CAAC,OAAD,CAAM,UAAW,EAAA,QAAO,QAAS,cAAY,uBACxC,CACC,CAAA,GACN,EAAA,EAAA,KAAA,CAAC,EAAA,WAAD,CAAmB,QAAiB,oBAApC,EACI,EAAA,EAAA,IAAA,CAAC,OAAD,CAAM,UAAW,EAAA,QAAO,kBAAW,EAAK,IAAW,CAAA,GACnD,EAAA,EAAA,IAAA,CAAC,OAAD,CAAM,UAAW,EAAA,QAAO,kBAAW,EAAK,MAAa,CAAA,CAC7C,GACX,GAER,GAEb"}
|
package/dist/br/PixQRCode.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"PixQRCode.js","names":[],"sources":["../../src/br/PixQRCode.tsx"],"sourcesContent":["
|
|
1
|
+
{"version":3,"file":"PixQRCode.js","names":[],"sources":["../../src/br/PixQRCode.tsx"],"sourcesContent":["/**\n * @tempest-limits props-count — `pix` and `payload` are the two ways to supply the\n * same code (build it, or bring one already built), `size`/`level` are the symbol,\n * and amountLabel/payeeLabel/labels exist so the surrounding copy can be translated\n * without wrapping the component.\n */\nimport type { HTMLAttributes } from \"react\";\nimport { useMemo } from \"react\";\n\nimport { CopyButton } from \"@/components/CopyButton\";\nimport { QRCode } from \"@/components/QRCode\";\nimport type { QRErrorCorrection } from \"@/components/QRCode\";\nimport { cn } from \"@/utils/cn\";\n\nimport { PixError, pixPayload } from \"./pix\";\nimport type { PixInput } from \"./pix\";\nimport styles from \"./PixQRCode.module.css\";\n\n/** Strings the component renders, so an app can translate them. */\nexport interface PixQRCodeLabels {\n /** Accessible name of the symbol. */\n qr: string;\n /** Copy button, idle. */\n copy: string;\n /** Copy button, just clicked. */\n copied: string;\n}\n\nconst DEFAULT_LABELS: PixQRCodeLabels = {\n qr: \"QR code do Pix\",\n copy: \"Copiar código\",\n copied: \"Copiado\",\n};\n\nexport interface PixQRCodeProps extends Omit<HTMLAttributes<HTMLDivElement>, \"children\"> {\n /**\n * Build the payload from its parts. Mutually exclusive with {@link payload}.\n */\n pix?: PixInput;\n /**\n * A payload your PSP already produced, used verbatim.\n *\n * Prefer this for a dynamic charge: the PSP signed that string, and rebuilding\n * it here from parsed parts would only add a way to get it wrong.\n */\n payload?: string;\n /** Rendered side of the symbol in px, quiet zone included. Default `192`. */\n size?: number;\n /**\n * Error-correction level. Default `\"M\"`.\n *\n * A printed QR that will be photographed at an angle scans better at `\"Q\"` or\n * `\"H\"`; the cost is a denser symbol, so raise the size with it.\n */\n level?: QRErrorCorrection;\n /** Render the copia-e-cola line and its copy button. Default `true`. */\n showCopy?: boolean;\n /** Amount caption, e.g. `\"R$ 25,50\"`. Rendered above the symbol when set. */\n amountLabel?: string;\n /** Payee caption. Rendered under the amount when set. */\n payeeLabel?: string;\n labels?: Partial<PixQRCodeLabels>;\n /** Called after the payload reaches the clipboard. */\n onCopied?: () => void;\n}\n\n/**\n * A Pix QR code with the copia-e-cola string next to it.\n *\n * The two affordances belong together: a QR is unusable on the device that is\n * *showing* it, which is exactly where a mobile checkout puts it, so every real\n * Pix screen ends up needing the copyable string as well. Rendering only the\n * symbol is the single most common mistake in a Pix flow.\n *\n * Everything is computed locally — {@link pixPayload} builds the string and the\n * SDK's own encoder draws the symbol — so the key, the amount and the txid never\n * leave the page. A QR-image service would receive all three.\n *\n * @throws {PixError} When neither `pix` nor `payload` is given, when both are, or\n * when `pix` fails validation. A bad key is a programming or data error rather\n * than a state to render, so it surfaces; wrap the screen in `ErrorBoundary` when\n * the values come from user input.\n *\n * @example\n * <PixQRCode\n * pix={{\n * key: \"loja@tempest.dev\",\n * merchantName: \"Loja Tempest\",\n * merchantCity: \"São Paulo\",\n * amount: 25.5,\n * txid: \"PEDIDO123\",\n * }}\n * amountLabel=\"R$ 25,50\"\n * payeeLabel=\"Loja Tempest\"\n * />\n *\n * @example\n * <PixQRCode payload={charge.brcode} level=\"Q\" size={220} />\n */\nexport function PixQRCode({\n pix,\n payload,\n size = 192,\n level = \"M\",\n showCopy = true,\n amountLabel,\n payeeLabel,\n labels,\n onCopied,\n className,\n ...rest\n}: PixQRCodeProps) {\n const value = useMemo(() => {\n if (payload !== undefined && pix !== undefined) {\n throw new PixError(\"Pass either `pix` or `payload` to PixQRCode, not both.\");\n }\n if (payload !== undefined) return payload.trim();\n if (pix !== undefined) return pixPayload(pix);\n throw new PixError(\"PixQRCode needs either `pix` or `payload`.\");\n }, [pix, payload]);\n\n const text = { ...DEFAULT_LABELS, ...labels };\n\n return (\n <div className={cn(styles.pix, className)} {...rest}>\n {amountLabel !== undefined && <span className={styles.amount}>{amountLabel}</span>}\n {payeeLabel !== undefined && <span className={styles.payee}>{payeeLabel}</span>}\n <div className={styles.symbol}>\n <QRCode value={value} size={size} level={level} label={text.qr} margin={2} />\n </div>\n {showCopy && (\n <div className={styles.copy}>\n <code className={styles.payload} data-testid=\"pix-payload\">\n {value}\n </code>\n <CopyButton value={value} onCopied={onCopied}>\n <span className={styles.copyIdle}>{text.copy}</span>\n <span className={styles.copyDone}>{text.copied}</span>\n </CopyButton>\n </div>\n )}\n </div>\n );\n}\n"],"mappings":";;;;;;;;AA4BA,IAAM,IAAkC;CACpC,IAAI;CACJ,MAAM;CACN,QAAQ;AACZ;AAmEA,SAAgB,EAAU,EACtB,QACA,YACA,UAAO,KACP,WAAQ,KACR,cAAW,IACX,gBACA,eACA,WACA,aACA,cACA,GAAG,KACY;CACf,IAAM,IAAQ,QAAc;EACxB,IAAI,MAAY,KAAA,KAAa,MAAQ,KAAA,GACjC,MAAM,IAAI,EAAS,wDAAwD;EAE/E,IAAI,MAAY,KAAA,GAAW,OAAO,EAAQ,KAAK;EAC/C,IAAI,MAAQ,KAAA,GAAW,OAAO,EAAW,CAAG;EAC5C,MAAM,IAAI,EAAS,4CAA4C;CACnE,GAAG,CAAC,GAAK,CAAO,CAAC,GAEX,IAAO;EAAE,GAAG;EAAgB,GAAG;CAAO;CAE5C,OACI,kBAAC,OAAD;EAAK,WAAW,EAAG,EAAO,KAAK,CAAS;EAAG,GAAI;YAA/C;GACK,MAAgB,KAAA,KAAa,kBAAC,QAAD;IAAM,WAAW,EAAO;cAAS;GAAkB,CAAA;GAChF,MAAe,KAAA,KAAa,kBAAC,QAAD;IAAM,WAAW,EAAO;cAAQ;GAAiB,CAAA;GAC9E,kBAAC,OAAD;IAAK,WAAW,EAAO;cACnB,kBAAC,GAAD;KAAe;KAAa;KAAa;KAAO,OAAO,EAAK;KAAI,QAAQ;IAAI,CAAA;GAC3E,CAAA;GACJ,KACG,kBAAC,OAAD;IAAK,WAAW,EAAO;cAAvB,CACI,kBAAC,QAAD;KAAM,WAAW,EAAO;KAAS,eAAY;eACxC;IACC,CAAA,GACN,kBAAC,GAAD;KAAmB;KAAiB;eAApC,CACI,kBAAC,QAAD;MAAM,WAAW,EAAO;gBAAW,EAAK;KAAW,CAAA,GACnD,kBAAC,QAAD;MAAM,WAAW,EAAO;gBAAW,EAAK;KAAa,CAAA,CAC7C;MACX;;EAER;;AAEb"}
|