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":"RichTextEditor.js","names":[],"sources":["../../src/editor/RichTextEditor.tsx"],"sourcesContent":["import { useEffect } from \"react\";\nimport { EditorContent, useEditor, useEditorState } from \"@tiptap/react\";\nimport StarterKit from \"@tiptap/starter-kit\";\nimport {\n Bold,\n Code,\n Heading1,\n Heading2,\n Italic,\n List,\n ListOrdered,\n Quote,\n Redo2,\n Strikethrough,\n Undo2,\n} from \"lucide-react\";\nimport { cn } from \"@/utils/cn\";\nimport styles from \"./RichTextEditor.module.css\";\n\nexport interface RichTextEditorProps {\n /** Current editor content as an HTML string (controlled). */\n value: string;\n /** Called with the updated HTML whenever the document changes. */\n onChange: (html: string) => void;\n /** Placeholder text shown when the editor is empty. */\n placeholder?: string;\n /** Whether the content is editable. Defaults to `true`. */\n editable?: boolean;\n /** Whether to render the formatting toolbar. Defaults to `true`. */\n toolbar?: boolean;\n /** Extra class names applied to the wrapper element. */\n className?: string;\n}\n\n/**\n * RichTextEditor — a controlled WYSIWYG editor built on tiptap v3.\n *\n * Renders an optional formatting toolbar (bold, italic, strike, code, headings,\n * lists, blockquote, undo/redo) above a ProseMirror-backed editable area. The\n * document is controlled via the `value` (HTML) / `onChange` pair: external\n * changes to `value` are synced into the editor without re-emitting updates.\n *\n * The toolbar's pressed states and the undo/redo `disabled` flags come from\n * `useEditorState`, which subscribes to exactly those values. `useEditor` does\n * not re-render on transactions (the tiptap v3 default), so reading `isActive`\n * and `can()` directly in the render body left every button stale — toggling\n * bold with a collapsed cursor arms the mark without changing the document, so no\n * update event ever brought React back to recompute the state. Subscribing\n * re-renders only when one of the tracked values actually flips, which is also\n * cheaper than re-rendering on every keystroke.\n *\n * Two effects keep the instance aligned with the props: one pushes an external\n * `value` change into the document with `emitUpdate: false` (so syncing never\n * re-emits `onChange`), the other mirrors the `editable` flag.\n */\nexport function RichTextEditor({\n value,\n onChange,\n placeholder,\n editable = true,\n toolbar = true,\n className,\n}: RichTextEditorProps) {\n const editor = useEditor({\n extensions: [StarterKit],\n content: value,\n editable,\n onUpdate: ({ editor: instance }) => onChange(instance.getHTML()),\n });\n\n useEffect(() => {\n if (!editor) return;\n if (value !== editor.getHTML()) {\n editor.commands.setContent(value, { emitUpdate: false });\n }\n }, [editor, value]);\n\n useEffect(() => {\n if (!editor) return;\n editor.setEditable(editable);\n }, [editor, editable]);\n\n const toolbarState = useEditorState({\n editor,\n selector: ({ editor: instance }) => ({\n bold: instance.isActive(\"bold\"),\n italic: instance.isActive(\"italic\"),\n strike: instance.isActive(\"strike\"),\n code: instance.isActive(\"code\"),\n heading1: instance.isActive(\"heading\", { level: 1 }),\n heading2: instance.isActive(\"heading\", { level: 2 }),\n bulletList: instance.isActive(\"bulletList\"),\n orderedList: instance.isActive(\"orderedList\"),\n blockquote: instance.isActive(\"blockquote\"),\n canUndo: instance.can().chain().focus().undo().run(),\n canRedo: instance.can().chain().focus().redo().run(),\n }),\n });\n\n if (!editor || !toolbarState) return null;\n\n const canUndo = toolbarState.canUndo;\n const canRedo = toolbarState.canRedo;\n\n return (\n <div className={cn(styles.wrapper, className)}>\n {toolbar && (\n <div className={styles.toolbar} role=\"toolbar\" aria-label=\"Formatação de texto\">\n <button\n type=\"button\"\n className={cn(styles.button, toolbarState.bold && styles.active)}\n aria-label=\"Negrito\"\n aria-pressed={toolbarState.bold}\n onClick={() => editor.chain().focus().toggleBold().run()}\n >\n <Bold size={16} aria-hidden />\n </button>\n <button\n type=\"button\"\n className={cn(styles.button, toolbarState.italic && styles.active)}\n aria-label=\"Itálico\"\n aria-pressed={toolbarState.italic}\n onClick={() => editor.chain().focus().toggleItalic().run()}\n >\n <Italic size={16} aria-hidden />\n </button>\n <button\n type=\"button\"\n className={cn(styles.button, toolbarState.strike && styles.active)}\n aria-label=\"Tachado\"\n aria-pressed={toolbarState.strike}\n onClick={() => editor.chain().focus().toggleStrike().run()}\n >\n <Strikethrough size={16} aria-hidden />\n </button>\n <button\n type=\"button\"\n className={cn(styles.button, toolbarState.code && styles.active)}\n aria-label=\"Código\"\n aria-pressed={toolbarState.code}\n onClick={() => editor.chain().focus().toggleCode().run()}\n >\n <Code size={16} aria-hidden />\n </button>\n <span className={styles.separator} aria-hidden />\n <button\n type=\"button\"\n className={cn(styles.button, toolbarState.heading1 && styles.active)}\n aria-label=\"Título 1\"\n aria-pressed={toolbarState.heading1}\n onClick={() => editor.chain().focus().toggleHeading({ level: 1 }).run()}\n >\n <Heading1 size={16} aria-hidden />\n </button>\n <button\n type=\"button\"\n className={cn(styles.button, toolbarState.heading2 && styles.active)}\n aria-label=\"Título 2\"\n aria-pressed={toolbarState.heading2}\n onClick={() => editor.chain().focus().toggleHeading({ level: 2 }).run()}\n >\n <Heading2 size={16} aria-hidden />\n </button>\n <span className={styles.separator} aria-hidden />\n <button\n type=\"button\"\n className={cn(styles.button, toolbarState.bulletList && styles.active)}\n aria-label=\"Lista com marcadores\"\n aria-pressed={toolbarState.bulletList}\n onClick={() => editor.chain().focus().toggleBulletList().run()}\n >\n <List size={16} aria-hidden />\n </button>\n <button\n type=\"button\"\n className={cn(styles.button, toolbarState.orderedList && styles.active)}\n aria-label=\"Lista numerada\"\n aria-pressed={toolbarState.orderedList}\n onClick={() => editor.chain().focus().toggleOrderedList().run()}\n >\n <ListOrdered size={16} aria-hidden />\n </button>\n <button\n type=\"button\"\n className={cn(styles.button, toolbarState.blockquote && styles.active)}\n aria-label=\"Citação\"\n aria-pressed={toolbarState.blockquote}\n onClick={() => editor.chain().focus().toggleBlockquote().run()}\n >\n <Quote size={16} aria-hidden />\n </button>\n <span className={styles.separator} aria-hidden />\n <button\n type=\"button\"\n className={styles.button}\n aria-label=\"Desfazer\"\n disabled={!canUndo}\n onClick={() => editor.chain().focus().undo().run()}\n >\n <Undo2 size={16} aria-hidden />\n </button>\n <button\n type=\"button\"\n className={styles.button}\n aria-label=\"Refazer\"\n disabled={!canRedo}\n onClick={() => editor.chain().focus().redo().run()}\n >\n <Redo2 size={16} aria-hidden />\n </button>\n </div>\n )}\n <EditorContent\n editor={editor}\n className={styles.content}\n data-placeholder={placeholder}\n />\n </div>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"RichTextEditor.js","names":[],"sources":["../../src/editor/RichTextEditor.tsx"],"sourcesContent":["/**\n * @tempest-limits file-lines, function-lines — the body builds the tiptap editor,\n * its toolbar state and the change plumbing: every toolbar button reads\n * `editor.isActive`, so the toolbar cannot be lifted out without passing the editor\n * instance back down.\n */\nimport { useEffect } from \"react\";\nimport { EditorContent, useEditor, useEditorState } from \"@tiptap/react\";\nimport StarterKit from \"@tiptap/starter-kit\";\nimport {\n Bold,\n Code,\n Heading1,\n Heading2,\n Italic,\n List,\n ListOrdered,\n Quote,\n Redo2,\n Strikethrough,\n Undo2,\n} from \"lucide-react\";\nimport { cn } from \"@/utils/cn\";\nimport styles from \"./RichTextEditor.module.css\";\n\nexport interface RichTextEditorProps {\n /** Current editor content as an HTML string (controlled). */\n value: string;\n /** Called with the updated HTML whenever the document changes. */\n onChange: (html: string) => void;\n /** Placeholder text shown when the editor is empty. */\n placeholder?: string;\n /** Whether the content is editable. Defaults to `true`. */\n editable?: boolean;\n /** Whether to render the formatting toolbar. Defaults to `true`. */\n toolbar?: boolean;\n /** Extra class names applied to the wrapper element. */\n className?: string;\n}\n\n/**\n * RichTextEditor — a controlled WYSIWYG editor built on tiptap v3.\n *\n * Renders an optional formatting toolbar (bold, italic, strike, code, headings,\n * lists, blockquote, undo/redo) above a ProseMirror-backed editable area. The\n * document is controlled via the `value` (HTML) / `onChange` pair: external\n * changes to `value` are synced into the editor without re-emitting updates.\n *\n * The toolbar's pressed states and the undo/redo `disabled` flags come from\n * `useEditorState`, which subscribes to exactly those values. `useEditor` does\n * not re-render on transactions (the tiptap v3 default), so reading `isActive`\n * and `can()` directly in the render body left every button stale — toggling\n * bold with a collapsed cursor arms the mark without changing the document, so no\n * update event ever brought React back to recompute the state. Subscribing\n * re-renders only when one of the tracked values actually flips, which is also\n * cheaper than re-rendering on every keystroke.\n *\n * Two effects keep the instance aligned with the props: one pushes an external\n * `value` change into the document with `emitUpdate: false` (so syncing never\n * re-emits `onChange`), the other mirrors the `editable` flag.\n */\nexport function RichTextEditor({\n value,\n onChange,\n placeholder,\n editable = true,\n toolbar = true,\n className,\n}: RichTextEditorProps) {\n const editor = useEditor({\n extensions: [StarterKit],\n content: value,\n editable,\n onUpdate: ({ editor: instance }) => onChange(instance.getHTML()),\n });\n\n useEffect(() => {\n if (!editor) return;\n if (value !== editor.getHTML()) {\n editor.commands.setContent(value, { emitUpdate: false });\n }\n }, [editor, value]);\n\n useEffect(() => {\n if (!editor) return;\n editor.setEditable(editable);\n }, [editor, editable]);\n\n const toolbarState = useEditorState({\n editor,\n selector: ({ editor: instance }) => ({\n bold: instance.isActive(\"bold\"),\n italic: instance.isActive(\"italic\"),\n strike: instance.isActive(\"strike\"),\n code: instance.isActive(\"code\"),\n heading1: instance.isActive(\"heading\", { level: 1 }),\n heading2: instance.isActive(\"heading\", { level: 2 }),\n bulletList: instance.isActive(\"bulletList\"),\n orderedList: instance.isActive(\"orderedList\"),\n blockquote: instance.isActive(\"blockquote\"),\n canUndo: instance.can().chain().focus().undo().run(),\n canRedo: instance.can().chain().focus().redo().run(),\n }),\n });\n\n if (!editor || !toolbarState) return null;\n\n const canUndo = toolbarState.canUndo;\n const canRedo = toolbarState.canRedo;\n\n return (\n <div className={cn(styles.wrapper, className)}>\n {toolbar && (\n <div className={styles.toolbar} role=\"toolbar\" aria-label=\"Formatação de texto\">\n <button\n type=\"button\"\n className={cn(styles.button, toolbarState.bold && styles.active)}\n aria-label=\"Negrito\"\n aria-pressed={toolbarState.bold}\n onClick={() => editor.chain().focus().toggleBold().run()}\n >\n <Bold size={16} aria-hidden />\n </button>\n <button\n type=\"button\"\n className={cn(styles.button, toolbarState.italic && styles.active)}\n aria-label=\"Itálico\"\n aria-pressed={toolbarState.italic}\n onClick={() => editor.chain().focus().toggleItalic().run()}\n >\n <Italic size={16} aria-hidden />\n </button>\n <button\n type=\"button\"\n className={cn(styles.button, toolbarState.strike && styles.active)}\n aria-label=\"Tachado\"\n aria-pressed={toolbarState.strike}\n onClick={() => editor.chain().focus().toggleStrike().run()}\n >\n <Strikethrough size={16} aria-hidden />\n </button>\n <button\n type=\"button\"\n className={cn(styles.button, toolbarState.code && styles.active)}\n aria-label=\"Código\"\n aria-pressed={toolbarState.code}\n onClick={() => editor.chain().focus().toggleCode().run()}\n >\n <Code size={16} aria-hidden />\n </button>\n <span className={styles.separator} aria-hidden />\n <button\n type=\"button\"\n className={cn(styles.button, toolbarState.heading1 && styles.active)}\n aria-label=\"Título 1\"\n aria-pressed={toolbarState.heading1}\n onClick={() => editor.chain().focus().toggleHeading({ level: 1 }).run()}\n >\n <Heading1 size={16} aria-hidden />\n </button>\n <button\n type=\"button\"\n className={cn(styles.button, toolbarState.heading2 && styles.active)}\n aria-label=\"Título 2\"\n aria-pressed={toolbarState.heading2}\n onClick={() => editor.chain().focus().toggleHeading({ level: 2 }).run()}\n >\n <Heading2 size={16} aria-hidden />\n </button>\n <span className={styles.separator} aria-hidden />\n <button\n type=\"button\"\n className={cn(styles.button, toolbarState.bulletList && styles.active)}\n aria-label=\"Lista com marcadores\"\n aria-pressed={toolbarState.bulletList}\n onClick={() => editor.chain().focus().toggleBulletList().run()}\n >\n <List size={16} aria-hidden />\n </button>\n <button\n type=\"button\"\n className={cn(styles.button, toolbarState.orderedList && styles.active)}\n aria-label=\"Lista numerada\"\n aria-pressed={toolbarState.orderedList}\n onClick={() => editor.chain().focus().toggleOrderedList().run()}\n >\n <ListOrdered size={16} aria-hidden />\n </button>\n <button\n type=\"button\"\n className={cn(styles.button, toolbarState.blockquote && styles.active)}\n aria-label=\"Citação\"\n aria-pressed={toolbarState.blockquote}\n onClick={() => editor.chain().focus().toggleBlockquote().run()}\n >\n <Quote size={16} aria-hidden />\n </button>\n <span className={styles.separator} aria-hidden />\n <button\n type=\"button\"\n className={styles.button}\n aria-label=\"Desfazer\"\n disabled={!canUndo}\n onClick={() => editor.chain().focus().undo().run()}\n >\n <Undo2 size={16} aria-hidden />\n </button>\n <button\n type=\"button\"\n className={styles.button}\n aria-label=\"Refazer\"\n disabled={!canRedo}\n onClick={() => editor.chain().focus().redo().run()}\n >\n <Redo2 size={16} aria-hidden />\n </button>\n </div>\n )}\n <EditorContent\n editor={editor}\n className={styles.content}\n data-placeholder={placeholder}\n />\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"FormField.cjs","names":[],"sources":["../../src/forms/FormField.tsx"],"sourcesContent":["
|
|
1
|
+
{"version":3,"file":"FormField.cjs","names":[],"sources":["../../src/forms/FormField.tsx"],"sourcesContent":["/**\n * @tempest-limits props-count — FormFieldChildProps is not a component's props — it\n * is the contract handed to whatever control the field wraps, and it is exactly what\n * react-hook-form's register returns (name, value, onChange, onBlur, ref) plus what\n * the field adds around it (error, label, helperText, required, id). Trimming it\n * breaks the wiring it exists to describe.\n */\nimport { cloneElement, isValidElement } from \"react\";\nimport type { ReactElement, ReactNode } from \"react\";\nimport {\n Controller,\n useFormContext,\n type Control,\n type ControllerRenderProps,\n type FieldPath,\n type FieldValues,\n} from \"react-hook-form\";\n\nexport interface FormFieldChildProps {\n name: string;\n value: unknown;\n onChange: (...args: unknown[]) => void;\n onBlur: () => void;\n ref: ControllerRenderProps[\"ref\"];\n error?: string;\n \"aria-invalid\"?: boolean;\n \"aria-describedby\"?: string;\n label?: ReactNode;\n helperText?: ReactNode;\n required?: boolean;\n id?: string;\n}\n\nexport interface FormFieldProps<\n TValues extends FieldValues = FieldValues,\n TName extends FieldPath<TValues> = FieldPath<TValues>,\n> {\n /** Field name (dot path supported, e.g. `\"address.city\"`). */\n name: TName;\n /** Field label rendered by the wrapped control. */\n label?: ReactNode;\n /** Helper text rendered by the wrapped control when there is no error. */\n helperText?: ReactNode;\n /** When `true`, marks the control as required (visual hint + native attribute). */\n required?: boolean;\n /**\n * Explicit `control` from `useForm()` / `useZodForm()`. Optional when a\n * `FormProvider` is in the tree.\n */\n control?: Control<TValues>;\n /**\n * The control to render. Receives `{ value, onChange, onBlur, ref, error, ... }`\n * via `cloneElement`. Pass `<Input />`, `<Select />`, masked inputs, etc.\n */\n children: ReactElement;\n}\n\n/**\n * Glue between `react-hook-form` `Controller` and the SDK's controlled\n * components. Wraps any control that accepts `{ value, onChange, label,\n * error }` and routes RHF state into it — eliminating the per-field\n * `<Controller render={...} />` boilerplate.\n *\n * @example\n * const form = useZodForm(schema);\n * <FormProvider {...form}>\n * <Form>\n * <FormField name=\"email\" label=\"Email\" required>\n * <Input type=\"email\" />\n * </FormField>\n * <FormField name=\"cep\" label=\"CEP\">\n * <CEPInput />\n * </FormField>\n * </Form>\n * </FormProvider>;\n */\nexport function FormField<\n TValues extends FieldValues = FieldValues,\n TName extends FieldPath<TValues> = FieldPath<TValues>,\n>({ name, label, helperText, required, control, children }: FormFieldProps<TValues, TName>) {\n const context = useFormContext<TValues>();\n const resolvedControl = control ?? context?.control;\n if (!resolvedControl) {\n throw new Error(\n \"FormField requires either a `control` prop or a <FormProvider> in the tree.\",\n );\n }\n\n return (\n <Controller\n name={name}\n control={resolvedControl}\n render={({ field, fieldState }) => {\n if (!isValidElement(children)) return children;\n const errorMessage = fieldState.error?.message;\n return cloneElement(children as ReactElement<FormFieldChildProps>, {\n name: field.name,\n value: field.value,\n onChange: field.onChange,\n onBlur: field.onBlur,\n ref: field.ref,\n label,\n helperText: errorMessage ? undefined : helperText,\n error: errorMessage,\n required,\n \"aria-invalid\": !!errorMessage,\n });\n }}\n />\n );\n}\n"],"mappings":"mFA4EA,SAAgB,EAGd,CAAE,OAAM,QAAO,aAAY,WAAU,UAAS,YAA4C,CACxF,IAAM,GAAA,EAAA,EAAA,eAAA,CAAkC,EAClC,EAAkB,GAAW,GAAS,QAC5C,GAAI,CAAC,EACD,MAAU,MACN,6EACJ,EAGJ,OACI,EAAA,EAAA,IAAA,CAAC,EAAA,WAAD,CACU,OACN,QAAS,EACT,QAAS,CAAE,QAAO,gBAAiB,CAC/B,GAAI,EAAA,EAAA,EAAA,eAAA,CAAgB,CAAQ,EAAG,OAAO,EACtC,IAAM,EAAe,EAAW,OAAO,QACvC,OAAA,EAAA,EAAA,aAAA,CAAoB,EAA+C,CAC/D,KAAM,EAAM,KACZ,MAAO,EAAM,MACb,SAAU,EAAM,SAChB,OAAQ,EAAM,OACd,IAAK,EAAM,IACX,QACA,WAAY,EAAe,IAAA,GAAY,EACvC,MAAO,EACP,WACA,eAAgB,CAAC,CAAC,CACtB,CAAC,CACL,CACH,CAAA,CAET"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"FormField.js","names":[],"sources":["../../src/forms/FormField.tsx"],"sourcesContent":["
|
|
1
|
+
{"version":3,"file":"FormField.js","names":[],"sources":["../../src/forms/FormField.tsx"],"sourcesContent":["/**\n * @tempest-limits props-count — FormFieldChildProps is not a component's props — it\n * is the contract handed to whatever control the field wraps, and it is exactly what\n * react-hook-form's register returns (name, value, onChange, onBlur, ref) plus what\n * the field adds around it (error, label, helperText, required, id). Trimming it\n * breaks the wiring it exists to describe.\n */\nimport { cloneElement, isValidElement } from \"react\";\nimport type { ReactElement, ReactNode } from \"react\";\nimport {\n Controller,\n useFormContext,\n type Control,\n type ControllerRenderProps,\n type FieldPath,\n type FieldValues,\n} from \"react-hook-form\";\n\nexport interface FormFieldChildProps {\n name: string;\n value: unknown;\n onChange: (...args: unknown[]) => void;\n onBlur: () => void;\n ref: ControllerRenderProps[\"ref\"];\n error?: string;\n \"aria-invalid\"?: boolean;\n \"aria-describedby\"?: string;\n label?: ReactNode;\n helperText?: ReactNode;\n required?: boolean;\n id?: string;\n}\n\nexport interface FormFieldProps<\n TValues extends FieldValues = FieldValues,\n TName extends FieldPath<TValues> = FieldPath<TValues>,\n> {\n /** Field name (dot path supported, e.g. `\"address.city\"`). */\n name: TName;\n /** Field label rendered by the wrapped control. */\n label?: ReactNode;\n /** Helper text rendered by the wrapped control when there is no error. */\n helperText?: ReactNode;\n /** When `true`, marks the control as required (visual hint + native attribute). */\n required?: boolean;\n /**\n * Explicit `control` from `useForm()` / `useZodForm()`. Optional when a\n * `FormProvider` is in the tree.\n */\n control?: Control<TValues>;\n /**\n * The control to render. Receives `{ value, onChange, onBlur, ref, error, ... }`\n * via `cloneElement`. Pass `<Input />`, `<Select />`, masked inputs, etc.\n */\n children: ReactElement;\n}\n\n/**\n * Glue between `react-hook-form` `Controller` and the SDK's controlled\n * components. Wraps any control that accepts `{ value, onChange, label,\n * error }` and routes RHF state into it — eliminating the per-field\n * `<Controller render={...} />` boilerplate.\n *\n * @example\n * const form = useZodForm(schema);\n * <FormProvider {...form}>\n * <Form>\n * <FormField name=\"email\" label=\"Email\" required>\n * <Input type=\"email\" />\n * </FormField>\n * <FormField name=\"cep\" label=\"CEP\">\n * <CEPInput />\n * </FormField>\n * </Form>\n * </FormProvider>;\n */\nexport function FormField<\n TValues extends FieldValues = FieldValues,\n TName extends FieldPath<TValues> = FieldPath<TValues>,\n>({ name, label, helperText, required, control, children }: FormFieldProps<TValues, TName>) {\n const context = useFormContext<TValues>();\n const resolvedControl = control ?? context?.control;\n if (!resolvedControl) {\n throw new Error(\n \"FormField requires either a `control` prop or a <FormProvider> in the tree.\",\n );\n }\n\n return (\n <Controller\n name={name}\n control={resolvedControl}\n render={({ field, fieldState }) => {\n if (!isValidElement(children)) return children;\n const errorMessage = fieldState.error?.message;\n return cloneElement(children as ReactElement<FormFieldChildProps>, {\n name: field.name,\n value: field.value,\n onChange: field.onChange,\n onBlur: field.onBlur,\n ref: field.ref,\n label,\n helperText: errorMessage ? undefined : helperText,\n error: errorMessage,\n required,\n \"aria-invalid\": !!errorMessage,\n });\n }}\n />\n );\n}\n"],"mappings":";;;;AA4EA,SAAgB,EAGd,EAAE,SAAM,UAAO,eAAY,aAAU,YAAS,eAA4C;CACxF,IAAM,IAAU,EAAwB,GAClC,IAAkB,KAAW,GAAS;CAC5C,IAAI,CAAC,GACD,MAAU,MACN,6EACJ;CAGJ,OACI,kBAAC,GAAD;EACU;EACN,SAAS;EACT,SAAS,EAAE,UAAO,oBAAiB;GAC/B,IAAI,CAAC,EAAe,CAAQ,GAAG,OAAO;GACtC,IAAM,IAAe,EAAW,OAAO;GACvC,OAAO,EAAa,GAA+C;IAC/D,MAAM,EAAM;IACZ,OAAO,EAAM;IACb,UAAU,EAAM;IAChB,QAAQ,EAAM;IACd,KAAK,EAAM;IACX;IACA,YAAY,IAAe,KAAA,IAAY;IACvC,OAAO;IACP;IACA,gBAAgB,CAAC,CAAC;GACtB,CAAC;EACL;CACH,CAAA;AAET"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TrajectoryMap.cjs","names":[],"sources":["../../src/geo/TrajectoryMap.tsx"],"sourcesContent":["import { lazy, Suspense, useEffect, useMemo, useRef, useState, type HTMLAttributes } from \"react\";\nimport { cn } from \"@/utils/cn\";\nimport { boundingBox, expandBounds } from \"./bounds\";\nimport { haversineKm } from \"./distance\";\nimport { fitProjection, type PixelPoint } from \"./projection\";\nimport type { Coordinate, GeoMarker } from \"./types\";\nimport styles from \"./TrajectoryMap.module.css\";\n\n// Leaflet layer is loaded on demand — only when a `tileUrl` is supplied, so\n// apps that stick to the tile-free SVG never pull leaflet into their bundle.\nconst LeafletTrajectory = lazy(() =>\n import(\"./leaflet-map\").then((m) => ({ default: m.LeafletTrajectory })),\n);\n\nexport interface TrajectoryMapProps extends Omit<HTMLAttributes<HTMLDivElement>, \"children\"> {\n /** Ordered coordinates forming the trajectory. */\n points: readonly Coordinate[];\n /** Optional \"current position\" marker, drawn on top of the path. */\n current?: Coordinate | null;\n /** Viewport height in pixels. Default: `320`. */\n height?: number;\n /** Inner padding in pixels kept clear on every edge. Default: `24`. */\n padding?: number;\n /** Draw a light reference grid behind the path (SVG mode). Default: `true`. */\n showGrid?: boolean;\n /** Draw a distance scale bar (SVG mode). Default: `true`. */\n showScale?: boolean;\n /** Path stroke color. Default: the primary token. */\n strokeColor?: string;\n /**\n * Tile URL template (`{z}/{x}/{y}`) of a map server **you host**. When set,\n * the map upgrades to a real Leaflet tile layer (requires `leaflet` to be\n * installed). Omit for the zero-dependency, zero-network SVG plot.\n */\n tileUrl?: string;\n /** Attribution text shown over the tile layer. */\n tileAttribution?: string;\n /** Accessible label for the map region. Default: `\"Trajetória\"`. */\n label?: string;\n /** Point markers to overlay (e.g. stops, POIs). Included in the auto-fit. */\n markers?: readonly GeoMarker[];\n /** Fired when a marker is clicked. */\n onMarkerClick?: (marker: GeoMarker, index: number) => void;\n}\n\nconst NICE_KM_STEPS = [0.05, 0.1, 0.25, 0.5, 1, 2, 5, 10, 25, 50, 100, 250, 500, 1000] as const;\n\n/** Pick the largest \"nice\" distance whose bar fits within `maxPixels`. */\nfunction pickScaleKm(kmPerPixel: number, maxPixels: number): { km: number; pixels: number } {\n const maxKm = kmPerPixel * maxPixels;\n let chosen: number = NICE_KM_STEPS[0];\n for (const step of NICE_KM_STEPS) {\n if (step <= maxKm) chosen = step;\n }\n return { km: chosen, pixels: chosen / kmPerPixel };\n}\n\n/** Format a distance in km for the scale bar (meters below 1 km). */\nfunction formatScale(km: number): string {\n return km < 1 ? `${Math.round(km * 1000)} m` : `${km % 1 === 0 ? km : km.toFixed(1)} km`;\n}\n\n/**\n * Plot a GPS trajectory. By default it renders a **tile-free SVG** map (Web\n * Mercator projection, auto-fit to the points, optional grid + scale bar) —\n * 100% self-contained, no external tiles or paid API. Pass `tileUrl` pointing\n * at a map server **you host** to upgrade to a real Leaflet tile layer.\n *\n * @example\n * // Zero-dependency SVG plot\n * <TrajectoryMap points={points} current={lastPoint} height={360} />\n *\n * @example\n * // Real tiles from your own server (requires `leaflet` installed)\n * <TrajectoryMap points={points} tileUrl=\"https://tiles.internal/{z}/{x}/{y}.png\" />\n */\nexport function TrajectoryMap({\n points,\n current,\n height = 320,\n padding = 24,\n showGrid = true,\n showScale = true,\n strokeColor,\n tileUrl,\n tileAttribution,\n label = \"Trajetória\",\n markers,\n onMarkerClick,\n className,\n style,\n ...rest\n}: TrajectoryMapProps) {\n const containerRef = useRef<HTMLDivElement>(null);\n const [width, setWidth] = useState<number>(600);\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 allPoints = useMemo<Coordinate[]>(\n () => [...points, ...(current ? [current] : []), ...(markers ?? [])],\n [points, current, markers],\n );\n\n const svg = useMemo(() => {\n const box = boundingBox(allPoints);\n if (!box) return null;\n\n const projection = fitProjection(expandBounds(box, 0.12), width, height, { padding });\n const pixels: PixelPoint[] = points.map((p) => projection.project(p));\n const polyline = pixels.map((p) => `${p.x.toFixed(1)},${p.y.toFixed(1)}`).join(\" \");\n\n const startPixel = pixels[0] ?? null;\n const endPixel = pixels.length > 0 ? pixels[pixels.length - 1] : null;\n const currentPixel = current ? projection.project(current) : null;\n\n // Distance-per-pixel at the map center, for the scale bar: measure the\n // center → east-edge span in both km (haversine) and pixels (projected).\n const centerCoord = {\n latitude: (box.minLatitude + box.maxLatitude) / 2,\n longitude: (box.minLongitude + box.maxLongitude) / 2,\n };\n const center = projection.project(centerCoord);\n const eastRef = projection.project({ ...centerCoord, longitude: box.maxLongitude });\n const edgeKm = haversineKm(centerCoord, { ...centerCoord, longitude: box.maxLongitude });\n const edgePixels = Math.abs(eastRef.x - center.x) || 1;\n const kmPerPixel = edgeKm / edgePixels || 0;\n\n const markerPixels = (markers ?? []).map((m) => ({ marker: m, p: projection.project(m) }));\n\n return { polyline, startPixel, endPixel, currentPixel, kmPerPixel, markerPixels };\n }, [allPoints, points, current, markers, width, height, padding]);\n\n // ── Leaflet tile mode ────────────────────────────────────────────────────\n if (tileUrl) {\n return (\n <div\n ref={containerRef}\n className={cn(styles.map, className)}\n style={{ height, ...style }}\n role=\"img\"\n aria-label={label}\n {...rest}\n >\n <Suspense fallback={<div className={styles.loading}>Carregando mapa…</div>}>\n <LeafletTrajectory\n points={points}\n current={current}\n tileUrl={tileUrl}\n tileAttribution={tileAttribution}\n strokeColor={strokeColor}\n />\n </Suspense>\n </div>\n );\n }\n\n // ── Tile-free SVG mode ─────────────────────────────────────────────────────\n const scale =\n showScale && svg && svg.kmPerPixel > 0\n ? pickScaleKm(svg.kmPerPixel, Math.min(120, width * 0.35))\n : null;\n\n return (\n <div\n ref={containerRef}\n className={cn(styles.map, className)}\n style={{ height, ...style }}\n role=\"img\"\n aria-label={label}\n {...rest}\n >\n {!svg ? (\n <div className={styles.empty}>Sem pontos de trajetória ainda.</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 >\n {showGrid && (\n <g className={styles.grid}>\n {[0.25, 0.5, 0.75].map((f) => (\n <line\n key={`h${f}`}\n x1={0}\n y1={height * f}\n x2={width}\n y2={height * f}\n />\n ))}\n {[0.25, 0.5, 0.75].map((f) => (\n <line\n key={`v${f}`}\n x1={width * f}\n y1={0}\n x2={width * f}\n y2={height}\n />\n ))}\n </g>\n )}\n\n {svg.polyline && (\n <polyline\n className={styles.path}\n points={svg.polyline}\n fill=\"none\"\n stroke={strokeColor}\n data-testid=\"trajectory-path\"\n />\n )}\n\n {svg.startPixel && (\n <circle\n className={styles.start}\n cx={svg.startPixel.x}\n cy={svg.startPixel.y}\n r={5}\n />\n )}\n {svg.endPixel && !current && (\n <circle\n className={styles.end}\n cx={svg.endPixel.x}\n cy={svg.endPixel.y}\n r={5}\n />\n )}\n {svg.currentPixel && (\n <>\n <circle\n className={styles.pulse}\n cx={svg.currentPixel.x}\n cy={svg.currentPixel.y}\n r={10}\n />\n <circle\n className={styles.current}\n cx={svg.currentPixel.x}\n cy={svg.currentPixel.y}\n r={5}\n />\n </>\n )}\n\n {svg.markerPixels.map(({ marker, p }, i) => {\n const interactive = Boolean(onMarkerClick);\n return (\n <circle\n key={marker.id ?? i}\n className={styles.marker}\n cx={p.x}\n cy={p.y}\n r={marker.radius ?? 6}\n style={marker.color ? { fill: marker.color } : undefined}\n data-marker-id={marker.id ?? i}\n tabIndex={interactive ? 0 : undefined}\n role={interactive ? \"button\" : undefined}\n aria-label={marker.label}\n onClick={interactive ? () => onMarkerClick?.(marker, i) : undefined}\n onKeyDown={\n interactive\n ? (e) => {\n if (e.key === \"Enter\" || e.key === \" \") {\n e.preventDefault();\n onMarkerClick?.(marker, i);\n }\n }\n : undefined\n }\n >\n {marker.label ? <title>{marker.label}</title> : null}\n </circle>\n );\n })}\n\n {scale && (\n <g\n className={styles.scale}\n transform={`translate(${padding}, ${height - padding})`}\n >\n <line x1={0} y1={0} x2={scale.pixels} y2={0} />\n <line x1={0} y1={-4} x2={0} y2={4} />\n <line x1={scale.pixels} y1={-4} x2={scale.pixels} y2={4} />\n <text x={scale.pixels / 2} y={-8} textAnchor=\"middle\">\n {formatScale(scale.km)}\n </text>\n </g>\n )}\n </svg>\n )}\n </div>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"TrajectoryMap.cjs","names":[],"sources":["../../src/geo/TrajectoryMap.tsx"],"sourcesContent":["/**\n * @tempest-limits file-lines, props-count, function-lines — the SVG plot (points,\n * current, height, padding, showGrid, showScale, strokeColor, label), the optional\n * leaflet tile layer (tileUrl, tileAttribution) and the overlay (markers,\n * onMarkerClick). The body projects the path, fits the bounds and hands the same\n * bounds to the tile layer when there is one.\n */\nimport { lazy, Suspense, useEffect, useMemo, useRef, useState, type HTMLAttributes } from \"react\";\nimport { cn } from \"@/utils/cn\";\nimport { boundingBox, expandBounds } from \"./bounds\";\nimport { haversineKm } from \"./distance\";\nimport { fitProjection, type PixelPoint } from \"./projection\";\nimport type { Coordinate, GeoMarker } from \"./types\";\nimport styles from \"./TrajectoryMap.module.css\";\n\n// Leaflet layer is loaded on demand — only when a `tileUrl` is supplied, so\n// apps that stick to the tile-free SVG never pull leaflet into their bundle.\nconst LeafletTrajectory = lazy(() =>\n import(\"./leaflet-map\").then((m) => ({ default: m.LeafletTrajectory })),\n);\n\nexport interface TrajectoryMapProps extends Omit<HTMLAttributes<HTMLDivElement>, \"children\"> {\n /** Ordered coordinates forming the trajectory. */\n points: readonly Coordinate[];\n /** Optional \"current position\" marker, drawn on top of the path. */\n current?: Coordinate | null;\n /** Viewport height in pixels. Default: `320`. */\n height?: number;\n /** Inner padding in pixels kept clear on every edge. Default: `24`. */\n padding?: number;\n /** Draw a light reference grid behind the path (SVG mode). Default: `true`. */\n showGrid?: boolean;\n /** Draw a distance scale bar (SVG mode). Default: `true`. */\n showScale?: boolean;\n /** Path stroke color. Default: the primary token. */\n strokeColor?: string;\n /**\n * Tile URL template (`{z}/{x}/{y}`) of a map server **you host**. When set,\n * the map upgrades to a real Leaflet tile layer (requires `leaflet` to be\n * installed). Omit for the zero-dependency, zero-network SVG plot.\n */\n tileUrl?: string;\n /** Attribution text shown over the tile layer. */\n tileAttribution?: string;\n /** Accessible label for the map region. Default: `\"Trajetória\"`. */\n label?: string;\n /** Point markers to overlay (e.g. stops, POIs). Included in the auto-fit. */\n markers?: readonly GeoMarker[];\n /** Fired when a marker is clicked. */\n onMarkerClick?: (marker: GeoMarker, index: number) => void;\n}\n\nconst NICE_KM_STEPS = [0.05, 0.1, 0.25, 0.5, 1, 2, 5, 10, 25, 50, 100, 250, 500, 1000] as const;\n\n/** Pick the largest \"nice\" distance whose bar fits within `maxPixels`. */\nfunction pickScaleKm(kmPerPixel: number, maxPixels: number): { km: number; pixels: number } {\n const maxKm = kmPerPixel * maxPixels;\n let chosen: number = NICE_KM_STEPS[0];\n for (const step of NICE_KM_STEPS) {\n if (step <= maxKm) chosen = step;\n }\n return { km: chosen, pixels: chosen / kmPerPixel };\n}\n\n/** Format a distance in km for the scale bar (meters below 1 km). */\nfunction formatScale(km: number): string {\n return km < 1 ? `${Math.round(km * 1000)} m` : `${km % 1 === 0 ? km : km.toFixed(1)} km`;\n}\n\n/**\n * Plot a GPS trajectory. By default it renders a **tile-free SVG** map (Web\n * Mercator projection, auto-fit to the points, optional grid + scale bar) —\n * 100% self-contained, no external tiles or paid API. Pass `tileUrl` pointing\n * at a map server **you host** to upgrade to a real Leaflet tile layer.\n *\n * @example\n * // Zero-dependency SVG plot\n * <TrajectoryMap points={points} current={lastPoint} height={360} />\n *\n * @example\n * // Real tiles from your own server (requires `leaflet` installed)\n * <TrajectoryMap points={points} tileUrl=\"https://tiles.internal/{z}/{x}/{y}.png\" />\n */\nexport function TrajectoryMap({\n points,\n current,\n height = 320,\n padding = 24,\n showGrid = true,\n showScale = true,\n strokeColor,\n tileUrl,\n tileAttribution,\n label = \"Trajetória\",\n markers,\n onMarkerClick,\n className,\n style,\n ...rest\n}: TrajectoryMapProps) {\n const containerRef = useRef<HTMLDivElement>(null);\n const [width, setWidth] = useState<number>(600);\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 allPoints = useMemo<Coordinate[]>(\n () => [...points, ...(current ? [current] : []), ...(markers ?? [])],\n [points, current, markers],\n );\n\n const svg = useMemo(() => {\n const box = boundingBox(allPoints);\n if (!box) return null;\n\n const projection = fitProjection(expandBounds(box, 0.12), width, height, { padding });\n const pixels: PixelPoint[] = points.map((p) => projection.project(p));\n const polyline = pixels.map((p) => `${p.x.toFixed(1)},${p.y.toFixed(1)}`).join(\" \");\n\n const startPixel = pixels[0] ?? null;\n const endPixel = pixels.length > 0 ? pixels[pixels.length - 1] : null;\n const currentPixel = current ? projection.project(current) : null;\n\n // Distance-per-pixel at the map center, for the scale bar: measure the\n // center → east-edge span in both km (haversine) and pixels (projected).\n const centerCoord = {\n latitude: (box.minLatitude + box.maxLatitude) / 2,\n longitude: (box.minLongitude + box.maxLongitude) / 2,\n };\n const center = projection.project(centerCoord);\n const eastRef = projection.project({ ...centerCoord, longitude: box.maxLongitude });\n const edgeKm = haversineKm(centerCoord, { ...centerCoord, longitude: box.maxLongitude });\n const edgePixels = Math.abs(eastRef.x - center.x) || 1;\n const kmPerPixel = edgeKm / edgePixels || 0;\n\n const markerPixels = (markers ?? []).map((m) => ({ marker: m, p: projection.project(m) }));\n\n return { polyline, startPixel, endPixel, currentPixel, kmPerPixel, markerPixels };\n }, [allPoints, points, current, markers, width, height, padding]);\n\n // ── Leaflet tile mode ────────────────────────────────────────────────────\n if (tileUrl) {\n return (\n <div\n ref={containerRef}\n className={cn(styles.map, className)}\n style={{ height, ...style }}\n role=\"img\"\n aria-label={label}\n {...rest}\n >\n <Suspense fallback={<div className={styles.loading}>Carregando mapa…</div>}>\n <LeafletTrajectory\n points={points}\n current={current}\n tileUrl={tileUrl}\n tileAttribution={tileAttribution}\n strokeColor={strokeColor}\n />\n </Suspense>\n </div>\n );\n }\n\n // ── Tile-free SVG mode ─────────────────────────────────────────────────────\n const scale =\n showScale && svg && svg.kmPerPixel > 0\n ? pickScaleKm(svg.kmPerPixel, Math.min(120, width * 0.35))\n : null;\n\n return (\n <div\n ref={containerRef}\n className={cn(styles.map, className)}\n style={{ height, ...style }}\n role=\"img\"\n aria-label={label}\n {...rest}\n >\n {!svg ? (\n <div className={styles.empty}>Sem pontos de trajetória ainda.</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 >\n {showGrid && (\n <g className={styles.grid}>\n {[0.25, 0.5, 0.75].map((f) => (\n <line\n key={`h${f}`}\n x1={0}\n y1={height * f}\n x2={width}\n y2={height * f}\n />\n ))}\n {[0.25, 0.5, 0.75].map((f) => (\n <line\n key={`v${f}`}\n x1={width * f}\n y1={0}\n x2={width * f}\n y2={height}\n />\n ))}\n </g>\n )}\n\n {svg.polyline && (\n <polyline\n className={styles.path}\n points={svg.polyline}\n fill=\"none\"\n stroke={strokeColor}\n data-testid=\"trajectory-path\"\n />\n )}\n\n {svg.startPixel && (\n <circle\n className={styles.start}\n cx={svg.startPixel.x}\n cy={svg.startPixel.y}\n r={5}\n />\n )}\n {svg.endPixel && !current && (\n <circle\n className={styles.end}\n cx={svg.endPixel.x}\n cy={svg.endPixel.y}\n r={5}\n />\n )}\n {svg.currentPixel && (\n <>\n <circle\n className={styles.pulse}\n cx={svg.currentPixel.x}\n cy={svg.currentPixel.y}\n r={10}\n />\n <circle\n className={styles.current}\n cx={svg.currentPixel.x}\n cy={svg.currentPixel.y}\n r={5}\n />\n </>\n )}\n\n {svg.markerPixels.map(({ marker, p }, i) => {\n const interactive = Boolean(onMarkerClick);\n return (\n <circle\n key={marker.id ?? i}\n className={styles.marker}\n cx={p.x}\n cy={p.y}\n r={marker.radius ?? 6}\n style={marker.color ? { fill: marker.color } : undefined}\n data-marker-id={marker.id ?? i}\n tabIndex={interactive ? 0 : undefined}\n role={interactive ? \"button\" : undefined}\n aria-label={marker.label}\n onClick={interactive ? () => onMarkerClick?.(marker, i) : undefined}\n onKeyDown={\n interactive\n ? (e) => {\n if (e.key === \"Enter\" || e.key === \" \") {\n e.preventDefault();\n onMarkerClick?.(marker, i);\n }\n }\n : undefined\n }\n >\n {marker.label ? <title>{marker.label}</title> : null}\n </circle>\n );\n })}\n\n {scale && (\n <g\n className={styles.scale}\n transform={`translate(${padding}, ${height - padding})`}\n >\n <line x1={0} y1={0} x2={scale.pixels} y2={0} />\n <line x1={0} y1={-4} x2={0} y2={4} />\n <line x1={scale.pixels} y1={-4} x2={scale.pixels} y2={4} />\n <text x={scale.pixels / 2} y={-8} textAnchor=\"middle\">\n {formatScale(scale.km)}\n </text>\n </g>\n )}\n </svg>\n )}\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"TrajectoryMap.js","names":[],"sources":["../../src/geo/TrajectoryMap.tsx"],"sourcesContent":["import { lazy, Suspense, useEffect, useMemo, useRef, useState, type HTMLAttributes } from \"react\";\nimport { cn } from \"@/utils/cn\";\nimport { boundingBox, expandBounds } from \"./bounds\";\nimport { haversineKm } from \"./distance\";\nimport { fitProjection, type PixelPoint } from \"./projection\";\nimport type { Coordinate, GeoMarker } from \"./types\";\nimport styles from \"./TrajectoryMap.module.css\";\n\n// Leaflet layer is loaded on demand — only when a `tileUrl` is supplied, so\n// apps that stick to the tile-free SVG never pull leaflet into their bundle.\nconst LeafletTrajectory = lazy(() =>\n import(\"./leaflet-map\").then((m) => ({ default: m.LeafletTrajectory })),\n);\n\nexport interface TrajectoryMapProps extends Omit<HTMLAttributes<HTMLDivElement>, \"children\"> {\n /** Ordered coordinates forming the trajectory. */\n points: readonly Coordinate[];\n /** Optional \"current position\" marker, drawn on top of the path. */\n current?: Coordinate | null;\n /** Viewport height in pixels. Default: `320`. */\n height?: number;\n /** Inner padding in pixels kept clear on every edge. Default: `24`. */\n padding?: number;\n /** Draw a light reference grid behind the path (SVG mode). Default: `true`. */\n showGrid?: boolean;\n /** Draw a distance scale bar (SVG mode). Default: `true`. */\n showScale?: boolean;\n /** Path stroke color. Default: the primary token. */\n strokeColor?: string;\n /**\n * Tile URL template (`{z}/{x}/{y}`) of a map server **you host**. When set,\n * the map upgrades to a real Leaflet tile layer (requires `leaflet` to be\n * installed). Omit for the zero-dependency, zero-network SVG plot.\n */\n tileUrl?: string;\n /** Attribution text shown over the tile layer. */\n tileAttribution?: string;\n /** Accessible label for the map region. Default: `\"Trajetória\"`. */\n label?: string;\n /** Point markers to overlay (e.g. stops, POIs). Included in the auto-fit. */\n markers?: readonly GeoMarker[];\n /** Fired when a marker is clicked. */\n onMarkerClick?: (marker: GeoMarker, index: number) => void;\n}\n\nconst NICE_KM_STEPS = [0.05, 0.1, 0.25, 0.5, 1, 2, 5, 10, 25, 50, 100, 250, 500, 1000] as const;\n\n/** Pick the largest \"nice\" distance whose bar fits within `maxPixels`. */\nfunction pickScaleKm(kmPerPixel: number, maxPixels: number): { km: number; pixels: number } {\n const maxKm = kmPerPixel * maxPixels;\n let chosen: number = NICE_KM_STEPS[0];\n for (const step of NICE_KM_STEPS) {\n if (step <= maxKm) chosen = step;\n }\n return { km: chosen, pixels: chosen / kmPerPixel };\n}\n\n/** Format a distance in km for the scale bar (meters below 1 km). */\nfunction formatScale(km: number): string {\n return km < 1 ? `${Math.round(km * 1000)} m` : `${km % 1 === 0 ? km : km.toFixed(1)} km`;\n}\n\n/**\n * Plot a GPS trajectory. By default it renders a **tile-free SVG** map (Web\n * Mercator projection, auto-fit to the points, optional grid + scale bar) —\n * 100% self-contained, no external tiles or paid API. Pass `tileUrl` pointing\n * at a map server **you host** to upgrade to a real Leaflet tile layer.\n *\n * @example\n * // Zero-dependency SVG plot\n * <TrajectoryMap points={points} current={lastPoint} height={360} />\n *\n * @example\n * // Real tiles from your own server (requires `leaflet` installed)\n * <TrajectoryMap points={points} tileUrl=\"https://tiles.internal/{z}/{x}/{y}.png\" />\n */\nexport function TrajectoryMap({\n points,\n current,\n height = 320,\n padding = 24,\n showGrid = true,\n showScale = true,\n strokeColor,\n tileUrl,\n tileAttribution,\n label = \"Trajetória\",\n markers,\n onMarkerClick,\n className,\n style,\n ...rest\n}: TrajectoryMapProps) {\n const containerRef = useRef<HTMLDivElement>(null);\n const [width, setWidth] = useState<number>(600);\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 allPoints = useMemo<Coordinate[]>(\n () => [...points, ...(current ? [current] : []), ...(markers ?? [])],\n [points, current, markers],\n );\n\n const svg = useMemo(() => {\n const box = boundingBox(allPoints);\n if (!box) return null;\n\n const projection = fitProjection(expandBounds(box, 0.12), width, height, { padding });\n const pixels: PixelPoint[] = points.map((p) => projection.project(p));\n const polyline = pixels.map((p) => `${p.x.toFixed(1)},${p.y.toFixed(1)}`).join(\" \");\n\n const startPixel = pixels[0] ?? null;\n const endPixel = pixels.length > 0 ? pixels[pixels.length - 1] : null;\n const currentPixel = current ? projection.project(current) : null;\n\n // Distance-per-pixel at the map center, for the scale bar: measure the\n // center → east-edge span in both km (haversine) and pixels (projected).\n const centerCoord = {\n latitude: (box.minLatitude + box.maxLatitude) / 2,\n longitude: (box.minLongitude + box.maxLongitude) / 2,\n };\n const center = projection.project(centerCoord);\n const eastRef = projection.project({ ...centerCoord, longitude: box.maxLongitude });\n const edgeKm = haversineKm(centerCoord, { ...centerCoord, longitude: box.maxLongitude });\n const edgePixels = Math.abs(eastRef.x - center.x) || 1;\n const kmPerPixel = edgeKm / edgePixels || 0;\n\n const markerPixels = (markers ?? []).map((m) => ({ marker: m, p: projection.project(m) }));\n\n return { polyline, startPixel, endPixel, currentPixel, kmPerPixel, markerPixels };\n }, [allPoints, points, current, markers, width, height, padding]);\n\n // ── Leaflet tile mode ────────────────────────────────────────────────────\n if (tileUrl) {\n return (\n <div\n ref={containerRef}\n className={cn(styles.map, className)}\n style={{ height, ...style }}\n role=\"img\"\n aria-label={label}\n {...rest}\n >\n <Suspense fallback={<div className={styles.loading}>Carregando mapa…</div>}>\n <LeafletTrajectory\n points={points}\n current={current}\n tileUrl={tileUrl}\n tileAttribution={tileAttribution}\n strokeColor={strokeColor}\n />\n </Suspense>\n </div>\n );\n }\n\n // ── Tile-free SVG mode ─────────────────────────────────────────────────────\n const scale =\n showScale && svg && svg.kmPerPixel > 0\n ? pickScaleKm(svg.kmPerPixel, Math.min(120, width * 0.35))\n : null;\n\n return (\n <div\n ref={containerRef}\n className={cn(styles.map, className)}\n style={{ height, ...style }}\n role=\"img\"\n aria-label={label}\n {...rest}\n >\n {!svg ? (\n <div className={styles.empty}>Sem pontos de trajetória ainda.</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 >\n {showGrid && (\n <g className={styles.grid}>\n {[0.25, 0.5, 0.75].map((f) => (\n <line\n key={`h${f}`}\n x1={0}\n y1={height * f}\n x2={width}\n y2={height * f}\n />\n ))}\n {[0.25, 0.5, 0.75].map((f) => (\n <line\n key={`v${f}`}\n x1={width * f}\n y1={0}\n x2={width * f}\n y2={height}\n />\n ))}\n </g>\n )}\n\n {svg.polyline && (\n <polyline\n className={styles.path}\n points={svg.polyline}\n fill=\"none\"\n stroke={strokeColor}\n data-testid=\"trajectory-path\"\n />\n )}\n\n {svg.startPixel && (\n <circle\n className={styles.start}\n cx={svg.startPixel.x}\n cy={svg.startPixel.y}\n r={5}\n />\n )}\n {svg.endPixel && !current && (\n <circle\n className={styles.end}\n cx={svg.endPixel.x}\n cy={svg.endPixel.y}\n r={5}\n />\n )}\n {svg.currentPixel && (\n <>\n <circle\n className={styles.pulse}\n cx={svg.currentPixel.x}\n cy={svg.currentPixel.y}\n r={10}\n />\n <circle\n className={styles.current}\n cx={svg.currentPixel.x}\n cy={svg.currentPixel.y}\n r={5}\n />\n </>\n )}\n\n {svg.markerPixels.map(({ marker, p }, i) => {\n const interactive = Boolean(onMarkerClick);\n return (\n <circle\n key={marker.id ?? i}\n className={styles.marker}\n cx={p.x}\n cy={p.y}\n r={marker.radius ?? 6}\n style={marker.color ? { fill: marker.color } : undefined}\n data-marker-id={marker.id ?? i}\n tabIndex={interactive ? 0 : undefined}\n role={interactive ? \"button\" : undefined}\n aria-label={marker.label}\n onClick={interactive ? () => onMarkerClick?.(marker, i) : undefined}\n onKeyDown={\n interactive\n ? (e) => {\n if (e.key === \"Enter\" || e.key === \" \") {\n e.preventDefault();\n onMarkerClick?.(marker, i);\n }\n }\n : undefined\n }\n >\n {marker.label ? <title>{marker.label}</title> : null}\n </circle>\n );\n })}\n\n {scale && (\n <g\n className={styles.scale}\n transform={`translate(${padding}, ${height - padding})`}\n >\n <line x1={0} y1={0} x2={scale.pixels} y2={0} />\n <line x1={0} y1={-4} x2={0} y2={4} />\n <line x1={scale.pixels} y1={-4} x2={scale.pixels} y2={4} />\n <text x={scale.pixels / 2} y={-8} textAnchor=\"middle\">\n {formatScale(scale.km)}\n </text>\n </g>\n )}\n </svg>\n )}\n </div>\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"TrajectoryMap.js","names":[],"sources":["../../src/geo/TrajectoryMap.tsx"],"sourcesContent":["/**\n * @tempest-limits file-lines, props-count, function-lines — the SVG plot (points,\n * current, height, padding, showGrid, showScale, strokeColor, label), the optional\n * leaflet tile layer (tileUrl, tileAttribution) and the overlay (markers,\n * onMarkerClick). The body projects the path, fits the bounds and hands the same\n * bounds to the tile layer when there is one.\n */\nimport { lazy, Suspense, useEffect, useMemo, useRef, useState, type HTMLAttributes } from \"react\";\nimport { cn } from \"@/utils/cn\";\nimport { boundingBox, expandBounds } from \"./bounds\";\nimport { haversineKm } from \"./distance\";\nimport { fitProjection, type PixelPoint } from \"./projection\";\nimport type { Coordinate, GeoMarker } from \"./types\";\nimport styles from \"./TrajectoryMap.module.css\";\n\n// Leaflet layer is loaded on demand — only when a `tileUrl` is supplied, so\n// apps that stick to the tile-free SVG never pull leaflet into their bundle.\nconst LeafletTrajectory = lazy(() =>\n import(\"./leaflet-map\").then((m) => ({ default: m.LeafletTrajectory })),\n);\n\nexport interface TrajectoryMapProps extends Omit<HTMLAttributes<HTMLDivElement>, \"children\"> {\n /** Ordered coordinates forming the trajectory. */\n points: readonly Coordinate[];\n /** Optional \"current position\" marker, drawn on top of the path. */\n current?: Coordinate | null;\n /** Viewport height in pixels. Default: `320`. */\n height?: number;\n /** Inner padding in pixels kept clear on every edge. Default: `24`. */\n padding?: number;\n /** Draw a light reference grid behind the path (SVG mode). Default: `true`. */\n showGrid?: boolean;\n /** Draw a distance scale bar (SVG mode). Default: `true`. */\n showScale?: boolean;\n /** Path stroke color. Default: the primary token. */\n strokeColor?: string;\n /**\n * Tile URL template (`{z}/{x}/{y}`) of a map server **you host**. When set,\n * the map upgrades to a real Leaflet tile layer (requires `leaflet` to be\n * installed). Omit for the zero-dependency, zero-network SVG plot.\n */\n tileUrl?: string;\n /** Attribution text shown over the tile layer. */\n tileAttribution?: string;\n /** Accessible label for the map region. Default: `\"Trajetória\"`. */\n label?: string;\n /** Point markers to overlay (e.g. stops, POIs). Included in the auto-fit. */\n markers?: readonly GeoMarker[];\n /** Fired when a marker is clicked. */\n onMarkerClick?: (marker: GeoMarker, index: number) => void;\n}\n\nconst NICE_KM_STEPS = [0.05, 0.1, 0.25, 0.5, 1, 2, 5, 10, 25, 50, 100, 250, 500, 1000] as const;\n\n/** Pick the largest \"nice\" distance whose bar fits within `maxPixels`. */\nfunction pickScaleKm(kmPerPixel: number, maxPixels: number): { km: number; pixels: number } {\n const maxKm = kmPerPixel * maxPixels;\n let chosen: number = NICE_KM_STEPS[0];\n for (const step of NICE_KM_STEPS) {\n if (step <= maxKm) chosen = step;\n }\n return { km: chosen, pixels: chosen / kmPerPixel };\n}\n\n/** Format a distance in km for the scale bar (meters below 1 km). */\nfunction formatScale(km: number): string {\n return km < 1 ? `${Math.round(km * 1000)} m` : `${km % 1 === 0 ? km : km.toFixed(1)} km`;\n}\n\n/**\n * Plot a GPS trajectory. By default it renders a **tile-free SVG** map (Web\n * Mercator projection, auto-fit to the points, optional grid + scale bar) —\n * 100% self-contained, no external tiles or paid API. Pass `tileUrl` pointing\n * at a map server **you host** to upgrade to a real Leaflet tile layer.\n *\n * @example\n * // Zero-dependency SVG plot\n * <TrajectoryMap points={points} current={lastPoint} height={360} />\n *\n * @example\n * // Real tiles from your own server (requires `leaflet` installed)\n * <TrajectoryMap points={points} tileUrl=\"https://tiles.internal/{z}/{x}/{y}.png\" />\n */\nexport function TrajectoryMap({\n points,\n current,\n height = 320,\n padding = 24,\n showGrid = true,\n showScale = true,\n strokeColor,\n tileUrl,\n tileAttribution,\n label = \"Trajetória\",\n markers,\n onMarkerClick,\n className,\n style,\n ...rest\n}: TrajectoryMapProps) {\n const containerRef = useRef<HTMLDivElement>(null);\n const [width, setWidth] = useState<number>(600);\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 allPoints = useMemo<Coordinate[]>(\n () => [...points, ...(current ? [current] : []), ...(markers ?? [])],\n [points, current, markers],\n );\n\n const svg = useMemo(() => {\n const box = boundingBox(allPoints);\n if (!box) return null;\n\n const projection = fitProjection(expandBounds(box, 0.12), width, height, { padding });\n const pixels: PixelPoint[] = points.map((p) => projection.project(p));\n const polyline = pixels.map((p) => `${p.x.toFixed(1)},${p.y.toFixed(1)}`).join(\" \");\n\n const startPixel = pixels[0] ?? null;\n const endPixel = pixels.length > 0 ? pixels[pixels.length - 1] : null;\n const currentPixel = current ? projection.project(current) : null;\n\n // Distance-per-pixel at the map center, for the scale bar: measure the\n // center → east-edge span in both km (haversine) and pixels (projected).\n const centerCoord = {\n latitude: (box.minLatitude + box.maxLatitude) / 2,\n longitude: (box.minLongitude + box.maxLongitude) / 2,\n };\n const center = projection.project(centerCoord);\n const eastRef = projection.project({ ...centerCoord, longitude: box.maxLongitude });\n const edgeKm = haversineKm(centerCoord, { ...centerCoord, longitude: box.maxLongitude });\n const edgePixels = Math.abs(eastRef.x - center.x) || 1;\n const kmPerPixel = edgeKm / edgePixels || 0;\n\n const markerPixels = (markers ?? []).map((m) => ({ marker: m, p: projection.project(m) }));\n\n return { polyline, startPixel, endPixel, currentPixel, kmPerPixel, markerPixels };\n }, [allPoints, points, current, markers, width, height, padding]);\n\n // ── Leaflet tile mode ────────────────────────────────────────────────────\n if (tileUrl) {\n return (\n <div\n ref={containerRef}\n className={cn(styles.map, className)}\n style={{ height, ...style }}\n role=\"img\"\n aria-label={label}\n {...rest}\n >\n <Suspense fallback={<div className={styles.loading}>Carregando mapa…</div>}>\n <LeafletTrajectory\n points={points}\n current={current}\n tileUrl={tileUrl}\n tileAttribution={tileAttribution}\n strokeColor={strokeColor}\n />\n </Suspense>\n </div>\n );\n }\n\n // ── Tile-free SVG mode ─────────────────────────────────────────────────────\n const scale =\n showScale && svg && svg.kmPerPixel > 0\n ? pickScaleKm(svg.kmPerPixel, Math.min(120, width * 0.35))\n : null;\n\n return (\n <div\n ref={containerRef}\n className={cn(styles.map, className)}\n style={{ height, ...style }}\n role=\"img\"\n aria-label={label}\n {...rest}\n >\n {!svg ? (\n <div className={styles.empty}>Sem pontos de trajetória ainda.</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 >\n {showGrid && (\n <g className={styles.grid}>\n {[0.25, 0.5, 0.75].map((f) => (\n <line\n key={`h${f}`}\n x1={0}\n y1={height * f}\n x2={width}\n y2={height * f}\n />\n ))}\n {[0.25, 0.5, 0.75].map((f) => (\n <line\n key={`v${f}`}\n x1={width * f}\n y1={0}\n x2={width * f}\n y2={height}\n />\n ))}\n </g>\n )}\n\n {svg.polyline && (\n <polyline\n className={styles.path}\n points={svg.polyline}\n fill=\"none\"\n stroke={strokeColor}\n data-testid=\"trajectory-path\"\n />\n )}\n\n {svg.startPixel && (\n <circle\n className={styles.start}\n cx={svg.startPixel.x}\n cy={svg.startPixel.y}\n r={5}\n />\n )}\n {svg.endPixel && !current && (\n <circle\n className={styles.end}\n cx={svg.endPixel.x}\n cy={svg.endPixel.y}\n r={5}\n />\n )}\n {svg.currentPixel && (\n <>\n <circle\n className={styles.pulse}\n cx={svg.currentPixel.x}\n cy={svg.currentPixel.y}\n r={10}\n />\n <circle\n className={styles.current}\n cx={svg.currentPixel.x}\n cy={svg.currentPixel.y}\n r={5}\n />\n </>\n )}\n\n {svg.markerPixels.map(({ marker, p }, i) => {\n const interactive = Boolean(onMarkerClick);\n return (\n <circle\n key={marker.id ?? i}\n className={styles.marker}\n cx={p.x}\n cy={p.y}\n r={marker.radius ?? 6}\n style={marker.color ? { fill: marker.color } : undefined}\n data-marker-id={marker.id ?? i}\n tabIndex={interactive ? 0 : undefined}\n role={interactive ? \"button\" : undefined}\n aria-label={marker.label}\n onClick={interactive ? () => onMarkerClick?.(marker, i) : undefined}\n onKeyDown={\n interactive\n ? (e) => {\n if (e.key === \"Enter\" || e.key === \" \") {\n e.preventDefault();\n onMarkerClick?.(marker, i);\n }\n }\n : undefined\n }\n >\n {marker.label ? <title>{marker.label}</title> : null}\n </circle>\n );\n })}\n\n {scale && (\n <g\n className={styles.scale}\n transform={`translate(${padding}, ${height - padding})`}\n >\n <line x1={0} y1={0} x2={scale.pixels} y2={0} />\n <line x1={0} y1={-4} x2={0} y2={4} />\n <line x1={scale.pixels} y1={-4} x2={scale.pixels} y2={4} />\n <text x={scale.pixels / 2} y={-8} textAnchor=\"middle\">\n {formatScale(scale.km)}\n </text>\n </g>\n )}\n </svg>\n )}\n </div>\n );\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"estimate.cjs","names":[],"sources":["../../src/geo/estimate.ts"],"sourcesContent":["import { haversineKm } from \"./distance\";\nimport type { Coordinate, TravelEstimate, TravelMode } from \"./types\";\n\n/**\n * Multiplier applied to the great-circle distance to approximate real road\n * distance. Mirrors `DEFAULT_CIRCUITY_FACTOR` from `tempest-fastapi-sdk`.\n */\nexport const DEFAULT_CIRCUITY_FACTOR = 1.3;\n\n/**\n * Default average car speed in km/h used to derive duration from distance.\n * Mirrors `DEFAULT_CAR_SPEED_KMH` from `tempest-fastapi-sdk`.\n */\nexport const DEFAULT_CAR_SPEED_KMH = 50.0;\n\n/**\n * Per-mode multipliers applied to the car duration. Mirrors\n * `DEFAULT_MODE_DURATION_FACTORS` from `tempest-fastapi-sdk` — motorcycles are\n * a touch faster, buses considerably slower (stops + dedicated lanes).\n */\nexport const DEFAULT_MODE_DURATION_FACTORS: Record<TravelMode, number> = {\n car: 1.0,\n motorcycle: 0.95,\n bus: 1.6,\n};\n\n/**\n * Look up the duration multiplier for a travel mode, falling back to `1.0`\n * for unknown modes. Mirrors `duration_factor` from `tempest-fastapi-sdk`.\n *\n * @param mode - Travel mode.\n * @param factors - Optional override map (partial merges over the defaults).\n * @returns The multiplier for `mode`.\n */\nexport function durationFactor(\n mode: TravelMode,\n factors?: Partial<Record<TravelMode, number>>,\n): number {\n return factors?.[mode] ?? DEFAULT_MODE_DURATION_FACTORS[mode] ?? 1.0;\n}\n\n/** Options for {@link estimateTravel}. */\nexport interface EstimateTravelOptions {\n /** Distance multiplier over the great-circle line. Default: `1.3`. */\n circuityFactor?: number;\n /** Average car speed in km/h. Default: `50`. */\n carSpeedKmh?: number;\n /** Per-mode duration multipliers (merged over the defaults). */\n modeDurationFactors?: Partial<Record<TravelMode, number>>;\n}\n\n/**\n * Offline travel estimate between two coordinates. No network — distance comes\n * from {@link haversineKm} scaled by circuity, duration from a mode-adjusted\n * average speed. Mirrors `estimate_travel` from `tempest-fastapi-sdk`.\n *\n * @param origin - Start coordinate.\n * @param destination - End coordinate.\n * @param mode - Travel mode. Default: `\"car\"`.\n * @param options - Tuning knobs (circuity, speed, per-mode factors).\n * @returns A `\"heuristic\"` {@link TravelEstimate}.\n * @throws {RangeError} If `carSpeedKmh <= 0` or `circuityFactor <= 0`.\n *\n * @example\n * estimateTravel(\n * { latitude: -23.5505, longitude: -46.6333 },\n * { latitude: -23.5629, longitude: -46.6544 },\n * \"car\",\n * ); // { mode: \"car\", distance_km: …, duration_minutes: …, source: \"heuristic\" }\n */\nexport function estimateTravel(\n origin: Coordinate,\n destination: Coordinate,\n mode: TravelMode = \"car\",\n options: EstimateTravelOptions = {},\n): TravelEstimate {\n const {\n circuityFactor = DEFAULT_CIRCUITY_FACTOR,\n carSpeedKmh = DEFAULT_CAR_SPEED_KMH,\n modeDurationFactors,\n } = options;\n\n if (carSpeedKmh <= 0) {\n throw new RangeError(\"carSpeedKmh must be greater than 0\");\n }\n if (circuityFactor <= 0) {\n throw new RangeError(\"circuityFactor must be greater than 0\");\n }\n\n const distanceKm = haversineKm(origin, destination) * circuityFactor;\n const carMinutes = (distanceKm / carSpeedKmh) * 60.0;\n const durationMinutes = carMinutes * durationFactor(mode, modeDurationFactors);\n\n return {\n mode,\n distance_km: distanceKm,\n duration_minutes: durationMinutes,\n source: \"heuristic\",\n };\n}\n"],"mappings":"kCAOA,IAAa,EAA0B,IAM1B,EAAwB,GAOxB,EAA4D,CACrE,IAAK,EACL,WAAY,IACZ,IAAK,GACT,EAUA,SAAgB,EACZ,EACA,EACM,CACN,OAAO,IAAU,IAAS,EAA8B,IAAS,CACrE,
|
|
1
|
+
{"version":3,"file":"estimate.cjs","names":[],"sources":["../../src/geo/estimate.ts"],"sourcesContent":["import { haversineKm } from \"./distance\";\nimport type { Coordinate, TravelEstimate, TravelMode } from \"./types\";\n\n/**\n * Multiplier applied to the great-circle distance to approximate real road\n * distance. Mirrors `DEFAULT_CIRCUITY_FACTOR` from `tempest-fastapi-sdk`.\n */\nexport const DEFAULT_CIRCUITY_FACTOR = 1.3;\n\n/**\n * Default average car speed in km/h used to derive duration from distance.\n * Mirrors `DEFAULT_CAR_SPEED_KMH` from `tempest-fastapi-sdk`.\n */\nexport const DEFAULT_CAR_SPEED_KMH = 50.0;\n\n/**\n * Per-mode multipliers applied to the car duration. Mirrors\n * `DEFAULT_MODE_DURATION_FACTORS` from `tempest-fastapi-sdk` — motorcycles are\n * a touch faster, buses considerably slower (stops + dedicated lanes).\n */\nexport const DEFAULT_MODE_DURATION_FACTORS: Record<TravelMode, number> = {\n car: 1.0,\n motorcycle: 0.95,\n bus: 1.6,\n};\n\n/**\n * Look up the duration multiplier for a travel mode, falling back to `1.0`\n * for unknown modes. Mirrors `duration_factor` from `tempest-fastapi-sdk`.\n *\n * @param mode - Travel mode.\n * @param factors - Optional override map (partial merges over the defaults).\n * @returns The multiplier for `mode`.\n */\nexport function durationFactor(\n mode: TravelMode,\n factors?: Partial<Record<TravelMode, number>>,\n): number {\n return factors?.[mode] ?? DEFAULT_MODE_DURATION_FACTORS[mode] ?? 1.0;\n}\n\n/** Options for {@link estimateTravel}. */\nexport interface EstimateTravelOptions {\n /** Distance multiplier over the great-circle line. Default: `1.3`. */\n circuityFactor?: number;\n /** Average car speed in km/h. Default: `50`. */\n carSpeedKmh?: number;\n /** Per-mode duration multipliers (merged over the defaults). */\n modeDurationFactors?: Partial<Record<TravelMode, number>>;\n}\n\n/**\n * Offline travel estimate between two coordinates. No network — distance comes\n * from {@link haversineKm} scaled by circuity, duration from a mode-adjusted\n * average speed. Mirrors `estimate_travel` from `tempest-fastapi-sdk`.\n *\n * @param origin - Start coordinate.\n * @param destination - End coordinate.\n * @param mode - Travel mode. Default: `\"car\"`.\n * @param options - Tuning knobs (circuity, speed, per-mode factors).\n * @returns A `\"heuristic\"` {@link TravelEstimate}.\n * @throws {RangeError} If `carSpeedKmh <= 0` or `circuityFactor <= 0`.\n *\n * @example\n * estimateTravel(\n * { latitude: -23.5505, longitude: -46.6333 },\n * { latitude: -23.5629, longitude: -46.6544 },\n * \"car\",\n * ); // { mode: \"car\", distance_km: …, duration_minutes: …, source: \"heuristic\" }\n *\n * @tempest-limits param-count — `(origin, destination, mode)` is the shape every\n * routing API uses, and the fourth argument is already the named-options object the\n * rule asks for. Folding origin and destination into it would hide that the pair is\n * ordered, and this function is re-exported from the package root, so the change\n * would break every caller to satisfy a count.\n */\nexport function estimateTravel(\n origin: Coordinate,\n destination: Coordinate,\n mode: TravelMode = \"car\",\n options: EstimateTravelOptions = {},\n): TravelEstimate {\n const {\n circuityFactor = DEFAULT_CIRCUITY_FACTOR,\n carSpeedKmh = DEFAULT_CAR_SPEED_KMH,\n modeDurationFactors,\n } = options;\n\n if (carSpeedKmh <= 0) {\n throw new RangeError(\"carSpeedKmh must be greater than 0\");\n }\n if (circuityFactor <= 0) {\n throw new RangeError(\"circuityFactor must be greater than 0\");\n }\n\n const distanceKm = haversineKm(origin, destination) * circuityFactor;\n const carMinutes = (distanceKm / carSpeedKmh) * 60.0;\n const durationMinutes = carMinutes * durationFactor(mode, modeDurationFactors);\n\n return {\n mode,\n distance_km: distanceKm,\n duration_minutes: durationMinutes,\n source: \"heuristic\",\n };\n}\n"],"mappings":"kCAOA,IAAa,EAA0B,IAM1B,EAAwB,GAOxB,EAA4D,CACrE,IAAK,EACL,WAAY,IACZ,IAAK,GACT,EAUA,SAAgB,EACZ,EACA,EACM,CACN,OAAO,IAAU,IAAS,EAA8B,IAAS,CACrE,CAqCA,SAAgB,EACZ,EACA,EACA,EAAmB,MACnB,EAAiC,CAAC,EACpB,CACd,GAAM,CACF,iBAAiB,EACjB,cAAA,GACA,uBACA,EAEJ,GAAI,GAAe,EACf,MAAU,WAAW,oCAAoC,EAE7D,GAAI,GAAkB,EAClB,MAAU,WAAW,uCAAuC,EAGhE,IAAM,EAAa,EAAA,YAAY,EAAQ,CAAW,EAAI,EAItD,MAAO,CACH,OACA,YAAa,EACb,iBANgB,EAAa,EAAe,GACX,EAAe,EAAM,CAAmB,EAMzE,OAAQ,WACZ,CACJ"}
|
package/dist/geo/estimate.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"estimate.js","names":[],"sources":["../../src/geo/estimate.ts"],"sourcesContent":["import { haversineKm } from \"./distance\";\nimport type { Coordinate, TravelEstimate, TravelMode } from \"./types\";\n\n/**\n * Multiplier applied to the great-circle distance to approximate real road\n * distance. Mirrors `DEFAULT_CIRCUITY_FACTOR` from `tempest-fastapi-sdk`.\n */\nexport const DEFAULT_CIRCUITY_FACTOR = 1.3;\n\n/**\n * Default average car speed in km/h used to derive duration from distance.\n * Mirrors `DEFAULT_CAR_SPEED_KMH` from `tempest-fastapi-sdk`.\n */\nexport const DEFAULT_CAR_SPEED_KMH = 50.0;\n\n/**\n * Per-mode multipliers applied to the car duration. Mirrors\n * `DEFAULT_MODE_DURATION_FACTORS` from `tempest-fastapi-sdk` — motorcycles are\n * a touch faster, buses considerably slower (stops + dedicated lanes).\n */\nexport const DEFAULT_MODE_DURATION_FACTORS: Record<TravelMode, number> = {\n car: 1.0,\n motorcycle: 0.95,\n bus: 1.6,\n};\n\n/**\n * Look up the duration multiplier for a travel mode, falling back to `1.0`\n * for unknown modes. Mirrors `duration_factor` from `tempest-fastapi-sdk`.\n *\n * @param mode - Travel mode.\n * @param factors - Optional override map (partial merges over the defaults).\n * @returns The multiplier for `mode`.\n */\nexport function durationFactor(\n mode: TravelMode,\n factors?: Partial<Record<TravelMode, number>>,\n): number {\n return factors?.[mode] ?? DEFAULT_MODE_DURATION_FACTORS[mode] ?? 1.0;\n}\n\n/** Options for {@link estimateTravel}. */\nexport interface EstimateTravelOptions {\n /** Distance multiplier over the great-circle line. Default: `1.3`. */\n circuityFactor?: number;\n /** Average car speed in km/h. Default: `50`. */\n carSpeedKmh?: number;\n /** Per-mode duration multipliers (merged over the defaults). */\n modeDurationFactors?: Partial<Record<TravelMode, number>>;\n}\n\n/**\n * Offline travel estimate between two coordinates. No network — distance comes\n * from {@link haversineKm} scaled by circuity, duration from a mode-adjusted\n * average speed. Mirrors `estimate_travel` from `tempest-fastapi-sdk`.\n *\n * @param origin - Start coordinate.\n * @param destination - End coordinate.\n * @param mode - Travel mode. Default: `\"car\"`.\n * @param options - Tuning knobs (circuity, speed, per-mode factors).\n * @returns A `\"heuristic\"` {@link TravelEstimate}.\n * @throws {RangeError} If `carSpeedKmh <= 0` or `circuityFactor <= 0`.\n *\n * @example\n * estimateTravel(\n * { latitude: -23.5505, longitude: -46.6333 },\n * { latitude: -23.5629, longitude: -46.6544 },\n * \"car\",\n * ); // { mode: \"car\", distance_km: …, duration_minutes: …, source: \"heuristic\" }\n */\nexport function estimateTravel(\n origin: Coordinate,\n destination: Coordinate,\n mode: TravelMode = \"car\",\n options: EstimateTravelOptions = {},\n): TravelEstimate {\n const {\n circuityFactor = DEFAULT_CIRCUITY_FACTOR,\n carSpeedKmh = DEFAULT_CAR_SPEED_KMH,\n modeDurationFactors,\n } = options;\n\n if (carSpeedKmh <= 0) {\n throw new RangeError(\"carSpeedKmh must be greater than 0\");\n }\n if (circuityFactor <= 0) {\n throw new RangeError(\"circuityFactor must be greater than 0\");\n }\n\n const distanceKm = haversineKm(origin, destination) * circuityFactor;\n const carMinutes = (distanceKm / carSpeedKmh) * 60.0;\n const durationMinutes = carMinutes * durationFactor(mode, modeDurationFactors);\n\n return {\n mode,\n distance_km: distanceKm,\n duration_minutes: durationMinutes,\n source: \"heuristic\",\n };\n}\n"],"mappings":";;AAOA,IAAa,IAA0B,KAM1B,IAAwB,IAOxB,IAA4D;CACrE,KAAK;CACL,YAAY;CACZ,KAAK;AACT;AAUA,SAAgB,EACZ,GACA,GACM;CACN,OAAO,IAAU,MAAS,EAA8B,MAAS;AACrE;
|
|
1
|
+
{"version":3,"file":"estimate.js","names":[],"sources":["../../src/geo/estimate.ts"],"sourcesContent":["import { haversineKm } from \"./distance\";\nimport type { Coordinate, TravelEstimate, TravelMode } from \"./types\";\n\n/**\n * Multiplier applied to the great-circle distance to approximate real road\n * distance. Mirrors `DEFAULT_CIRCUITY_FACTOR` from `tempest-fastapi-sdk`.\n */\nexport const DEFAULT_CIRCUITY_FACTOR = 1.3;\n\n/**\n * Default average car speed in km/h used to derive duration from distance.\n * Mirrors `DEFAULT_CAR_SPEED_KMH` from `tempest-fastapi-sdk`.\n */\nexport const DEFAULT_CAR_SPEED_KMH = 50.0;\n\n/**\n * Per-mode multipliers applied to the car duration. Mirrors\n * `DEFAULT_MODE_DURATION_FACTORS` from `tempest-fastapi-sdk` — motorcycles are\n * a touch faster, buses considerably slower (stops + dedicated lanes).\n */\nexport const DEFAULT_MODE_DURATION_FACTORS: Record<TravelMode, number> = {\n car: 1.0,\n motorcycle: 0.95,\n bus: 1.6,\n};\n\n/**\n * Look up the duration multiplier for a travel mode, falling back to `1.0`\n * for unknown modes. Mirrors `duration_factor` from `tempest-fastapi-sdk`.\n *\n * @param mode - Travel mode.\n * @param factors - Optional override map (partial merges over the defaults).\n * @returns The multiplier for `mode`.\n */\nexport function durationFactor(\n mode: TravelMode,\n factors?: Partial<Record<TravelMode, number>>,\n): number {\n return factors?.[mode] ?? DEFAULT_MODE_DURATION_FACTORS[mode] ?? 1.0;\n}\n\n/** Options for {@link estimateTravel}. */\nexport interface EstimateTravelOptions {\n /** Distance multiplier over the great-circle line. Default: `1.3`. */\n circuityFactor?: number;\n /** Average car speed in km/h. Default: `50`. */\n carSpeedKmh?: number;\n /** Per-mode duration multipliers (merged over the defaults). */\n modeDurationFactors?: Partial<Record<TravelMode, number>>;\n}\n\n/**\n * Offline travel estimate between two coordinates. No network — distance comes\n * from {@link haversineKm} scaled by circuity, duration from a mode-adjusted\n * average speed. Mirrors `estimate_travel` from `tempest-fastapi-sdk`.\n *\n * @param origin - Start coordinate.\n * @param destination - End coordinate.\n * @param mode - Travel mode. Default: `\"car\"`.\n * @param options - Tuning knobs (circuity, speed, per-mode factors).\n * @returns A `\"heuristic\"` {@link TravelEstimate}.\n * @throws {RangeError} If `carSpeedKmh <= 0` or `circuityFactor <= 0`.\n *\n * @example\n * estimateTravel(\n * { latitude: -23.5505, longitude: -46.6333 },\n * { latitude: -23.5629, longitude: -46.6544 },\n * \"car\",\n * ); // { mode: \"car\", distance_km: …, duration_minutes: …, source: \"heuristic\" }\n *\n * @tempest-limits param-count — `(origin, destination, mode)` is the shape every\n * routing API uses, and the fourth argument is already the named-options object the\n * rule asks for. Folding origin and destination into it would hide that the pair is\n * ordered, and this function is re-exported from the package root, so the change\n * would break every caller to satisfy a count.\n */\nexport function estimateTravel(\n origin: Coordinate,\n destination: Coordinate,\n mode: TravelMode = \"car\",\n options: EstimateTravelOptions = {},\n): TravelEstimate {\n const {\n circuityFactor = DEFAULT_CIRCUITY_FACTOR,\n carSpeedKmh = DEFAULT_CAR_SPEED_KMH,\n modeDurationFactors,\n } = options;\n\n if (carSpeedKmh <= 0) {\n throw new RangeError(\"carSpeedKmh must be greater than 0\");\n }\n if (circuityFactor <= 0) {\n throw new RangeError(\"circuityFactor must be greater than 0\");\n }\n\n const distanceKm = haversineKm(origin, destination) * circuityFactor;\n const carMinutes = (distanceKm / carSpeedKmh) * 60.0;\n const durationMinutes = carMinutes * durationFactor(mode, modeDurationFactors);\n\n return {\n mode,\n distance_km: distanceKm,\n duration_minutes: durationMinutes,\n source: \"heuristic\",\n };\n}\n"],"mappings":";;AAOA,IAAa,IAA0B,KAM1B,IAAwB,IAOxB,IAA4D;CACrE,KAAK;CACL,YAAY;CACZ,KAAK;AACT;AAUA,SAAgB,EACZ,GACA,GACM;CACN,OAAO,IAAU,MAAS,EAA8B,MAAS;AACrE;AAqCA,SAAgB,EACZ,GACA,GACA,IAAmB,OACnB,IAAiC,CAAC,GACpB;CACd,IAAM,EACF,oBAAiB,GACjB,iBAAA,IACA,2BACA;CAEJ,IAAI,KAAe,GACf,MAAU,WAAW,oCAAoC;CAE7D,IAAI,KAAkB,GAClB,MAAU,WAAW,uCAAuC;CAGhE,IAAM,IAAa,EAAY,GAAQ,CAAW,IAAI;CAItD,OAAO;EACH;EACA,aAAa;EACb,kBANgB,IAAa,IAAe,KACX,EAAe,GAAM,CAAmB;EAMzE,QAAQ;CACZ;AACJ"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"projection.cjs","names":[],"sources":["../../src/geo/projection.ts"],"sourcesContent":["import { clampLatitude } from \"./types\";\nimport type { Coordinate, GeoBounds } from \"./types\";\n\n/**\n * A point projected onto the unit Web Mercator plane. Both axes are in `[0, 1]`\n * — `x` grows east, `y` grows south (screen convention).\n */\nexport interface MercatorPoint {\n x: number;\n y: number;\n}\n\n/** A pixel coordinate inside the plotting viewport. */\nexport interface PixelPoint {\n x: number;\n y: number;\n}\n\n/**\n * Web Mercator (EPSG:3857) latitude clamp. Latitudes beyond this diverge to\n * infinity in the projection, so tile maps cap here.\n */\nexport const MERCATOR_MAX_LATITUDE = 85.05112878;\n\n/**\n * Project a geographic coordinate onto the unit Web Mercator plane. This is the\n * same projection tile servers use, so a self-hosted tile layer and the\n * tile-free SVG plot line up pixel-for-pixel.\n *\n * @param coord - Coordinate to project.\n * @returns `{ x, y }` in `[0, 1]`.\n */\nexport function projectMercator(coord: Coordinate): MercatorPoint {\n const lat = Math.max(\n -MERCATOR_MAX_LATITUDE,\n Math.min(MERCATOR_MAX_LATITUDE, clampLatitude(coord.latitude)),\n );\n const latRad = (lat * Math.PI) / 180;\n const x = (coord.longitude + 180) / 360;\n const y = (1 - Math.log(Math.tan(latRad) + 1 / Math.cos(latRad)) / Math.PI) / 2;\n return { x, y };\n}\n\n/**\n * Inverse of {@link projectMercator}: recover a coordinate from a unit-plane\n * point.\n *\n * @param point - `{ x, y }` in `[0, 1]`.\n * @returns The geographic coordinate.\n */\nexport function unprojectMercator(point: MercatorPoint): Coordinate {\n const longitude = point.x * 360 - 180;\n const n = Math.PI * (1 - 2 * point.y);\n const latitude = (Math.atan(Math.sinh(n)) * 180) / Math.PI;\n return { latitude, longitude };\n}\n\n/** A ready-to-use mapping from coordinates to viewport pixels. */\nexport interface FittedProjection {\n /** Project a coordinate to a pixel inside the viewport. */\n project: (coord: Coordinate) => PixelPoint;\n /** Uniform scale (unit-plane → pixels) actually used, after aspect fit. */\n scale: number;\n /** Viewport width in pixels. */\n width: number;\n /** Viewport height in pixels. */\n height: number;\n}\n\n/** Options for {@link fitProjection}. */\nexport interface FitProjectionOptions {\n /** Inner padding in pixels kept clear on every edge. Default: `16`. */\n padding?: number;\n}\n\n/**\n * Build a projection that fits `bounds` into a `width × height` viewport while\n * preserving aspect ratio (uniform scale, centered). This is what powers the\n * tile-free trajectory plot: project the bounds, scale to the SVG box, keep\n * shapes undistorted.\n *\n * @param bounds - Geographic extent to fit.\n * @param width - Viewport width in pixels.\n * @param height - Viewport height in pixels.\n * @param options - Padding tuning.\n * @returns A {@link FittedProjection} with a `project(coord)` mapper.\n */\nexport function fitProjection(\n bounds: GeoBounds,\n width: number,\n height: number,\n options: FitProjectionOptions = {},\n): FittedProjection {\n const { padding = 16 } = options;\n\n const topLeft = projectMercator({\n latitude: bounds.maxLatitude,\n longitude: bounds.minLongitude,\n });\n const bottomRight = projectMercator({\n latitude: bounds.minLatitude,\n longitude: bounds.maxLongitude,\n });\n\n const spanX = bottomRight.x - topLeft.x || Number.EPSILON;\n const spanY = bottomRight.y - topLeft.y || Number.EPSILON;\n\n const innerWidth = Math.max(1, width - padding * 2);\n const innerHeight = Math.max(1, height - padding * 2);\n\n // Uniform scale keeps the trajectory undistorted; fit the tighter axis.\n const scale = Math.min(innerWidth / spanX, innerHeight / spanY);\n\n // Center the projected content within the padded box.\n const offsetX = padding + (innerWidth - spanX * scale) / 2;\n const offsetY = padding + (innerHeight - spanY * scale) / 2;\n\n const project = (coord: Coordinate): PixelPoint => {\n const projected = projectMercator(coord);\n return {\n x: offsetX + (projected.x - topLeft.x) * scale,\n y: offsetY + (projected.y - topLeft.y) * scale,\n };\n };\n\n return { project, scale, width, height };\n}\n"],"mappings":"+BAsBA,IAAa,EAAwB,YAUrC,SAAgB,EAAgB,EAAkC,CAK9D,IAAM,EAJM,KAAK,IACb,aACA,KAAK,IAAI,EAAuB,EAAA,cAAc,EAAM,QAAQ,CAAC,CAEjD,EAAM,KAAK,GAAM,IAGjC,MAAO,CAAE,GAFE,EAAM,UAAY,KAAO,IAExB,GADD,EAAI,KAAK,IAAI,KAAK,IAAI,CAAM,EAAI,EAAI,KAAK,IAAI,CAAM,CAAC,EAAI,KAAK,IAAM,CAChE,CAClB,CASA,SAAgB,EAAkB,EAAkC,CAChE,IAAM,EAAY,EAAM,EAAI,IAAM,IAC5B,EAAI,KAAK,IAAM,EAAI,EAAI,EAAM,GAEnC,MAAO,CAAE,SADS,KAAK,KAAK,KAAK,KAAK,CAAC,CAAC,EAAI,IAAO,KAAK,GACrC,WAAU,CACjC,
|
|
1
|
+
{"version":3,"file":"projection.cjs","names":[],"sources":["../../src/geo/projection.ts"],"sourcesContent":["import { clampLatitude } from \"./types\";\nimport type { Coordinate, GeoBounds } from \"./types\";\n\n/**\n * A point projected onto the unit Web Mercator plane. Both axes are in `[0, 1]`\n * — `x` grows east, `y` grows south (screen convention).\n */\nexport interface MercatorPoint {\n x: number;\n y: number;\n}\n\n/** A pixel coordinate inside the plotting viewport. */\nexport interface PixelPoint {\n x: number;\n y: number;\n}\n\n/**\n * Web Mercator (EPSG:3857) latitude clamp. Latitudes beyond this diverge to\n * infinity in the projection, so tile maps cap here.\n */\nexport const MERCATOR_MAX_LATITUDE = 85.05112878;\n\n/**\n * Project a geographic coordinate onto the unit Web Mercator plane. This is the\n * same projection tile servers use, so a self-hosted tile layer and the\n * tile-free SVG plot line up pixel-for-pixel.\n *\n * @param coord - Coordinate to project.\n * @returns `{ x, y }` in `[0, 1]`.\n */\nexport function projectMercator(coord: Coordinate): MercatorPoint {\n const lat = Math.max(\n -MERCATOR_MAX_LATITUDE,\n Math.min(MERCATOR_MAX_LATITUDE, clampLatitude(coord.latitude)),\n );\n const latRad = (lat * Math.PI) / 180;\n const x = (coord.longitude + 180) / 360;\n const y = (1 - Math.log(Math.tan(latRad) + 1 / Math.cos(latRad)) / Math.PI) / 2;\n return { x, y };\n}\n\n/**\n * Inverse of {@link projectMercator}: recover a coordinate from a unit-plane\n * point.\n *\n * @param point - `{ x, y }` in `[0, 1]`.\n * @returns The geographic coordinate.\n */\nexport function unprojectMercator(point: MercatorPoint): Coordinate {\n const longitude = point.x * 360 - 180;\n const n = Math.PI * (1 - 2 * point.y);\n const latitude = (Math.atan(Math.sinh(n)) * 180) / Math.PI;\n return { latitude, longitude };\n}\n\n/** A ready-to-use mapping from coordinates to viewport pixels. */\nexport interface FittedProjection {\n /** Project a coordinate to a pixel inside the viewport. */\n project: (coord: Coordinate) => PixelPoint;\n /** Uniform scale (unit-plane → pixels) actually used, after aspect fit. */\n scale: number;\n /** Viewport width in pixels. */\n width: number;\n /** Viewport height in pixels. */\n height: number;\n}\n\n/** Options for {@link fitProjection}. */\nexport interface FitProjectionOptions {\n /** Inner padding in pixels kept clear on every edge. Default: `16`. */\n padding?: number;\n}\n\n/**\n * Build a projection that fits `bounds` into a `width × height` viewport while\n * preserving aspect ratio (uniform scale, centered). This is what powers the\n * tile-free trajectory plot: project the bounds, scale to the SVG box, keep\n * shapes undistorted.\n *\n * @param bounds - Geographic extent to fit.\n * @param width - Viewport width in pixels.\n * @param height - Viewport height in pixels.\n * @param options - Padding tuning.\n * @returns A {@link FittedProjection} with a `project(coord)` mapper.\n *\n * @tempest-limits param-count — `(bounds, width, height)` is the viewport the caller\n * measured, usually straight out of a `getBoundingClientRect()`, and the fourth\n * argument is already the options object. Public surface via the package root:\n * renaming these into one bag breaks callers and buys a count, not clarity.\n */\nexport function fitProjection(\n bounds: GeoBounds,\n width: number,\n height: number,\n options: FitProjectionOptions = {},\n): FittedProjection {\n const { padding = 16 } = options;\n\n const topLeft = projectMercator({\n latitude: bounds.maxLatitude,\n longitude: bounds.minLongitude,\n });\n const bottomRight = projectMercator({\n latitude: bounds.minLatitude,\n longitude: bounds.maxLongitude,\n });\n\n const spanX = bottomRight.x - topLeft.x || Number.EPSILON;\n const spanY = bottomRight.y - topLeft.y || Number.EPSILON;\n\n const innerWidth = Math.max(1, width - padding * 2);\n const innerHeight = Math.max(1, height - padding * 2);\n\n // Uniform scale keeps the trajectory undistorted; fit the tighter axis.\n const scale = Math.min(innerWidth / spanX, innerHeight / spanY);\n\n // Center the projected content within the padded box.\n const offsetX = padding + (innerWidth - spanX * scale) / 2;\n const offsetY = padding + (innerHeight - spanY * scale) / 2;\n\n const project = (coord: Coordinate): PixelPoint => {\n const projected = projectMercator(coord);\n return {\n x: offsetX + (projected.x - topLeft.x) * scale,\n y: offsetY + (projected.y - topLeft.y) * scale,\n };\n };\n\n return { project, scale, width, height };\n}\n"],"mappings":"+BAsBA,IAAa,EAAwB,YAUrC,SAAgB,EAAgB,EAAkC,CAK9D,IAAM,EAJM,KAAK,IACb,aACA,KAAK,IAAI,EAAuB,EAAA,cAAc,EAAM,QAAQ,CAAC,CAEjD,EAAM,KAAK,GAAM,IAGjC,MAAO,CAAE,GAFE,EAAM,UAAY,KAAO,IAExB,GADD,EAAI,KAAK,IAAI,KAAK,IAAI,CAAM,EAAI,EAAI,KAAK,IAAI,CAAM,CAAC,EAAI,KAAK,IAAM,CAChE,CAClB,CASA,SAAgB,EAAkB,EAAkC,CAChE,IAAM,EAAY,EAAM,EAAI,IAAM,IAC5B,EAAI,KAAK,IAAM,EAAI,EAAI,EAAM,GAEnC,MAAO,CAAE,SADS,KAAK,KAAK,KAAK,KAAK,CAAC,CAAC,EAAI,IAAO,KAAK,GACrC,WAAU,CACjC,CAqCA,SAAgB,EACZ,EACA,EACA,EACA,EAAgC,CAAC,EACjB,CAChB,GAAM,CAAE,UAAU,IAAO,EAEnB,EAAU,EAAgB,CAC5B,SAAU,EAAO,YACjB,UAAW,EAAO,YACtB,CAAC,EACK,EAAc,EAAgB,CAChC,SAAU,EAAO,YACjB,UAAW,EAAO,YACtB,CAAC,EAEK,EAAQ,EAAY,EAAI,EAAQ,UAChC,EAAQ,EAAY,EAAI,EAAQ,UAEhC,EAAa,KAAK,IAAI,EAAG,EAAQ,EAAU,CAAC,EAC5C,EAAc,KAAK,IAAI,EAAG,EAAS,EAAU,CAAC,EAG9C,EAAQ,KAAK,IAAI,EAAa,EAAO,EAAc,CAAK,EAGxD,EAAU,GAAW,EAAa,EAAQ,GAAS,EACnD,EAAU,GAAW,EAAc,EAAQ,GAAS,EAU1D,MAAO,CAAE,QARQ,GAAkC,CAC/C,IAAM,EAAY,EAAgB,CAAK,EACvC,MAAO,CACH,EAAG,GAAW,EAAU,EAAI,EAAQ,GAAK,EACzC,EAAG,GAAW,EAAU,EAAI,EAAQ,GAAK,CAC7C,CACJ,EAEkB,QAAO,QAAO,QAAO,CAC3C"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"projection.js","names":[],"sources":["../../src/geo/projection.ts"],"sourcesContent":["import { clampLatitude } from \"./types\";\nimport type { Coordinate, GeoBounds } from \"./types\";\n\n/**\n * A point projected onto the unit Web Mercator plane. Both axes are in `[0, 1]`\n * — `x` grows east, `y` grows south (screen convention).\n */\nexport interface MercatorPoint {\n x: number;\n y: number;\n}\n\n/** A pixel coordinate inside the plotting viewport. */\nexport interface PixelPoint {\n x: number;\n y: number;\n}\n\n/**\n * Web Mercator (EPSG:3857) latitude clamp. Latitudes beyond this diverge to\n * infinity in the projection, so tile maps cap here.\n */\nexport const MERCATOR_MAX_LATITUDE = 85.05112878;\n\n/**\n * Project a geographic coordinate onto the unit Web Mercator plane. This is the\n * same projection tile servers use, so a self-hosted tile layer and the\n * tile-free SVG plot line up pixel-for-pixel.\n *\n * @param coord - Coordinate to project.\n * @returns `{ x, y }` in `[0, 1]`.\n */\nexport function projectMercator(coord: Coordinate): MercatorPoint {\n const lat = Math.max(\n -MERCATOR_MAX_LATITUDE,\n Math.min(MERCATOR_MAX_LATITUDE, clampLatitude(coord.latitude)),\n );\n const latRad = (lat * Math.PI) / 180;\n const x = (coord.longitude + 180) / 360;\n const y = (1 - Math.log(Math.tan(latRad) + 1 / Math.cos(latRad)) / Math.PI) / 2;\n return { x, y };\n}\n\n/**\n * Inverse of {@link projectMercator}: recover a coordinate from a unit-plane\n * point.\n *\n * @param point - `{ x, y }` in `[0, 1]`.\n * @returns The geographic coordinate.\n */\nexport function unprojectMercator(point: MercatorPoint): Coordinate {\n const longitude = point.x * 360 - 180;\n const n = Math.PI * (1 - 2 * point.y);\n const latitude = (Math.atan(Math.sinh(n)) * 180) / Math.PI;\n return { latitude, longitude };\n}\n\n/** A ready-to-use mapping from coordinates to viewport pixels. */\nexport interface FittedProjection {\n /** Project a coordinate to a pixel inside the viewport. */\n project: (coord: Coordinate) => PixelPoint;\n /** Uniform scale (unit-plane → pixels) actually used, after aspect fit. */\n scale: number;\n /** Viewport width in pixels. */\n width: number;\n /** Viewport height in pixels. */\n height: number;\n}\n\n/** Options for {@link fitProjection}. */\nexport interface FitProjectionOptions {\n /** Inner padding in pixels kept clear on every edge. Default: `16`. */\n padding?: number;\n}\n\n/**\n * Build a projection that fits `bounds` into a `width × height` viewport while\n * preserving aspect ratio (uniform scale, centered). This is what powers the\n * tile-free trajectory plot: project the bounds, scale to the SVG box, keep\n * shapes undistorted.\n *\n * @param bounds - Geographic extent to fit.\n * @param width - Viewport width in pixels.\n * @param height - Viewport height in pixels.\n * @param options - Padding tuning.\n * @returns A {@link FittedProjection} with a `project(coord)` mapper.\n */\nexport function fitProjection(\n bounds: GeoBounds,\n width: number,\n height: number,\n options: FitProjectionOptions = {},\n): FittedProjection {\n const { padding = 16 } = options;\n\n const topLeft = projectMercator({\n latitude: bounds.maxLatitude,\n longitude: bounds.minLongitude,\n });\n const bottomRight = projectMercator({\n latitude: bounds.minLatitude,\n longitude: bounds.maxLongitude,\n });\n\n const spanX = bottomRight.x - topLeft.x || Number.EPSILON;\n const spanY = bottomRight.y - topLeft.y || Number.EPSILON;\n\n const innerWidth = Math.max(1, width - padding * 2);\n const innerHeight = Math.max(1, height - padding * 2);\n\n // Uniform scale keeps the trajectory undistorted; fit the tighter axis.\n const scale = Math.min(innerWidth / spanX, innerHeight / spanY);\n\n // Center the projected content within the padded box.\n const offsetX = padding + (innerWidth - spanX * scale) / 2;\n const offsetY = padding + (innerHeight - spanY * scale) / 2;\n\n const project = (coord: Coordinate): PixelPoint => {\n const projected = projectMercator(coord);\n return {\n x: offsetX + (projected.x - topLeft.x) * scale,\n y: offsetY + (projected.y - topLeft.y) * scale,\n };\n };\n\n return { project, scale, width, height };\n}\n"],"mappings":";;AAsBA,IAAa,IAAwB;AAUrC,SAAgB,EAAgB,GAAkC;CAK9D,IAAM,IAJM,KAAK,IACb,cACA,KAAK,IAAI,GAAuB,EAAc,EAAM,QAAQ,CAAC,CAEjD,IAAM,KAAK,KAAM;CAGjC,OAAO;EAAE,IAFE,EAAM,YAAY,OAAO;EAExB,IADD,IAAI,KAAK,IAAI,KAAK,IAAI,CAAM,IAAI,IAAI,KAAK,IAAI,CAAM,CAAC,IAAI,KAAK,MAAM;CAChE;AAClB;AASA,SAAgB,EAAkB,GAAkC;CAChE,IAAM,IAAY,EAAM,IAAI,MAAM,KAC5B,IAAI,KAAK,MAAM,IAAI,IAAI,EAAM;CAEnC,OAAO;EAAE,UADS,KAAK,KAAK,KAAK,KAAK,CAAC,CAAC,IAAI,MAAO,KAAK;EACrC;CAAU;AACjC;
|
|
1
|
+
{"version":3,"file":"projection.js","names":[],"sources":["../../src/geo/projection.ts"],"sourcesContent":["import { clampLatitude } from \"./types\";\nimport type { Coordinate, GeoBounds } from \"./types\";\n\n/**\n * A point projected onto the unit Web Mercator plane. Both axes are in `[0, 1]`\n * — `x` grows east, `y` grows south (screen convention).\n */\nexport interface MercatorPoint {\n x: number;\n y: number;\n}\n\n/** A pixel coordinate inside the plotting viewport. */\nexport interface PixelPoint {\n x: number;\n y: number;\n}\n\n/**\n * Web Mercator (EPSG:3857) latitude clamp. Latitudes beyond this diverge to\n * infinity in the projection, so tile maps cap here.\n */\nexport const MERCATOR_MAX_LATITUDE = 85.05112878;\n\n/**\n * Project a geographic coordinate onto the unit Web Mercator plane. This is the\n * same projection tile servers use, so a self-hosted tile layer and the\n * tile-free SVG plot line up pixel-for-pixel.\n *\n * @param coord - Coordinate to project.\n * @returns `{ x, y }` in `[0, 1]`.\n */\nexport function projectMercator(coord: Coordinate): MercatorPoint {\n const lat = Math.max(\n -MERCATOR_MAX_LATITUDE,\n Math.min(MERCATOR_MAX_LATITUDE, clampLatitude(coord.latitude)),\n );\n const latRad = (lat * Math.PI) / 180;\n const x = (coord.longitude + 180) / 360;\n const y = (1 - Math.log(Math.tan(latRad) + 1 / Math.cos(latRad)) / Math.PI) / 2;\n return { x, y };\n}\n\n/**\n * Inverse of {@link projectMercator}: recover a coordinate from a unit-plane\n * point.\n *\n * @param point - `{ x, y }` in `[0, 1]`.\n * @returns The geographic coordinate.\n */\nexport function unprojectMercator(point: MercatorPoint): Coordinate {\n const longitude = point.x * 360 - 180;\n const n = Math.PI * (1 - 2 * point.y);\n const latitude = (Math.atan(Math.sinh(n)) * 180) / Math.PI;\n return { latitude, longitude };\n}\n\n/** A ready-to-use mapping from coordinates to viewport pixels. */\nexport interface FittedProjection {\n /** Project a coordinate to a pixel inside the viewport. */\n project: (coord: Coordinate) => PixelPoint;\n /** Uniform scale (unit-plane → pixels) actually used, after aspect fit. */\n scale: number;\n /** Viewport width in pixels. */\n width: number;\n /** Viewport height in pixels. */\n height: number;\n}\n\n/** Options for {@link fitProjection}. */\nexport interface FitProjectionOptions {\n /** Inner padding in pixels kept clear on every edge. Default: `16`. */\n padding?: number;\n}\n\n/**\n * Build a projection that fits `bounds` into a `width × height` viewport while\n * preserving aspect ratio (uniform scale, centered). This is what powers the\n * tile-free trajectory plot: project the bounds, scale to the SVG box, keep\n * shapes undistorted.\n *\n * @param bounds - Geographic extent to fit.\n * @param width - Viewport width in pixels.\n * @param height - Viewport height in pixels.\n * @param options - Padding tuning.\n * @returns A {@link FittedProjection} with a `project(coord)` mapper.\n *\n * @tempest-limits param-count — `(bounds, width, height)` is the viewport the caller\n * measured, usually straight out of a `getBoundingClientRect()`, and the fourth\n * argument is already the options object. Public surface via the package root:\n * renaming these into one bag breaks callers and buys a count, not clarity.\n */\nexport function fitProjection(\n bounds: GeoBounds,\n width: number,\n height: number,\n options: FitProjectionOptions = {},\n): FittedProjection {\n const { padding = 16 } = options;\n\n const topLeft = projectMercator({\n latitude: bounds.maxLatitude,\n longitude: bounds.minLongitude,\n });\n const bottomRight = projectMercator({\n latitude: bounds.minLatitude,\n longitude: bounds.maxLongitude,\n });\n\n const spanX = bottomRight.x - topLeft.x || Number.EPSILON;\n const spanY = bottomRight.y - topLeft.y || Number.EPSILON;\n\n const innerWidth = Math.max(1, width - padding * 2);\n const innerHeight = Math.max(1, height - padding * 2);\n\n // Uniform scale keeps the trajectory undistorted; fit the tighter axis.\n const scale = Math.min(innerWidth / spanX, innerHeight / spanY);\n\n // Center the projected content within the padded box.\n const offsetX = padding + (innerWidth - spanX * scale) / 2;\n const offsetY = padding + (innerHeight - spanY * scale) / 2;\n\n const project = (coord: Coordinate): PixelPoint => {\n const projected = projectMercator(coord);\n return {\n x: offsetX + (projected.x - topLeft.x) * scale,\n y: offsetY + (projected.y - topLeft.y) * scale,\n };\n };\n\n return { project, scale, width, height };\n}\n"],"mappings":";;AAsBA,IAAa,IAAwB;AAUrC,SAAgB,EAAgB,GAAkC;CAK9D,IAAM,IAJM,KAAK,IACb,cACA,KAAK,IAAI,GAAuB,EAAc,EAAM,QAAQ,CAAC,CAEjD,IAAM,KAAK,KAAM;CAGjC,OAAO;EAAE,IAFE,EAAM,YAAY,OAAO;EAExB,IADD,IAAI,KAAK,IAAI,KAAK,IAAI,CAAM,IAAI,IAAI,KAAK,IAAI,CAAM,CAAC,IAAI,KAAK,MAAM;CAChE;AAClB;AASA,SAAgB,EAAkB,GAAkC;CAChE,IAAM,IAAY,EAAM,IAAI,MAAM,KAC5B,IAAI,KAAK,MAAM,IAAI,IAAI,EAAM;CAEnC,OAAO;EAAE,UADS,KAAK,KAAK,KAAK,KAAK,CAAC,CAAC,IAAI,MAAO,KAAK;EACrC;CAAU;AACjC;AAqCA,SAAgB,EACZ,GACA,GACA,GACA,IAAgC,CAAC,GACjB;CAChB,IAAM,EAAE,aAAU,OAAO,GAEnB,IAAU,EAAgB;EAC5B,UAAU,EAAO;EACjB,WAAW,EAAO;CACtB,CAAC,GACK,IAAc,EAAgB;EAChC,UAAU,EAAO;EACjB,WAAW,EAAO;CACtB,CAAC,GAEK,IAAQ,EAAY,IAAI,EAAQ,eAChC,IAAQ,EAAY,IAAI,EAAQ,eAEhC,IAAa,KAAK,IAAI,GAAG,IAAQ,IAAU,CAAC,GAC5C,IAAc,KAAK,IAAI,GAAG,IAAS,IAAU,CAAC,GAG9C,IAAQ,KAAK,IAAI,IAAa,GAAO,IAAc,CAAK,GAGxD,IAAU,KAAW,IAAa,IAAQ,KAAS,GACnD,IAAU,KAAW,IAAc,IAAQ,KAAS;CAU1D,OAAO;EAAE,UARQ,MAAkC;GAC/C,IAAM,IAAY,EAAgB,CAAK;GACvC,OAAO;IACH,GAAG,KAAW,EAAU,IAAI,EAAQ,KAAK;IACzC,GAAG,KAAW,EAAU,IAAI,EAAQ,KAAK;GAC7C;EACJ;EAEkB;EAAO;EAAO;CAAO;AAC3C"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-event-listener.cjs","names":[],"sources":["../../src/hooks/use-event-listener.ts"],"sourcesContent":["import { useEffect, useRef } from \"react\";\n\ntype AnyEventTarget = EventTarget | { current: EventTarget | null } | null | undefined;\n\n/**\n * Subscribe to a DOM event with React-friendly semantics.\n *\n * - Handler is stored in a ref so callers can pass inline functions without\n * resubscribing on every render.\n * - `target` defaults to `window`. Accepts a raw `EventTarget` (window, document,\n * element) OR a ref pointing at one.\n * - Returns nothing — cleanup is automatic on unmount or when `eventName`/`target` change.\n */\nexport function useEventListener<K extends keyof WindowEventMap>(\n eventName: K,\n handler: (event: WindowEventMap[K]) => void,\n target?: AnyEventTarget,\n options?: AddEventListenerOptions | boolean,\n): void;\nexport function useEventListener<K extends keyof DocumentEventMap>(\n eventName: K,\n handler: (event: DocumentEventMap[K]) => void,\n target?: AnyEventTarget,\n options?: AddEventListenerOptions | boolean,\n): void;\nexport function useEventListener<K extends keyof HTMLElementEventMap>(\n eventName: K,\n handler: (event: HTMLElementEventMap[K]) => void,\n target?: AnyEventTarget,\n options?: AddEventListenerOptions | boolean,\n): void;\nexport function useEventListener(\n eventName: string,\n handler: (event: Event) => void,\n target?: AnyEventTarget,\n options?: AddEventListenerOptions | boolean,\n): void {\n const handlerRef = useRef(handler);\n useEffect(() => {\n handlerRef.current = handler;\n }, [handler]);\n\n useEffect(() => {\n const resolvedTarget: EventTarget | null =\n target === undefined\n ? typeof window === \"undefined\"\n ? null\n : window\n : \"current\" in (target as { current: unknown })\n ? ((target as { current: EventTarget | null }).current ?? null)\n : (target as EventTarget | null);\n\n if (!resolvedTarget?.addEventListener) return;\n\n const listener: EventListener = (event) => handlerRef.current(event);\n resolvedTarget.addEventListener(eventName, listener, options);\n return () => resolvedTarget.removeEventListener(eventName, listener, options);\n }, [eventName, target, options]);\n}\n"],"mappings":"
|
|
1
|
+
{"version":3,"file":"use-event-listener.cjs","names":[],"sources":["../../src/hooks/use-event-listener.ts"],"sourcesContent":["import { useEffect, useRef } from \"react\";\n\ntype AnyEventTarget = EventTarget | { current: EventTarget | null } | null | undefined;\n\n/**\n * Subscribe to a DOM event with React-friendly semantics.\n *\n * - Handler is stored in a ref so callers can pass inline functions without\n * resubscribing on every render.\n * - `target` defaults to `window`. Accepts a raw `EventTarget` (window, document,\n * element) OR a ref pointing at one.\n * - Returns nothing — cleanup is automatic on unmount or when `eventName`/`target` change.\n *\n * @tempest-limits param-count — deliberately the same positional shape as\n * `addEventListener(type, listener, options)`, with the target inserted where React\n * needs it. A hook that mirrors a DOM API is read by everyone who already knows the\n * DOM API; a bespoke options bag would make the familiar call unfamiliar.\n */\nexport function useEventListener<K extends keyof WindowEventMap>(\n eventName: K,\n handler: (event: WindowEventMap[K]) => void,\n target?: AnyEventTarget,\n options?: AddEventListenerOptions | boolean,\n): void;\nexport function useEventListener<K extends keyof DocumentEventMap>(\n eventName: K,\n handler: (event: DocumentEventMap[K]) => void,\n target?: AnyEventTarget,\n options?: AddEventListenerOptions | boolean,\n): void;\nexport function useEventListener<K extends keyof HTMLElementEventMap>(\n eventName: K,\n handler: (event: HTMLElementEventMap[K]) => void,\n target?: AnyEventTarget,\n options?: AddEventListenerOptions | boolean,\n): void;\nexport function useEventListener(\n eventName: string,\n handler: (event: Event) => void,\n target?: AnyEventTarget,\n options?: AddEventListenerOptions | boolean,\n): void {\n const handlerRef = useRef(handler);\n useEffect(() => {\n handlerRef.current = handler;\n }, [handler]);\n\n useEffect(() => {\n const resolvedTarget: EventTarget | null =\n target === undefined\n ? typeof window === \"undefined\"\n ? null\n : window\n : \"current\" in (target as { current: unknown })\n ? ((target as { current: EventTarget | null }).current ?? null)\n : (target as EventTarget | null);\n\n if (!resolvedTarget?.addEventListener) return;\n\n const listener: EventListener = (event) => handlerRef.current(event);\n resolvedTarget.addEventListener(eventName, listener, options);\n return () => resolvedTarget.removeEventListener(eventName, listener, options);\n }, [eventName, target, options]);\n}\n"],"mappings":"uBAoCA,SAAgB,EACZ,EACA,EACA,EACA,EACI,CACJ,IAAM,GAAA,EAAA,EAAA,OAAA,CAAoB,CAAO,GACjC,EAAA,EAAA,UAAA,KAAgB,CACZ,EAAW,QAAU,CACzB,EAAG,CAAC,CAAO,CAAC,GAEZ,EAAA,EAAA,UAAA,KAAgB,CACZ,IAAM,EACF,IAAW,IAAA,GACL,OAAO,OAAW,IACd,KACA,OACJ,YAAc,EACV,EAA2C,SAAW,KACvD,EAEb,GAAI,CAAC,GAAgB,iBAAkB,OAEvC,IAAM,EAA2B,GAAU,EAAW,QAAQ,CAAK,EAEnE,OADA,EAAe,iBAAiB,EAAW,EAAU,CAAO,MAC/C,EAAe,oBAAoB,EAAW,EAAU,CAAO,CAChF,EAAG,CAAC,EAAW,EAAQ,CAAO,CAAC,CACnC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-event-listener.js","names":[],"sources":["../../src/hooks/use-event-listener.ts"],"sourcesContent":["import { useEffect, useRef } from \"react\";\n\ntype AnyEventTarget = EventTarget | { current: EventTarget | null } | null | undefined;\n\n/**\n * Subscribe to a DOM event with React-friendly semantics.\n *\n * - Handler is stored in a ref so callers can pass inline functions without\n * resubscribing on every render.\n * - `target` defaults to `window`. Accepts a raw `EventTarget` (window, document,\n * element) OR a ref pointing at one.\n * - Returns nothing — cleanup is automatic on unmount or when `eventName`/`target` change.\n */\nexport function useEventListener<K extends keyof WindowEventMap>(\n eventName: K,\n handler: (event: WindowEventMap[K]) => void,\n target?: AnyEventTarget,\n options?: AddEventListenerOptions | boolean,\n): void;\nexport function useEventListener<K extends keyof DocumentEventMap>(\n eventName: K,\n handler: (event: DocumentEventMap[K]) => void,\n target?: AnyEventTarget,\n options?: AddEventListenerOptions | boolean,\n): void;\nexport function useEventListener<K extends keyof HTMLElementEventMap>(\n eventName: K,\n handler: (event: HTMLElementEventMap[K]) => void,\n target?: AnyEventTarget,\n options?: AddEventListenerOptions | boolean,\n): void;\nexport function useEventListener(\n eventName: string,\n handler: (event: Event) => void,\n target?: AnyEventTarget,\n options?: AddEventListenerOptions | boolean,\n): void {\n const handlerRef = useRef(handler);\n useEffect(() => {\n handlerRef.current = handler;\n }, [handler]);\n\n useEffect(() => {\n const resolvedTarget: EventTarget | null =\n target === undefined\n ? typeof window === \"undefined\"\n ? null\n : window\n : \"current\" in (target as { current: unknown })\n ? ((target as { current: EventTarget | null }).current ?? null)\n : (target as EventTarget | null);\n\n if (!resolvedTarget?.addEventListener) return;\n\n const listener: EventListener = (event) => handlerRef.current(event);\n resolvedTarget.addEventListener(eventName, listener, options);\n return () => resolvedTarget.removeEventListener(eventName, listener, options);\n }, [eventName, target, options]);\n}\n"],"mappings":";;
|
|
1
|
+
{"version":3,"file":"use-event-listener.js","names":[],"sources":["../../src/hooks/use-event-listener.ts"],"sourcesContent":["import { useEffect, useRef } from \"react\";\n\ntype AnyEventTarget = EventTarget | { current: EventTarget | null } | null | undefined;\n\n/**\n * Subscribe to a DOM event with React-friendly semantics.\n *\n * - Handler is stored in a ref so callers can pass inline functions without\n * resubscribing on every render.\n * - `target` defaults to `window`. Accepts a raw `EventTarget` (window, document,\n * element) OR a ref pointing at one.\n * - Returns nothing — cleanup is automatic on unmount or when `eventName`/`target` change.\n *\n * @tempest-limits param-count — deliberately the same positional shape as\n * `addEventListener(type, listener, options)`, with the target inserted where React\n * needs it. A hook that mirrors a DOM API is read by everyone who already knows the\n * DOM API; a bespoke options bag would make the familiar call unfamiliar.\n */\nexport function useEventListener<K extends keyof WindowEventMap>(\n eventName: K,\n handler: (event: WindowEventMap[K]) => void,\n target?: AnyEventTarget,\n options?: AddEventListenerOptions | boolean,\n): void;\nexport function useEventListener<K extends keyof DocumentEventMap>(\n eventName: K,\n handler: (event: DocumentEventMap[K]) => void,\n target?: AnyEventTarget,\n options?: AddEventListenerOptions | boolean,\n): void;\nexport function useEventListener<K extends keyof HTMLElementEventMap>(\n eventName: K,\n handler: (event: HTMLElementEventMap[K]) => void,\n target?: AnyEventTarget,\n options?: AddEventListenerOptions | boolean,\n): void;\nexport function useEventListener(\n eventName: string,\n handler: (event: Event) => void,\n target?: AnyEventTarget,\n options?: AddEventListenerOptions | boolean,\n): void {\n const handlerRef = useRef(handler);\n useEffect(() => {\n handlerRef.current = handler;\n }, [handler]);\n\n useEffect(() => {\n const resolvedTarget: EventTarget | null =\n target === undefined\n ? typeof window === \"undefined\"\n ? null\n : window\n : \"current\" in (target as { current: unknown })\n ? ((target as { current: EventTarget | null }).current ?? null)\n : (target as EventTarget | null);\n\n if (!resolvedTarget?.addEventListener) return;\n\n const listener: EventListener = (event) => handlerRef.current(event);\n resolvedTarget.addEventListener(eventName, listener, options);\n return () => resolvedTarget.removeEventListener(eventName, listener, options);\n }, [eventName, target, options]);\n}\n"],"mappings":";;AAoCA,SAAgB,EACZ,GACA,GACA,GACA,GACI;CACJ,IAAM,IAAa,EAAO,CAAO;CAKjC,AAJA,QAAgB;EACZ,EAAW,UAAU;CACzB,GAAG,CAAC,CAAO,CAAC,GAEZ,QAAgB;EACZ,IAAM,IACF,MAAW,KAAA,IACL,OAAO,SAAW,MACd,OACA,SACJ,aAAc,IACV,EAA2C,WAAW,OACvD;EAEb,IAAI,CAAC,GAAgB,kBAAkB;EAEvC,IAAM,KAA2B,MAAU,EAAW,QAAQ,CAAK;EAEnE,OADA,EAAe,iBAAiB,GAAW,GAAU,CAAO,SAC/C,EAAe,oBAAoB,GAAW,GAAU,CAAO;CAChF,GAAG;EAAC;EAAW;EAAQ;CAAO,CAAC;AACnC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-local-storage.cjs","names":[],"sources":["../../src/hooks/use-local-storage.ts"],"sourcesContent":["import { useCallback, useEffect, useState } from \"react\";\n\nexport type LocalStorageOptions<T> = {\n serialize?: (value: T) => string;\n deserialize?: (raw: string) => T;\n};\n\nconst defaultSerialize = JSON.stringify;\nconst defaultDeserialize = <T>(raw: string): T => JSON.parse(raw) as T;\n\n/**\n * State synced with `localStorage`. SSR-safe — initial render returns the\n * provided default; the stored value is hydrated after mount. Updates to the\n * same key in other tabs are picked up via the `storage` event.\n *\n * @param key - localStorage key.\n * @param defaultValue - value used when nothing is stored or in SSR.\n * @param options - custom `serialize` / `deserialize` (default JSON).\n * @returns Tuple `[value, setValue, remove]`.\n */\nexport function useLocalStorage<T>(\n key: string,\n defaultValue: T,\n options: LocalStorageOptions<T> = {},\n): [T, (value: T | ((prev: T) => T)) => void, () => void] {\n const serialize = options.serialize ?? defaultSerialize;\n const deserialize = options.deserialize ?? defaultDeserialize;\n\n const read = useCallback((): T => {\n if (typeof window === \"undefined\") return defaultValue;\n try {\n const raw = window.localStorage.getItem(key);\n if (raw === null) return defaultValue;\n return deserialize(raw);\n } catch {\n return defaultValue;\n }\n }, [key, defaultValue, deserialize]);\n\n const [value, setStored] = useState<T>(read);\n\n useEffect(() => {\n setStored(read());\n }, [read]);\n\n const setValue = useCallback(\n (next: T | ((prev: T) => T)): void => {\n setStored((current) => {\n const resolved =\n typeof next === \"function\" ? (next as (prev: T) => T)(current) : next;\n try {\n if (typeof window !== \"undefined\") {\n window.localStorage.setItem(key, serialize(resolved));\n }\n } catch {\n /*
|
|
1
|
+
{"version":3,"file":"use-local-storage.cjs","names":[],"sources":["../../src/hooks/use-local-storage.ts"],"sourcesContent":["import { useCallback, useEffect, useState } from \"react\";\n\nexport type LocalStorageOptions<T> = {\n serialize?: (value: T) => string;\n deserialize?: (raw: string) => T;\n};\n\nconst defaultSerialize = JSON.stringify;\nconst defaultDeserialize = <T>(raw: string): T => JSON.parse(raw) as T;\n\n/**\n * State synced with `localStorage`. SSR-safe — initial render returns the\n * provided default; the stored value is hydrated after mount. Updates to the\n * same key in other tabs are picked up via the `storage` event.\n *\n * @tempest-limits empty-catch — the three swallows here are the hook's contract.\n * A write that hits the quota (or Safari private mode) still updates React state,\n * so the session keeps working and only persistence is lost; a `storage` event\n * carrying a value this tab cannot deserialize is another tab's bug, and the\n * useful response is to keep the value already rendered rather than tear the\n * component down over a foreign write.\n *\n * @param key - localStorage key.\n * @param defaultValue - value used when nothing is stored or in SSR.\n * @param options - custom `serialize` / `deserialize` (default JSON).\n * @returns Tuple `[value, setValue, remove]`.\n */\nexport function useLocalStorage<T>(\n key: string,\n defaultValue: T,\n options: LocalStorageOptions<T> = {},\n): [T, (value: T | ((prev: T) => T)) => void, () => void] {\n const serialize = options.serialize ?? defaultSerialize;\n const deserialize = options.deserialize ?? defaultDeserialize;\n\n const read = useCallback((): T => {\n if (typeof window === \"undefined\") return defaultValue;\n try {\n const raw = window.localStorage.getItem(key);\n if (raw === null) return defaultValue;\n return deserialize(raw);\n } catch {\n return defaultValue;\n }\n }, [key, defaultValue, deserialize]);\n\n const [value, setStored] = useState<T>(read);\n\n useEffect(() => {\n setStored(read());\n }, [read]);\n\n const setValue = useCallback(\n (next: T | ((prev: T) => T)): void => {\n setStored((current) => {\n const resolved =\n typeof next === \"function\" ? (next as (prev: T) => T)(current) : next;\n try {\n if (typeof window !== \"undefined\") {\n window.localStorage.setItem(key, serialize(resolved));\n }\n } catch {\n /* empty */\n }\n return resolved;\n });\n },\n [key, serialize],\n );\n\n const remove = useCallback((): void => {\n try {\n if (typeof window !== \"undefined\") {\n window.localStorage.removeItem(key);\n }\n } catch {\n /* empty */\n }\n setStored(defaultValue);\n }, [key, defaultValue]);\n\n useEffect(() => {\n if (typeof window === \"undefined\") return;\n const onStorage = (event: StorageEvent): void => {\n if (event.key !== key) return;\n if (event.newValue === null) {\n setStored(defaultValue);\n return;\n }\n try {\n setStored(deserialize(event.newValue));\n } catch {\n /* empty */\n }\n };\n window.addEventListener(\"storage\", onStorage);\n return () => window.removeEventListener(\"storage\", onStorage);\n }, [key, defaultValue, deserialize]);\n\n return [value, setValue, remove];\n}\n"],"mappings":"uBAOA,IAAM,EAAmB,KAAK,UACxB,EAAyB,GAAmB,KAAK,MAAM,CAAG,EAmBhE,SAAgB,EACZ,EACA,EACA,EAAkC,CAAC,EACmB,CACtD,IAAM,EAAY,EAAQ,WAAa,EACjC,EAAc,EAAQ,aAAe,EAErC,GAAA,EAAA,EAAA,YAAA,KAA4B,CAC9B,GAAI,OAAO,OAAW,IAAa,OAAO,EAC1C,GAAI,CACA,IAAM,EAAM,OAAO,aAAa,QAAQ,CAAG,EAE3C,OADI,IAAQ,KAAa,EAClB,EAAY,CAAG,CAC1B,MAAQ,CACJ,OAAO,CACX,CACJ,EAAG,CAAC,EAAK,EAAc,CAAW,CAAC,EAE7B,CAAC,EAAO,IAAA,EAAA,EAAA,SAAA,CAAyB,CAAI,GAE3C,EAAA,EAAA,UAAA,KAAgB,CACZ,EAAU,EAAK,CAAC,CACpB,EAAG,CAAC,CAAI,CAAC,EAET,IAAM,GAAA,EAAA,EAAA,YAAA,CACD,GAAqC,CAClC,EAAW,GAAY,CACnB,IAAM,EACF,OAAO,GAAS,WAAc,EAAwB,CAAO,EAAI,EACrE,GAAI,CACI,OAAO,OAAW,KAClB,OAAO,aAAa,QAAQ,EAAK,EAAU,CAAQ,CAAC,CAE5D,MAAQ,CAER,CACA,OAAO,CACX,CAAC,CACL,EACA,CAAC,EAAK,CAAS,CACnB,EAEM,GAAA,EAAA,EAAA,YAAA,KAAiC,CACnC,GAAI,CACI,OAAO,OAAW,KAClB,OAAO,aAAa,WAAW,CAAG,CAE1C,MAAQ,CAER,CACA,EAAU,CAAY,CAC1B,EAAG,CAAC,EAAK,CAAY,CAAC,EAoBtB,OAlBA,EAAA,EAAA,UAAA,KAAgB,CACZ,GAAI,OAAO,OAAW,IAAa,OACnC,IAAM,EAAa,GAA8B,CACzC,KAAM,MAAQ,EAClB,IAAI,EAAM,WAAa,KAAM,CACzB,EAAU,CAAY,EACtB,MACJ,CACA,GAAI,CACA,EAAU,EAAY,EAAM,QAAQ,CAAC,CACzC,MAAQ,CAER,CALA,CAMJ,EAEA,OADA,OAAO,iBAAiB,UAAW,CAAS,MAC/B,OAAO,oBAAoB,UAAW,CAAS,CAChE,EAAG,CAAC,EAAK,EAAc,CAAW,CAAC,EAE5B,CAAC,EAAO,EAAU,CAAM,CACnC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"use-local-storage.js","names":[],"sources":["../../src/hooks/use-local-storage.ts"],"sourcesContent":["import { useCallback, useEffect, useState } from \"react\";\n\nexport type LocalStorageOptions<T> = {\n serialize?: (value: T) => string;\n deserialize?: (raw: string) => T;\n};\n\nconst defaultSerialize = JSON.stringify;\nconst defaultDeserialize = <T>(raw: string): T => JSON.parse(raw) as T;\n\n/**\n * State synced with `localStorage`. SSR-safe — initial render returns the\n * provided default; the stored value is hydrated after mount. Updates to the\n * same key in other tabs are picked up via the `storage` event.\n *\n * @param key - localStorage key.\n * @param defaultValue - value used when nothing is stored or in SSR.\n * @param options - custom `serialize` / `deserialize` (default JSON).\n * @returns Tuple `[value, setValue, remove]`.\n */\nexport function useLocalStorage<T>(\n key: string,\n defaultValue: T,\n options: LocalStorageOptions<T> = {},\n): [T, (value: T | ((prev: T) => T)) => void, () => void] {\n const serialize = options.serialize ?? defaultSerialize;\n const deserialize = options.deserialize ?? defaultDeserialize;\n\n const read = useCallback((): T => {\n if (typeof window === \"undefined\") return defaultValue;\n try {\n const raw = window.localStorage.getItem(key);\n if (raw === null) return defaultValue;\n return deserialize(raw);\n } catch {\n return defaultValue;\n }\n }, [key, defaultValue, deserialize]);\n\n const [value, setStored] = useState<T>(read);\n\n useEffect(() => {\n setStored(read());\n }, [read]);\n\n const setValue = useCallback(\n (next: T | ((prev: T) => T)): void => {\n setStored((current) => {\n const resolved =\n typeof next === \"function\" ? (next as (prev: T) => T)(current) : next;\n try {\n if (typeof window !== \"undefined\") {\n window.localStorage.setItem(key, serialize(resolved));\n }\n } catch {\n /*
|
|
1
|
+
{"version":3,"file":"use-local-storage.js","names":[],"sources":["../../src/hooks/use-local-storage.ts"],"sourcesContent":["import { useCallback, useEffect, useState } from \"react\";\n\nexport type LocalStorageOptions<T> = {\n serialize?: (value: T) => string;\n deserialize?: (raw: string) => T;\n};\n\nconst defaultSerialize = JSON.stringify;\nconst defaultDeserialize = <T>(raw: string): T => JSON.parse(raw) as T;\n\n/**\n * State synced with `localStorage`. SSR-safe — initial render returns the\n * provided default; the stored value is hydrated after mount. Updates to the\n * same key in other tabs are picked up via the `storage` event.\n *\n * @tempest-limits empty-catch — the three swallows here are the hook's contract.\n * A write that hits the quota (or Safari private mode) still updates React state,\n * so the session keeps working and only persistence is lost; a `storage` event\n * carrying a value this tab cannot deserialize is another tab's bug, and the\n * useful response is to keep the value already rendered rather than tear the\n * component down over a foreign write.\n *\n * @param key - localStorage key.\n * @param defaultValue - value used when nothing is stored or in SSR.\n * @param options - custom `serialize` / `deserialize` (default JSON).\n * @returns Tuple `[value, setValue, remove]`.\n */\nexport function useLocalStorage<T>(\n key: string,\n defaultValue: T,\n options: LocalStorageOptions<T> = {},\n): [T, (value: T | ((prev: T) => T)) => void, () => void] {\n const serialize = options.serialize ?? defaultSerialize;\n const deserialize = options.deserialize ?? defaultDeserialize;\n\n const read = useCallback((): T => {\n if (typeof window === \"undefined\") return defaultValue;\n try {\n const raw = window.localStorage.getItem(key);\n if (raw === null) return defaultValue;\n return deserialize(raw);\n } catch {\n return defaultValue;\n }\n }, [key, defaultValue, deserialize]);\n\n const [value, setStored] = useState<T>(read);\n\n useEffect(() => {\n setStored(read());\n }, [read]);\n\n const setValue = useCallback(\n (next: T | ((prev: T) => T)): void => {\n setStored((current) => {\n const resolved =\n typeof next === \"function\" ? (next as (prev: T) => T)(current) : next;\n try {\n if (typeof window !== \"undefined\") {\n window.localStorage.setItem(key, serialize(resolved));\n }\n } catch {\n /* empty */\n }\n return resolved;\n });\n },\n [key, serialize],\n );\n\n const remove = useCallback((): void => {\n try {\n if (typeof window !== \"undefined\") {\n window.localStorage.removeItem(key);\n }\n } catch {\n /* empty */\n }\n setStored(defaultValue);\n }, [key, defaultValue]);\n\n useEffect(() => {\n if (typeof window === \"undefined\") return;\n const onStorage = (event: StorageEvent): void => {\n if (event.key !== key) return;\n if (event.newValue === null) {\n setStored(defaultValue);\n return;\n }\n try {\n setStored(deserialize(event.newValue));\n } catch {\n /* empty */\n }\n };\n window.addEventListener(\"storage\", onStorage);\n return () => window.removeEventListener(\"storage\", onStorage);\n }, [key, defaultValue, deserialize]);\n\n return [value, setValue, remove];\n}\n"],"mappings":";;AAOA,IAAM,IAAmB,KAAK,WACxB,KAAyB,MAAmB,KAAK,MAAM,CAAG;AAmBhE,SAAgB,EACZ,GACA,GACA,IAAkC,CAAC,GACmB;CACtD,IAAM,IAAY,EAAQ,aAAa,GACjC,IAAc,EAAQ,eAAe,GAErC,IAAO,QAAqB;EAC9B,IAAI,OAAO,SAAW,KAAa,OAAO;EAC1C,IAAI;GACA,IAAM,IAAM,OAAO,aAAa,QAAQ,CAAG;GAE3C,OADI,MAAQ,OAAa,IAClB,EAAY,CAAG;EAC1B,QAAQ;GACJ,OAAO;EACX;CACJ,GAAG;EAAC;EAAK;EAAc;CAAW,CAAC,GAE7B,CAAC,GAAO,KAAa,EAAY,CAAI;CAE3C,QAAgB;EACZ,EAAU,EAAK,CAAC;CACpB,GAAG,CAAC,CAAI,CAAC;CAET,IAAM,IAAW,GACZ,MAAqC;EAClC,GAAW,MAAY;GACnB,IAAM,IACF,OAAO,KAAS,aAAc,EAAwB,CAAO,IAAI;GACrE,IAAI;IACA,AAAI,OAAO,SAAW,OAClB,OAAO,aAAa,QAAQ,GAAK,EAAU,CAAQ,CAAC;GAE5D,QAAQ,CAER;GACA,OAAO;EACX,CAAC;CACL,GACA,CAAC,GAAK,CAAS,CACnB,GAEM,IAAS,QAAwB;EACnC,IAAI;GACA,AAAI,OAAO,SAAW,OAClB,OAAO,aAAa,WAAW,CAAG;EAE1C,QAAQ,CAER;EACA,EAAU,CAAY;CAC1B,GAAG,CAAC,GAAK,CAAY,CAAC;CAoBtB,OAlBA,QAAgB;EACZ,IAAI,OAAO,SAAW,KAAa;EACnC,IAAM,KAAa,MAA8B;GACzC,MAAM,QAAQ,GAClB;QAAI,EAAM,aAAa,MAAM;KACzB,EAAU,CAAY;KACtB;IACJ;IACA,IAAI;KACA,EAAU,EAAY,EAAM,QAAQ,CAAC;IACzC,QAAQ,CAER;GALA;EAMJ;EAEA,OADA,OAAO,iBAAiB,WAAW,CAAS,SAC/B,OAAO,oBAAoB,WAAW,CAAS;CAChE,GAAG;EAAC;EAAK;EAAc;CAAW,CAAC,GAE5B;EAAC;EAAO;EAAU;CAAM;AACnC"}
|