xertica-ui 2.2.1 → 2.4.0
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/CHANGELOG.md +564 -525
- package/README.md +417 -382
- package/bin/cli.ts +1244 -748
- package/bin/generate-tokens.ts +262 -262
- package/bin/language-config.ts +5 -8
- package/components/assets/xertica-orbe-animation.ts +1162 -1162
- package/components/assistant/code-block/CodeBlock.tsx +268 -268
- package/components/assistant/code-block/code-block.stories.tsx +57 -57
- package/components/assistant/code-block/code-block.test.tsx +44 -44
- package/components/assistant/code-block/index.ts +1 -1
- package/components/assistant/formatted-document/FormattedDocument.tsx +147 -147
- package/components/assistant/formatted-document/formatted-document.stories.tsx +51 -51
- package/components/assistant/formatted-document/formatted-document.test.tsx +42 -42
- package/components/assistant/formatted-document/index.ts +1 -1
- package/components/assistant/index.ts +6 -6
- package/components/assistant/markdown-message/MarkdownMessage.tsx +152 -152
- package/components/assistant/markdown-message/index.ts +1 -1
- package/components/assistant/markdown-message/markdown-message.stories.tsx +50 -50
- package/components/assistant/markdown-message/markdown-message.test.tsx +33 -33
- package/components/assistant/modern-chat-input/ModernChatInput.tsx +17 -7
- package/components/assistant/modern-chat-input/index.ts +1 -1
- package/components/assistant/modern-chat-input/modern-chat-input.stories.tsx +131 -131
- package/components/assistant/modern-chat-input/modern-chat-input.test.tsx +79 -79
- package/components/assistant/xertica-assistant/index.ts +3 -3
- package/components/assistant/xertica-assistant/parts/AssistantCollapsedView.tsx +99 -99
- package/components/assistant/xertica-assistant/parts/AssistantConversationList.tsx +104 -106
- package/components/assistant/xertica-assistant/parts/AssistantDocumentEditor.tsx +81 -81
- package/components/assistant/xertica-assistant/parts/AssistantFeedbackDialog.tsx +88 -78
- package/components/assistant/xertica-assistant/parts/AssistantHeader.tsx +75 -75
- package/components/assistant/xertica-assistant/parts/AssistantMessageBubble.tsx +564 -560
- package/components/assistant/xertica-assistant/parts/AssistantTabBar.tsx +67 -67
- package/components/assistant/xertica-assistant/parts/AssistantTypingIndicator.tsx +41 -41
- package/components/assistant/xertica-assistant/parts/AssistantWelcomeScreen.tsx +103 -103
- package/components/assistant/xertica-assistant/parts/index.ts +16 -16
- package/components/assistant/xertica-assistant/types.ts +134 -134
- package/components/assistant/xertica-assistant/use-assistant.ts +615 -615
- package/components/assistant/xertica-assistant/xertica-assistant.stories.tsx +407 -407
- package/components/assistant/xertica-assistant/xertica-assistant.test.tsx +65 -65
- package/components/assistant/xertica-assistant/xertica-assistant.tsx +611 -613
- package/components/blocks/card-patterns/ActivityCard.tsx +100 -100
- package/components/blocks/card-patterns/FeatureCard.tsx +109 -109
- package/components/blocks/card-patterns/FeatureCardSkeleton.tsx +1 -6
- package/components/blocks/card-patterns/NotificationCard.tsx +140 -140
- package/components/blocks/card-patterns/ProfileCard.tsx +112 -114
- package/components/blocks/card-patterns/ProjectCard.tsx +123 -123
- package/components/blocks/card-patterns/ProjectCardSkeleton.tsx +1 -6
- package/components/blocks/card-patterns/QuickActionCard.tsx +68 -68
- package/components/blocks/card-patterns/card-patterns.mdx +123 -123
- package/components/blocks/card-patterns/card-patterns.stories.tsx +594 -594
- package/components/blocks/card-patterns/index.ts +29 -29
- package/components/blocks/index.ts +1 -1
- package/components/brand/branding/branding.stories.tsx +57 -57
- package/components/brand/index.ts +6 -6
- package/components/brand/language-selector/index.ts +1 -1
- package/components/brand/language-selector/language-selector.mdx +126 -126
- package/components/brand/language-selector/language-selector.stories.tsx +1 -4
- package/components/brand/theme-toggle/ThemeToggle.tsx +74 -70
- package/components/brand/theme-toggle/index.ts +1 -1
- package/components/brand/theme-toggle/theme-toggle.stories.tsx +34 -34
- package/components/brand/theme-toggle/theme-toggle.test.tsx +34 -34
- package/components/brand/xertica-logo/XerticaLogo.stories.tsx +82 -82
- package/components/brand/xertica-logo/XerticaLogo.tsx +104 -104
- package/components/brand/xertica-logo/index.ts +1 -1
- package/components/brand/xertica-logo/xertica-logo.test.tsx +26 -26
- package/components/brand/xertica-orbe/XerticaOrbe.tsx +1927 -1927
- package/components/brand/xertica-orbe/index.ts +1 -1
- package/components/brand/xertica-orbe/xertica-orbe.stories.tsx +40 -40
- package/components/brand/xertica-orbe/xertica-orbe.test.tsx +19 -19
- package/components/brand/xertica-provider/XerticaProvider.tsx +1 -4
- package/components/brand/xertica-provider/index.ts +1 -1
- package/components/brand/xertica-provider/xertica-provider.test.tsx +74 -74
- package/components/brand/xertica-xlogo/XerticaXLogo.stories.tsx +79 -79
- package/components/brand/xertica-xlogo/XerticaXLogo.tsx +65 -65
- package/components/brand/xertica-xlogo/index.ts +1 -1
- package/components/brand/xertica-xlogo/xertica-xlogo.test.tsx +16 -16
- package/components/examples/ApiKeyMapExample.tsx +71 -71
- package/components/examples/DrawingMapExample.tsx +565 -565
- package/components/examples/FilterableMapExample.tsx +393 -393
- package/components/examples/LocationPickerExample.tsx +348 -348
- package/components/examples/MapExamples.tsx +268 -268
- package/components/examples/MapGmpExample.tsx +169 -169
- package/components/examples/MapShowcase.tsx +471 -471
- package/components/examples/RouteMapExamples.tsx +329 -329
- package/components/examples/SidebarLogoExample.tsx +65 -65
- package/components/examples/SimpleFilterableMap.tsx +219 -219
- package/components/examples/index.ts +45 -45
- package/components/figma/ImageWithFallback.tsx +27 -27
- package/components/hooks/index.ts +13 -13
- package/components/hooks/use-layout-shortcuts.ts +43 -43
- package/components/index.ts +86 -90
- package/components/layout/header/header.stories.tsx +204 -204
- package/components/layout/header/header.test.tsx +75 -75
- package/components/layout/header/header.tsx +349 -349
- package/components/layout/header/index.ts +1 -1
- package/components/layout/index.ts +2 -2
- package/components/layout/sidebar/index.ts +3 -3
- package/components/layout/sidebar/sidebar.stories.tsx +586 -586
- package/components/layout/sidebar/sidebar.test.tsx +76 -76
- package/components/layout/sidebar/sidebar.tsx +1079 -1073
- package/components/layout/sidebar/use-sidebar.ts +104 -104
- package/components/media/FloatingMediaWrapper.tsx +371 -371
- package/components/media/audio-player/AudioPlayer.stories.tsx +124 -124
- package/components/media/audio-player/AudioPlayer.test.tsx +106 -106
- package/components/media/audio-player/AudioPlayer.tsx +767 -765
- package/components/media/audio-player/index.ts +1 -1
- package/components/media/audio-player/use-audio-player.ts +312 -312
- package/components/media/index.ts +3 -3
- package/components/media/video-player/VideoPlayer.stories.tsx +98 -98
- package/components/media/video-player/VideoPlayer.test.tsx +73 -73
- package/components/media/video-player/VideoPlayer.tsx +310 -310
- package/components/media/video-player/index.ts +1 -1
- package/components/pages/forgot-password-page/ForgotPasswordPage.stories.tsx +24 -24
- package/components/pages/forgot-password-page/ForgotPasswordPage.tsx +188 -188
- package/components/pages/forgot-password-page/forgot-password-page.test.tsx +45 -45
- package/components/pages/forgot-password-page/index.ts +1 -1
- package/components/pages/home-content/HomeContent.stories.tsx +43 -43
- package/components/pages/home-content/HomeContent.tsx +120 -120
- package/components/pages/home-content/index.ts +1 -1
- package/components/pages/home-page/HomePage.stories.tsx +39 -39
- package/components/pages/home-page/HomePage.tsx +78 -74
- package/components/pages/home-page/home-page.test.tsx +53 -53
- package/components/pages/home-page/index.ts +1 -1
- package/components/pages/index.ts +8 -8
- package/components/pages/login-page/LoginPage.stories.tsx +39 -39
- package/components/pages/login-page/LoginPage.tsx +218 -216
- package/components/pages/login-page/index.ts +1 -1
- package/components/pages/login-page/login-page.test.tsx +63 -63
- package/components/pages/reset-password-page/ResetPasswordPage.stories.tsx +24 -24
- package/components/pages/reset-password-page/ResetPasswordPage.tsx +243 -239
- package/components/pages/reset-password-page/index.ts +1 -1
- package/components/pages/template-content/TemplateContent.stories.tsx +43 -43
- package/components/pages/template-content/TemplateContent.tsx +1354 -1235
- package/components/pages/template-content/index.ts +1 -1
- package/components/pages/template-page/TemplatePage.stories.tsx +39 -39
- package/components/pages/template-page/TemplatePage.tsx +62 -62
- package/components/pages/template-page/index.ts +1 -1
- package/components/pages/template-page/template-page.test.tsx +52 -52
- package/components/pages/verify-email-page/VerifyEmailPage.stories.tsx +41 -41
- package/components/pages/verify-email-page/VerifyEmailPage.tsx +206 -206
- package/components/pages/verify-email-page/index.ts +1 -1
- package/components/public-api-smoke.test.tsx +52 -52
- package/components/shared/CustomTooltipContent.tsx +48 -48
- package/components/shared/assistant-utils.test.ts +16 -16
- package/components/shared/assistant-utils.ts +225 -225
- package/components/shared/error-boundary.stories.tsx +114 -132
- package/components/shared/error-boundary.tsx +150 -154
- package/components/shared/error-fallbacks.tsx +222 -226
- package/components/shared/layout-constants.ts +8 -8
- package/components/shared/navigation.ts +35 -35
- package/components/shared/use-mobile.test.ts +16 -16
- package/components/shared/use-mobile.ts +36 -36
- package/components/shared/utils.test.ts +14 -14
- package/components/shared/utils.ts +6 -6
- package/components/ui/accordion/accordion.stories.tsx +105 -105
- package/components/ui/accordion/accordion.test.tsx +59 -59
- package/components/ui/accordion/accordion.tsx +77 -77
- package/components/ui/accordion/index.ts +1 -1
- package/components/ui/alert/alert.stories.tsx +86 -86
- package/components/ui/alert/alert.test.tsx +53 -53
- package/components/ui/alert/alert.tsx +93 -93
- package/components/ui/alert/index.ts +1 -1
- package/components/ui/alert-dialog/alert-dialog.stories.tsx +84 -84
- package/components/ui/alert-dialog/alert-dialog.test.tsx +70 -70
- package/components/ui/alert-dialog/alert-dialog.tsx +149 -149
- package/components/ui/alert-dialog/index.ts +1 -1
- package/components/ui/aspect-ratio/aspect-ratio.stories.tsx +46 -46
- package/components/ui/aspect-ratio/aspect-ratio.test.tsx +28 -28
- package/components/ui/aspect-ratio/aspect-ratio.tsx +20 -20
- package/components/ui/aspect-ratio/index.ts +1 -1
- package/components/ui/assistant-chart/AssistantChart.tsx +64 -64
- package/components/ui/assistant-chart/assistant-chart.stories.tsx +44 -44
- package/components/ui/assistant-chart/assistant-chart.test.tsx +46 -46
- package/components/ui/assistant-chart/index.ts +1 -1
- package/components/ui/avatar/avatar.stories.tsx +86 -86
- package/components/ui/avatar/avatar.test.tsx +55 -55
- package/components/ui/avatar/avatar.tsx +71 -71
- package/components/ui/avatar/index.ts +1 -1
- package/components/ui/badge/badge.stories.tsx +72 -72
- package/components/ui/badge/badge.test.tsx +40 -40
- package/components/ui/badge/badge.tsx +58 -58
- package/components/ui/badge/index.ts +1 -1
- package/components/ui/breadcrumb/breadcrumb.stories.tsx +123 -123
- package/components/ui/breadcrumb/breadcrumb.test.tsx +70 -70
- package/components/ui/breadcrumb/breadcrumb.tsx +114 -114
- package/components/ui/breadcrumb/index.ts +1 -1
- package/components/ui/button/button.stories.tsx +183 -183
- package/components/ui/button/button.test.tsx +64 -64
- package/components/ui/button/button.tsx +98 -98
- package/components/ui/button/index.ts +1 -1
- package/components/ui/calendar/calendar.stories.tsx +108 -108
- package/components/ui/calendar/calendar.test.tsx +53 -53
- package/components/ui/calendar/calendar.tsx +230 -230
- package/components/ui/calendar/index.ts +1 -1
- package/components/ui/card/card.stories.tsx +301 -301
- package/components/ui/card/card.test.tsx +55 -55
- package/components/ui/card/card.tsx +83 -83
- package/components/ui/card/index.ts +1 -1
- package/components/ui/carousel/carousel.stories.tsx +80 -80
- package/components/ui/carousel/carousel.test.tsx +75 -75
- package/components/ui/carousel/carousel.tsx +242 -242
- package/components/ui/carousel/index.ts +1 -1
- package/components/ui/chart/chart.stories.tsx +1328 -1328
- package/components/ui/chart/chart.test.tsx +178 -178
- package/components/ui/chart/chart.tsx +2232 -2232
- package/components/ui/chart/index.ts +1 -1
- package/components/ui/checkbox/checkbox.stories.tsx +109 -109
- package/components/ui/checkbox/checkbox.test.tsx +49 -49
- package/components/ui/checkbox/checkbox.tsx +68 -68
- package/components/ui/checkbox/index.ts +1 -1
- package/components/ui/collapsible/collapsible.stories.tsx +45 -45
- package/components/ui/collapsible/collapsible.test.tsx +51 -51
- package/components/ui/collapsible/collapsible.tsx +32 -32
- package/components/ui/collapsible/index.ts +1 -1
- package/components/ui/command/command.stories.tsx +134 -134
- package/components/ui/command/command.test.tsx +48 -48
- package/components/ui/command/command.tsx +163 -163
- package/components/ui/command/index.ts +1 -1
- package/components/ui/context-menu/context-menu.stories.tsx +76 -76
- package/components/ui/context-menu/context-menu.test.tsx +61 -61
- package/components/ui/context-menu/context-menu.tsx +236 -236
- package/components/ui/context-menu/index.ts +1 -1
- package/components/ui/dialog/dialog.stories.tsx +174 -174
- package/components/ui/dialog/dialog.test.tsx +78 -78
- package/components/ui/dialog/dialog.tsx +189 -189
- package/components/ui/dialog/index.ts +1 -1
- package/components/ui/drawer/drawer.stories.tsx +71 -71
- package/components/ui/drawer/drawer.test.tsx +67 -67
- package/components/ui/drawer/drawer.tsx +146 -146
- package/components/ui/drawer/index.ts +1 -1
- package/components/ui/dropdown-menu/dropdown-menu.stories.tsx +156 -156
- package/components/ui/dropdown-menu/dropdown-menu.test.tsx +62 -62
- package/components/ui/dropdown-menu/dropdown-menu.tsx +240 -240
- package/components/ui/dropdown-menu/index.ts +1 -1
- package/components/ui/empty/empty.stories.tsx +85 -85
- package/components/ui/empty/empty.test.tsx +31 -31
- package/components/ui/empty/empty.tsx +88 -88
- package/components/ui/empty/index.ts +1 -1
- package/components/ui/file-upload/file-upload.stories.tsx +144 -144
- package/components/ui/file-upload/file-upload.test.tsx +65 -65
- package/components/ui/file-upload/file-upload.tsx +142 -142
- package/components/ui/file-upload/index.ts +2 -2
- package/components/ui/file-upload/use-file-upload.ts +177 -177
- package/components/ui/form/form.stories.tsx +85 -85
- package/components/ui/form/form.test.tsx +75 -75
- package/components/ui/form/form.tsx +163 -163
- package/components/ui/form/index.ts +1 -1
- package/components/ui/google-maps-loader/google-maps-loader.test.tsx +35 -35
- package/components/ui/google-maps-loader/google-maps-loader.tsx +465 -465
- package/components/ui/google-maps-loader/index.ts +1 -1
- package/components/ui/hover-card/hover-card.stories.tsx +61 -61
- package/components/ui/hover-card/hover-card.test.tsx +48 -48
- package/components/ui/hover-card/hover-card.tsx +50 -50
- package/components/ui/hover-card/index.ts +1 -1
- package/components/ui/index.ts +400 -400
- package/components/ui/input/index.ts +1 -1
- package/components/ui/input/input.stories.tsx +153 -153
- package/components/ui/input/input.test.tsx +47 -47
- package/components/ui/input/input.tsx +57 -57
- package/components/ui/input-otp/index.ts +1 -1
- package/components/ui/input-otp/input-otp.stories.tsx +120 -120
- package/components/ui/input-otp/input-otp.test.tsx +74 -74
- package/components/ui/input-otp/input-otp.tsx +101 -101
- package/components/ui/label/index.ts +1 -1
- package/components/ui/label/label.stories.tsx +74 -74
- package/components/ui/label/label.test.tsx +45 -45
- package/components/ui/label/label.tsx +53 -53
- package/components/ui/map/index.ts +1 -1
- package/components/ui/map/map.stories.tsx +86 -86
- package/components/ui/map/map.test.tsx +82 -82
- package/components/ui/map/map.tsx +506 -506
- package/components/ui/map/mock.test.tsx +13 -13
- package/components/ui/map-config/index.ts +1 -1
- package/components/ui/map-config/map-config.ts +18 -18
- package/components/ui/map-layers/index.ts +1 -1
- package/components/ui/map-layers/map-layers.test.tsx +48 -48
- package/components/ui/map-layers/map-layers.tsx +126 -126
- package/components/ui/map.exports/index.ts +1 -1
- package/components/ui/map.exports/map.exports.ts +31 -31
- package/components/ui/menubar/index.ts +1 -1
- package/components/ui/menubar/menubar.stories.tsx +130 -130
- package/components/ui/menubar/menubar.test.tsx +53 -53
- package/components/ui/menubar/menubar.tsx +265 -265
- package/components/ui/navigation-menu/index.ts +1 -1
- package/components/ui/navigation-menu/navigation-menu.stories.tsx +126 -126
- package/components/ui/navigation-menu/navigation-menu.test.tsx +47 -47
- package/components/ui/navigation-menu/navigation-menu.tsx +165 -165
- package/components/ui/notification-badge/index.ts +1 -1
- package/components/ui/notification-badge/notification-badge.stories.tsx +66 -66
- package/components/ui/notification-badge/notification-badge.test.tsx +61 -61
- package/components/ui/notification-badge/notification-badge.tsx +91 -91
- package/components/ui/page-header/index.ts +1 -1
- package/components/ui/page-header/page-header.stories.tsx +69 -69
- package/components/ui/page-header/page-header.test.tsx +37 -37
- package/components/ui/page-header/page-header.tsx +124 -124
- package/components/ui/pagination/index.ts +3 -3
- package/components/ui/pagination/pagination.stories.tsx +210 -210
- package/components/ui/pagination/pagination.test.tsx +63 -63
- package/components/ui/pagination/pagination.tsx +140 -140
- package/components/ui/pagination/use-pagination.ts +173 -173
- package/components/ui/popover/index.ts +1 -1
- package/components/ui/popover/popover.stories.tsx +73 -73
- package/components/ui/popover/popover.test.tsx +48 -48
- package/components/ui/popover/popover.tsx +54 -54
- package/components/ui/progress/index.ts +1 -1
- package/components/ui/progress/progress.stories.tsx +55 -55
- package/components/ui/progress/progress.test.tsx +23 -23
- package/components/ui/progress/progress.tsx +68 -68
- package/components/ui/radio-group/index.ts +1 -1
- package/components/ui/radio-group/radio-group.stories.tsx +114 -114
- package/components/ui/radio-group/radio-group.test.tsx +78 -78
- package/components/ui/radio-group/radio-group.tsx +93 -93
- package/components/ui/rating/index.ts +1 -1
- package/components/ui/rating/rating.stories.tsx +50 -50
- package/components/ui/rating/rating.test.tsx +48 -48
- package/components/ui/rating/rating.tsx +145 -145
- package/components/ui/resizable/index.ts +1 -1
- package/components/ui/resizable/resizable.stories.tsx +88 -88
- package/components/ui/resizable/resizable.test.tsx +61 -61
- package/components/ui/resizable/resizable.tsx +452 -452
- package/components/ui/rich-text-editor/index.ts +7 -7
- package/components/ui/rich-text-editor/rich-text-editor.stories.tsx +290 -290
- package/components/ui/rich-text-editor/rich-text-editor.test.tsx +86 -86
- package/components/ui/rich-text-editor/rich-text-editor.tsx +634 -634
- package/components/ui/rich-text-editor/use-rich-text-editor.ts +453 -453
- package/components/ui/route-map/index.ts +1 -1
- package/components/ui/route-map/route-map.stories.tsx +48 -48
- package/components/ui/route-map/route-map.test.tsx +108 -108
- package/components/ui/route-map/route-map.tsx +349 -349
- package/components/ui/scroll-area/index.ts +1 -1
- package/components/ui/scroll-area/scroll-area.stories.tsx +31 -31
- package/components/ui/scroll-area/scroll-area.test.tsx +27 -27
- package/components/ui/scroll-area/scroll-area.tsx +70 -70
- package/components/ui/search/index.ts +1 -1
- package/components/ui/search/search.stories.tsx +107 -107
- package/components/ui/search/search.test.tsx +67 -67
- package/components/ui/search/search.tsx +141 -141
- package/components/ui/select/index.ts +1 -1
- package/components/ui/select/select.stories.tsx +163 -163
- package/components/ui/select/select.test.tsx +99 -99
- package/components/ui/select/select.tsx +195 -195
- package/components/ui/separator/index.ts +1 -1
- package/components/ui/separator/separator.stories.tsx +55 -55
- package/components/ui/separator/separator.test.tsx +23 -23
- package/components/ui/separator/separator.tsx +39 -39
- package/components/ui/sheet/index.ts +1 -1
- package/components/ui/sheet/sheet.stories.tsx +93 -93
- package/components/ui/sheet/sheet.test.tsx +62 -62
- package/components/ui/sheet/sheet.tsx +149 -149
- package/components/ui/simple-map/index.ts +1 -1
- package/components/ui/simple-map/simple-map.stories.tsx +44 -44
- package/components/ui/simple-map/simple-map.test.tsx +36 -36
- package/components/ui/simple-map/simple-map.tsx +92 -92
- package/components/ui/skeleton/index.ts +1 -1
- package/components/ui/skeleton/skeleton.stories.tsx +36 -36
- package/components/ui/skeleton/skeleton.test.tsx +19 -19
- package/components/ui/skeleton/skeleton.tsx +25 -25
- package/components/ui/slider/index.ts +1 -1
- package/components/ui/slider/slider.stories.tsx +44 -44
- package/components/ui/slider/slider.test.tsx +25 -25
- package/components/ui/slider/slider.tsx +66 -66
- package/components/ui/sonner/index.ts +1 -1
- package/components/ui/sonner/sonner.stories.tsx +41 -41
- package/components/ui/sonner/sonner.test.tsx +24 -24
- package/components/ui/sonner/sonner.tsx +74 -74
- package/components/ui/stats-card/index.ts +2 -2
- package/components/ui/stats-card/stats-card-skeleton.tsx +1 -3
- package/components/ui/stats-card/stats-card.stories.tsx +99 -99
- package/components/ui/stats-card/stats-card.test.tsx +34 -34
- package/components/ui/stats-card/stats-card.tsx +93 -93
- package/components/ui/stepper/index.ts +3 -3
- package/components/ui/stepper/stepper.stories.tsx +171 -171
- package/components/ui/stepper/stepper.test.tsx +47 -47
- package/components/ui/stepper/stepper.tsx +190 -190
- package/components/ui/stepper/use-stepper.ts +139 -139
- package/components/ui/switch/index.ts +1 -1
- package/components/ui/switch/switch.stories.tsx +93 -93
- package/components/ui/switch/switch.test.tsx +44 -44
- package/components/ui/switch/switch.tsx +70 -70
- package/components/ui/table/index.ts +1 -1
- package/components/ui/table/table.stories.tsx +114 -114
- package/components/ui/table/table.test.tsx +43 -43
- package/components/ui/table/table.tsx +104 -104
- package/components/ui/tabs/index.ts +1 -1
- package/components/ui/tabs/tabs.stories.tsx +140 -140
- package/components/ui/tabs/tabs.test.tsx +50 -50
- package/components/ui/tabs/tabs.tsx +66 -66
- package/components/ui/textarea/index.ts +1 -1
- package/components/ui/textarea/textarea.stories.tsx +69 -69
- package/components/ui/textarea/textarea.test.tsx +41 -41
- package/components/ui/textarea/textarea.tsx +61 -61
- package/components/ui/timeline/index.ts +1 -1
- package/components/ui/timeline/timeline.stories.tsx +97 -97
- package/components/ui/timeline/timeline.test.tsx +53 -53
- package/components/ui/timeline/timeline.tsx +124 -124
- package/components/ui/toggle/index.ts +1 -1
- package/components/ui/toggle/toggle.stories.tsx +56 -56
- package/components/ui/toggle/toggle.test.tsx +32 -32
- package/components/ui/toggle/toggle.tsx +55 -55
- package/components/ui/toggle-group/index.ts +1 -1
- package/components/ui/toggle-group/toggle-group.stories.tsx +66 -66
- package/components/ui/toggle-group/toggle-group.test.tsx +47 -47
- package/components/ui/toggle-group/toggle-group.tsx +79 -79
- package/components/ui/tooltip/index.ts +1 -1
- package/components/ui/tooltip/tooltip.stories.tsx +83 -83
- package/components/ui/tooltip/tooltip.test.tsx +39 -39
- package/components/ui/tooltip/tooltip.tsx +69 -69
- package/components/ui/tree-view/index.ts +4 -4
- package/components/ui/tree-view/tree-view.stories.tsx +154 -154
- package/components/ui/tree-view/tree-view.test.tsx +58 -58
- package/components/ui/tree-view/tree-view.tsx +171 -171
- package/components/ui/tree-view/use-tree-view.ts +237 -237
- package/components.json +892 -892
- package/contexts/ApiKeyContext.test.tsx +26 -26
- package/contexts/ApiKeyContext.tsx +196 -196
- package/contexts/AssistenteContext.test.tsx +17 -17
- package/contexts/AssistenteContext.tsx +113 -113
- package/contexts/AuthContext.tsx +121 -118
- package/contexts/BrandColorsContext.test.tsx +21 -21
- package/contexts/BrandColorsContext.tsx +251 -251
- package/contexts/LanguageContext.tsx +1 -2
- package/contexts/LayoutContext.test.tsx +29 -29
- package/contexts/LayoutContext.tsx +140 -140
- package/contexts/ThemeContext.test.tsx +38 -38
- package/contexts/ThemeContext.tsx +111 -111
- package/contexts/index.ts +8 -8
- package/contexts/theme-data.ts +340 -340
- package/dist/AssistantChart-COGiOV-g.cjs +3541 -0
- package/dist/AssistantChart-CWX1OWNM.js +3373 -0
- package/dist/AudioPlayer-9psiEucT.cjs +1282 -0
- package/dist/AudioPlayer-Dp2bD1Gk.js +1278 -0
- package/dist/BrandColorsContext-DZT7JjeD.js +659 -0
- package/dist/BrandColorsContext-awnBCmC4.cjs +666 -0
- package/dist/CodeBlock-DYkTfR0f.js +221 -0
- package/dist/CodeBlock-EOvp9cVu.cjs +223 -0
- package/dist/CustomTooltipContent-BhdIeBEg.cjs +54 -0
- package/dist/CustomTooltipContent-CNbVB2NS.js +33 -0
- package/dist/FeatureCard-BZ4CYxFf.cjs +497 -0
- package/dist/FeatureCard-DNycVGwT.js +485 -0
- package/dist/FeatureCardSkeleton-DZqc96mt.js +27 -0
- package/dist/FeatureCardSkeleton-pTa0YNKP.cjs +29 -0
- package/dist/LayoutContext-BEq_-n98.cjs +96 -0
- package/dist/LayoutContext-DNl1xSoX.js +92 -0
- package/dist/ThemeContext-CMD3z2Dz.cjs +1930 -0
- package/dist/ThemeContext-x_F2zsnv.js +1923 -0
- package/dist/VerifyEmailPage-BJjAMUTW.js +3223 -0
- package/dist/VerifyEmailPage-Bv8Ah_TK.cjs +3235 -0
- package/dist/VerifyEmailPage-CkBYfsNy.cjs +3232 -0
- package/dist/VerifyEmailPage-Cyl55sJb.js +3226 -0
- package/dist/VerifyEmailPage-X14vhdyl.js +3296 -0
- package/dist/VerifyEmailPage-u_Dn7t1U.cjs +3305 -0
- package/dist/XerticaOrbe-Uk2JML1-.cjs +1927 -0
- package/dist/XerticaOrbe-jA5T2iOk.js +1925 -0
- package/dist/XerticaProvider-BErr83Bg.js +42 -0
- package/dist/XerticaProvider-CwOkHxiT.cjs +44 -0
- package/dist/XerticaProvider-DUOJg9iX.js +49 -0
- package/dist/XerticaProvider-Dl_b72_l.cjs +51 -0
- package/dist/XerticaXLogo-BX3ueACh.js +255 -0
- package/dist/XerticaXLogo-mqjoBiLI.js +252 -0
- package/dist/XerticaXLogo-qBPhwK3g.cjs +260 -0
- package/dist/XerticaXLogo-uQgwns_E.cjs +257 -0
- package/dist/alert-dialog-DhwPioBa.cjs +885 -0
- package/dist/alert-dialog-DqlRW_An.js +831 -0
- package/dist/assistant.cjs.js +8 -4
- package/dist/assistant.es.js +5 -11
- package/dist/avatar-3kO2Anrp.js +54 -0
- package/dist/avatar-BCM7YQRC.cjs +77 -0
- package/dist/blocks.cjs.js +9 -4
- package/dist/blocks.es.js +2 -16
- package/dist/brand.cjs.js +10 -5
- package/dist/brand.es.js +3 -11
- package/dist/breadcrumb-BKtHF4gk.cjs +98 -0
- package/dist/breadcrumb-ifNsA7Zl.js +90 -0
- package/dist/button-0BlA47It.cjs +85 -0
- package/dist/button-DZHzN1Gd.js +62 -0
- package/dist/cli.js +471 -93
- package/dist/components/brand/theme-toggle/ThemeToggle.d.ts +1 -1
- package/dist/components/index.d.ts +1 -1
- package/dist/dropdown-menu-BMcykFDf.cjs +225 -0
- package/dist/dropdown-menu-Dn_eV2Xb.js +190 -0
- package/dist/google-maps-loader-BCe58h9D.js +308 -0
- package/dist/google-maps-loader-casMyxlo.cjs +316 -0
- package/dist/hooks.cjs.js +12 -8
- package/dist/hooks.es.js +10 -27
- package/dist/index-9GWd0qxq.cjs +12 -0
- package/dist/index-BabBx2pa.js +6 -0
- package/dist/index.cjs.js +37 -32
- package/dist/index.es.js +30 -363
- package/dist/input-C_UiS2Py.cjs +152 -0
- package/dist/input-cc-PTD4R.js +123 -0
- package/dist/layout.cjs.js +10 -6
- package/dist/layout.es.js +7 -9
- package/dist/media.cjs.js +8 -3
- package/dist/media.es.js +1 -6
- package/dist/pages.cjs.js +8 -3
- package/dist/pages.es.js +1 -11
- package/dist/progress-C7Lti5wo.js +80 -0
- package/dist/progress-Cqwxbqs1.cjs +103 -0
- package/dist/rich-text-editor-DqLICivI.js +2832 -0
- package/dist/rich-text-editor-DxO1Hz3a.cjs +2903 -0
- package/dist/select-CH6v_KcQ.cjs +161 -0
- package/dist/select-D-xvCZK2.js +130 -0
- package/dist/sidebar-3XyzjVBw.js +792 -0
- package/dist/sidebar-B4ZWaMrE.js +792 -0
- package/dist/sidebar-BS1p2V7t.cjs +795 -0
- package/dist/sidebar-DyYvgyBj.cjs +795 -0
- package/dist/skeleton-DjiHerJn.cjs +87 -0
- package/dist/skeleton-DtR5tkYe.js +78 -0
- package/dist/slider-B00b9SVK.cjs +78 -0
- package/dist/slider-DQCNUUMj.js +56 -0
- package/dist/sonner-B-jWlik1.cjs +68 -0
- package/dist/sonner-C9tiqj4f.js +47 -0
- package/dist/tooltip-D8n9UYoU.cjs +72 -0
- package/dist/tooltip-RtbSmPYJ.js +48 -0
- package/dist/ui.cjs.js +23 -18
- package/dist/ui.es.js +16 -303
- package/dist/use-audio-player-B78fd2ct.js +188 -0
- package/dist/use-audio-player-DGvhPrgR.cjs +190 -0
- package/dist/use-mobile-BdXTRb0Z.cjs +51 -0
- package/dist/use-mobile-Ce2cBAQe.js +29 -0
- package/dist/xertica-assistant-B1NaSFFj.js +2173 -0
- package/dist/xertica-assistant-B687qEPU.js +2165 -0
- package/dist/xertica-assistant-CIaUlbIt.cjs +2180 -0
- package/dist/xertica-assistant-sOHwTgIP.cjs +2172 -0
- package/dist/xertica-ui.css +1 -1
- package/docs/ai-usage.md +195 -195
- package/docs/architecture-improvements.md +456 -456
- package/docs/architecture.md +312 -306
- package/docs/components/accordion.md +109 -109
- package/docs/components/alert-dialog.md +127 -127
- package/docs/components/alert.md +106 -106
- package/docs/components/aspect-ratio.md +58 -58
- package/docs/components/assistant-chart.md +47 -47
- package/docs/components/assistant.md +428 -426
- package/docs/components/audio-player.md +167 -167
- package/docs/components/avatar.md +101 -101
- package/docs/components/badge.md +84 -84
- package/docs/components/branding.md +252 -252
- package/docs/components/breadcrumb.md +104 -104
- package/docs/components/button.md +156 -156
- package/docs/components/calendar.md +141 -141
- package/docs/components/card-patterns.md +447 -445
- package/docs/components/card.md +245 -245
- package/docs/components/carousel.md +100 -100
- package/docs/components/chart.md +638 -638
- package/docs/components/checkbox.md +88 -88
- package/docs/components/code-block.md +105 -105
- package/docs/components/collapsible.md +86 -86
- package/docs/components/command.md +113 -113
- package/docs/components/context-menu.md +81 -81
- package/docs/components/dialog.md +198 -198
- package/docs/components/drawer.md +105 -105
- package/docs/components/dropdown-menu.md +127 -127
- package/docs/components/empty.md +127 -127
- package/docs/components/error-boundary.md +201 -191
- package/docs/components/file-upload.md +189 -189
- package/docs/components/floating-media-wrapper.md +63 -63
- package/docs/components/form.md +177 -177
- package/docs/components/formatted-document.md +105 -105
- package/docs/components/google-maps-loader.md +44 -44
- package/docs/components/header.md +177 -177
- package/docs/components/hooks.md +432 -430
- package/docs/components/hover-card.md +86 -86
- package/docs/components/image-with-fallback.md +107 -107
- package/docs/components/input-otp.md +95 -95
- package/docs/components/input.md +130 -130
- package/docs/components/label.md +69 -69
- package/docs/components/language-selector.md +20 -16
- package/docs/components/map-layers.md +138 -138
- package/docs/components/map.md +84 -84
- package/docs/components/markdown-message.md +47 -47
- package/docs/components/menubar.md +89 -89
- package/docs/components/modern-chat-input.md +164 -164
- package/docs/components/navigation-menu.md +83 -83
- package/docs/components/notification-badge.md +78 -78
- package/docs/components/page-header.md +93 -93
- package/docs/components/pages.md +323 -309
- package/docs/components/pagination.md +334 -334
- package/docs/components/popover.md +116 -116
- package/docs/components/progress.md +103 -103
- package/docs/components/radio-group.md +133 -133
- package/docs/components/rating.md +77 -77
- package/docs/components/resizable.md +84 -84
- package/docs/components/rich-text-editor.md +255 -255
- package/docs/components/route-map.md +124 -124
- package/docs/components/scroll-area.md +58 -58
- package/docs/components/search.md +87 -87
- package/docs/components/select.md +144 -144
- package/docs/components/separator.md +58 -58
- package/docs/components/sheet.md +122 -122
- package/docs/components/sidebar.md +314 -314
- package/docs/components/simple-map.md +51 -51
- package/docs/components/skeleton.md +99 -99
- package/docs/components/slider.md +84 -84
- package/docs/components/sonner.md +115 -115
- package/docs/components/stats-card.md +120 -120
- package/docs/components/stepper.md +268 -268
- package/docs/components/switch.md +106 -106
- package/docs/components/table.md +138 -138
- package/docs/components/tabs.md +117 -117
- package/docs/components/textarea.md +86 -86
- package/docs/components/theme-toggle.md +73 -73
- package/docs/components/timeline.md +121 -121
- package/docs/components/toggle-group.md +68 -68
- package/docs/components/toggle.md +62 -62
- package/docs/components/tooltip.md +116 -116
- package/docs/components/tree-view.md +238 -238
- package/docs/components/use-mobile.md +96 -96
- package/docs/components/video-player.md +68 -68
- package/docs/components/xertica-logo.md +36 -36
- package/docs/components/xertica-orbe.md +35 -35
- package/docs/components/xertica-provider.md +65 -65
- package/docs/components/xertica-xlogo.md +35 -35
- package/docs/decision-tree.md +293 -293
- package/docs/doc-audit.md +244 -243
- package/docs/form-sizing.md +162 -162
- package/docs/getting-started.md +616 -591
- package/docs/guidelines.md +330 -328
- package/docs/i18n.md +61 -57
- package/docs/installation.md +268 -267
- package/docs/layout.md +143 -143
- package/docs/llms.md +295 -295
- package/docs/patterns/analytics.md +194 -194
- package/docs/patterns/crud.md +149 -149
- package/docs/patterns/dashboard.md +138 -138
- package/docs/patterns/detail-page.md +296 -296
- package/docs/patterns/form.md +241 -241
- package/docs/patterns/login.md +156 -156
- package/docs/patterns/settings.md +368 -368
- package/docs/patterns/wizard.md +213 -213
- package/docs/state-management.md +289 -289
- package/guidelines/Guidelines.md +409 -406
- package/hooks/useTheme.test.tsx +16 -16
- package/hooks/useTheme.ts +4 -4
- package/imports/Podcast.tsx +540 -540
- package/imports/XerticaAi.tsx +46 -46
- package/imports/XerticaX.tsx +15 -15
- package/imports/svg-aueiaqngck.ts +20 -20
- package/imports/svg-v9krss1ozd.ts +23 -23
- package/imports/svg-vhrdofe3qe.ts +6 -6
- package/llms-compact.txt +2 -1
- package/llms.txt +2 -1
- package/mcp/resources.json +22 -22
- package/mcp/tools.json +35 -35
- package/package.json +219 -213
- package/scripts/ai-validator.ts +91 -91
- package/scripts/cleanup-case-dupes.ts +62 -62
- package/scripts/generate-ai-manifests.ts +107 -107
- package/styles/globals.css +13 -13
- package/styles/xertica/app-overrides/chat.css +61 -61
- package/styles/xertica/app-overrides/scrollbar.css +33 -33
- package/styles/xertica/base.css +90 -71
- package/styles/xertica/integrations/google-maps.css +76 -76
- package/styles/xertica/integrations/sonner.css +73 -73
- package/styles/xertica/theme-map.css +102 -99
- package/styles/xertica/tokens.css +240 -236
- package/templates/CLAUDE.md +16 -1
- package/templates/eslint.config.js +26 -26
- package/templates/guidelines/Guidelines.md +577 -553
- package/templates/package.json +69 -69
- package/templates/postcss.config.js +6 -6
- package/templates/src/app/App.tsx +46 -46
- package/templates/src/app/components/AppLayout.tsx +55 -55
- package/templates/src/app/components/AuthGuard.tsx +131 -82
- package/templates/src/app/context/AuthContext.tsx +108 -108
- package/templates/src/features/assistant/index.ts +5 -5
- package/templates/src/features/auth/index.ts +4 -4
- package/templates/src/features/auth/ui/AuthPageShell.tsx +32 -32
- package/templates/src/features/auth/ui/ForgotPasswordContent.tsx +70 -72
- package/templates/src/features/auth/ui/LoginContent.tsx +92 -92
- package/templates/src/features/auth/ui/ResetPasswordContent.tsx +6 -2
- package/templates/src/features/auth/ui/SocialLoginButtons.tsx +78 -78
- package/templates/src/features/auth/ui/VerifyEmailContent.tsx +2 -6
- package/templates/src/features/home/data/mock.ts +41 -35
- package/templates/src/features/home/index.ts +11 -11
- package/templates/src/features/home/store/dashboardStore.ts +25 -25
- package/templates/src/features/home/ui/HomeContent.tsx +117 -119
- package/templates/src/features/template/index.ts +5 -5
- package/templates/src/features/template/ui/CrudTemplate.tsx +1 -4
- package/templates/src/features/template/ui/LoginTemplate.tsx +1 -1
- package/templates/src/features/template/ui/TemplateContent.tsx +29 -21
- package/templates/src/locales/en/pages/templates.json +17 -17
- package/templates/src/locales/es/pages/templates.json +17 -17
- package/templates/src/locales/pt-BR/pages/templates.json +17 -17
- package/templates/src/main.tsx +11 -11
- package/templates/src/pages/AssistantPage.tsx +26 -20
- package/templates/src/pages/ForgotPasswordPage.tsx +6 -6
- package/templates/src/pages/HomePage.tsx +53 -49
- package/templates/src/pages/LoginPage.tsx +10 -10
- package/templates/src/pages/ResetPasswordPage.tsx +6 -6
- package/templates/src/pages/TemplatePage.tsx +28 -28
- package/templates/src/pages/VerifyEmailPage.tsx +6 -6
- package/templates/src/shared/config/navigation.ts +19 -19
- package/templates/src/shared/error-boundary.tsx +150 -154
- package/templates/src/shared/error-fallbacks.tsx +222 -226
- package/templates/src/shared/lib/auth.ts +20 -20
- package/templates/src/shared/types/auth.ts +3 -3
- package/templates/src/styles/index.css +95 -95
- package/templates/src/styles/xertica/tokens.css +240 -236
- package/templates/tsconfig.json +25 -25
- package/templates/tsconfig.node.json +12 -12
- package/templates/vite-env.d.ts +1 -1
- package/templates/vite.config.js +20 -20
- package/templates/vite.config.ts +54 -51
- package/utils/color-utils.ts +72 -72
- package/utils/demo-responses.test.ts +10 -10
- package/utils/demo-responses.ts +151 -151
- package/utils/gemini.test.ts +25 -25
- package/utils/gemini.ts +155 -155
|
@@ -1,506 +1,506 @@
|
|
|
1
|
-
import React, { useEffect, useRef, useState } from 'react';
|
|
2
|
-
import { createRoot } from 'react-dom/client';
|
|
3
|
-
import { cn } from '../../shared/utils';
|
|
4
|
-
import { useGoogleMapsLoader } from '../google-maps-loader';
|
|
5
|
-
import { useMapLayers, MapLayersConfig } from '../map-layers';
|
|
6
|
-
|
|
7
|
-
declare global {
|
|
8
|
-
namespace JSX {
|
|
9
|
-
interface IntrinsicElements {
|
|
10
|
-
'gmp-map': React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement> & {
|
|
11
|
-
'map-id'?: string;
|
|
12
|
-
center?: string | google.maps.LatLng | google.maps.LatLngLiteral;
|
|
13
|
-
zoom?: number;
|
|
14
|
-
};
|
|
15
|
-
'gmp-advanced-marker': React.DetailedHTMLProps<
|
|
16
|
-
React.HTMLAttributes<HTMLElement>,
|
|
17
|
-
HTMLElement
|
|
18
|
-
> & {
|
|
19
|
-
title?: string;
|
|
20
|
-
position?: string | google.maps.LatLng | google.maps.LatLngLiteral;
|
|
21
|
-
};
|
|
22
|
-
}
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
export interface MapProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
26
|
-
center?: { lat: number; lng: number };
|
|
27
|
-
zoom?: number;
|
|
28
|
-
mapTypeId?: string;
|
|
29
|
-
mapId?: string;
|
|
30
|
-
markers?: Array<{
|
|
31
|
-
position: { lat: number; lng: number };
|
|
32
|
-
label?: string;
|
|
33
|
-
title?: string;
|
|
34
|
-
info?: string;
|
|
35
|
-
customColor?: string;
|
|
36
|
-
icon?: string;
|
|
37
|
-
iconSvg?: string;
|
|
38
|
-
iconColor?: string;
|
|
39
|
-
infoWindowContent?: string; // Custom HTML content for InfoWindow
|
|
40
|
-
richContent?: React.ReactNode; // Custom React component for InfoWindow
|
|
41
|
-
}>;
|
|
42
|
-
circle?: {
|
|
43
|
-
center: { lat: number; lng: number };
|
|
44
|
-
radius: number;
|
|
45
|
-
fillColor?: string;
|
|
46
|
-
strokeColor?: string;
|
|
47
|
-
};
|
|
48
|
-
polygon?: {
|
|
49
|
-
paths: Array<{ lat: number; lng: number }>[];
|
|
50
|
-
fillColor?: string;
|
|
51
|
-
strokeColor?: string;
|
|
52
|
-
};
|
|
53
|
-
layers?: MapLayersConfig;
|
|
54
|
-
height?: string;
|
|
55
|
-
apiKey?: string;
|
|
56
|
-
mapContainerClassName?: string;
|
|
57
|
-
disableDefaultUI?: boolean;
|
|
58
|
-
zoomControl?: boolean;
|
|
59
|
-
streetViewControl?: boolean;
|
|
60
|
-
mapTypeControl?: boolean;
|
|
61
|
-
fullscreenControl?: boolean;
|
|
62
|
-
gestureHandling?: 'cooperative' | 'greedy' | 'none' | 'auto';
|
|
63
|
-
onMapLoad?: (map: google.maps.Map) => void;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
const DEFAULT_CENTER = { lat: -23.5505, lng: -46.6333 };
|
|
67
|
-
const DEFAULT_ZOOM = 12;
|
|
68
|
-
|
|
69
|
-
const MapContent = React.forwardRef<HTMLDivElement, MapProps & { apiKey: string }>(
|
|
70
|
-
({ apiKey, ...props }, ref) => {
|
|
71
|
-
const { isLoaded, loadError, load } = useGoogleMapsLoader();
|
|
72
|
-
const {
|
|
73
|
-
center = DEFAULT_CENTER,
|
|
74
|
-
zoom = DEFAULT_ZOOM,
|
|
75
|
-
markers = [],
|
|
76
|
-
circle,
|
|
77
|
-
polygon,
|
|
78
|
-
layers,
|
|
79
|
-
height = '400px',
|
|
80
|
-
mapContainerClassName,
|
|
81
|
-
disableDefaultUI = false,
|
|
82
|
-
zoomControl = true,
|
|
83
|
-
streetViewControl = false,
|
|
84
|
-
mapTypeControl = false,
|
|
85
|
-
fullscreenControl = true,
|
|
86
|
-
gestureHandling = 'cooperative',
|
|
87
|
-
onMapLoad,
|
|
88
|
-
className,
|
|
89
|
-
...divProps
|
|
90
|
-
} = props;
|
|
91
|
-
|
|
92
|
-
const [selectedMarker, setSelectedMarker] = useState<number | null>(null);
|
|
93
|
-
const mapRef = useRef<google.maps.Map | null>(null);
|
|
94
|
-
const gmpMapRef = useRef<any>(null); // Ref for custom element
|
|
95
|
-
const infoWindowRef = useRef<google.maps.InfoWindow | null>(null);
|
|
96
|
-
const circleRef = useRef<google.maps.Circle | null>(null);
|
|
97
|
-
const polygonRef = useRef<google.maps.Polygon | null>(null);
|
|
98
|
-
|
|
99
|
-
// Resolve theme colors for Map shapes (Circle, Polygon)
|
|
100
|
-
const [themeColors, setThemeColors] = useState({
|
|
101
|
-
primary: '#4F46E5',
|
|
102
|
-
chart2: '#10B981',
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
useEffect(() => {
|
|
106
|
-
if (typeof window !== 'undefined') {
|
|
107
|
-
const styles = getComputedStyle(document.documentElement);
|
|
108
|
-
setThemeColors({
|
|
109
|
-
primary: styles.getPropertyValue('--primary').trim() || '#4F46E5',
|
|
110
|
-
chart2: styles.getPropertyValue('--chart-2').trim() || '#10B981',
|
|
111
|
-
});
|
|
112
|
-
}
|
|
113
|
-
}, []);
|
|
114
|
-
|
|
115
|
-
// Load API Key
|
|
116
|
-
useEffect(() => {
|
|
117
|
-
if (!isLoaded && apiKey && !loadError && load) {
|
|
118
|
-
load(apiKey).catch(console.error);
|
|
119
|
-
}
|
|
120
|
-
}, [isLoaded, apiKey, loadError, load]);
|
|
121
|
-
|
|
122
|
-
// Handle gmp-map initialization
|
|
123
|
-
useEffect(() => {
|
|
124
|
-
if (!isLoaded || !gmpMapRef.current) return;
|
|
125
|
-
|
|
126
|
-
const gmpMap = gmpMapRef.current;
|
|
127
|
-
|
|
128
|
-
// Access the underlying map instance
|
|
129
|
-
if (gmpMap.innerMap) {
|
|
130
|
-
mapRef.current = gmpMap.innerMap;
|
|
131
|
-
if (onMapLoad) {
|
|
132
|
-
onMapLoad(gmpMap.innerMap);
|
|
133
|
-
}
|
|
134
|
-
} else {
|
|
135
|
-
// Fallback or wait for it to be ready if needed, usually available immediately after upgrade
|
|
136
|
-
// Listener for 'gmp-map-load' doesn't exist standardly, but checking availability
|
|
137
|
-
const interval = setInterval(() => {
|
|
138
|
-
if (gmpMap.innerMap) {
|
|
139
|
-
mapRef.current = gmpMap.innerMap;
|
|
140
|
-
if (onMapLoad) {
|
|
141
|
-
onMapLoad(gmpMap.innerMap);
|
|
142
|
-
}
|
|
143
|
-
clearInterval(interval);
|
|
144
|
-
}
|
|
145
|
-
}, 100);
|
|
146
|
-
return () => clearInterval(interval);
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
return () => {
|
|
150
|
-
mapRef.current = null;
|
|
151
|
-
};
|
|
152
|
-
}, [isLoaded]);
|
|
153
|
-
|
|
154
|
-
// Sync properties with gmp-map custom element
|
|
155
|
-
useEffect(() => {
|
|
156
|
-
if (gmpMapRef.current) {
|
|
157
|
-
if (center) gmpMapRef.current.center = center;
|
|
158
|
-
}
|
|
159
|
-
}, [center]);
|
|
160
|
-
|
|
161
|
-
useEffect(() => {
|
|
162
|
-
if (gmpMapRef.current) {
|
|
163
|
-
if (zoom !== undefined) gmpMapRef.current.zoom = zoom;
|
|
164
|
-
}
|
|
165
|
-
}, [zoom]);
|
|
166
|
-
|
|
167
|
-
useEffect(() => {
|
|
168
|
-
if (gmpMapRef.current) {
|
|
169
|
-
if (props.mapTypeId) gmpMapRef.current.mapTypeId = props.mapTypeId;
|
|
170
|
-
}
|
|
171
|
-
}, [props.mapTypeId]);
|
|
172
|
-
|
|
173
|
-
// mapId is now set declaratively on the gmp-map element
|
|
174
|
-
|
|
175
|
-
// Update Circle (Imperative approach still needed for shapes as there are no gmp-circle elements yet)
|
|
176
|
-
useEffect(() => {
|
|
177
|
-
const map = mapRef.current;
|
|
178
|
-
if (!map || !isLoaded) return;
|
|
179
|
-
|
|
180
|
-
circleRef.current?.setMap(null);
|
|
181
|
-
circleRef.current = null;
|
|
182
|
-
|
|
183
|
-
if (circle && circle.center && circle.radius) {
|
|
184
|
-
circleRef.current = new google.maps.Circle({
|
|
185
|
-
map,
|
|
186
|
-
center: circle.center,
|
|
187
|
-
radius: circle.radius,
|
|
188
|
-
fillColor: circle.fillColor || themeColors.primary,
|
|
189
|
-
fillOpacity: 0.2,
|
|
190
|
-
strokeColor: circle.strokeColor || themeColors.primary,
|
|
191
|
-
strokeOpacity: 0.8,
|
|
192
|
-
strokeWeight: 2,
|
|
193
|
-
});
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
return () => {
|
|
197
|
-
circleRef.current?.setMap(null);
|
|
198
|
-
};
|
|
199
|
-
}, [circle, isLoaded, themeColors, mapRef.current]); // Added mapRef.current dependency
|
|
200
|
-
|
|
201
|
-
// Update Polygon (Imperative approach still needed)
|
|
202
|
-
useEffect(() => {
|
|
203
|
-
const map = mapRef.current;
|
|
204
|
-
if (!map || !isLoaded) return;
|
|
205
|
-
|
|
206
|
-
polygonRef.current?.setMap(null);
|
|
207
|
-
polygonRef.current = null;
|
|
208
|
-
|
|
209
|
-
if (polygon && polygon.paths) {
|
|
210
|
-
polygonRef.current = new google.maps.Polygon({
|
|
211
|
-
map,
|
|
212
|
-
paths: polygon.paths,
|
|
213
|
-
fillColor: polygon.fillColor || themeColors.chart2,
|
|
214
|
-
fillOpacity: 0.2,
|
|
215
|
-
strokeColor: polygon.strokeColor || themeColors.chart2,
|
|
216
|
-
strokeOpacity: 0.8,
|
|
217
|
-
strokeWeight: 2,
|
|
218
|
-
});
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
return () => {
|
|
222
|
-
polygonRef.current?.setMap(null);
|
|
223
|
-
};
|
|
224
|
-
}, [polygon, isLoaded, themeColors, mapRef.current]);
|
|
225
|
-
|
|
226
|
-
// Layers
|
|
227
|
-
useMapLayers(mapRef.current, layers || {});
|
|
228
|
-
|
|
229
|
-
// InfoWindow Management
|
|
230
|
-
useEffect(() => {
|
|
231
|
-
const map = mapRef.current;
|
|
232
|
-
if (!map || selectedMarker === null) {
|
|
233
|
-
infoWindowRef.current?.close();
|
|
234
|
-
return;
|
|
235
|
-
}
|
|
236
|
-
|
|
237
|
-
const markerData = markers[selectedMarker];
|
|
238
|
-
if (!markerData) return;
|
|
239
|
-
|
|
240
|
-
// Note: We can't easily attach InfoWindow to gmp-advanced-marker via `anchor` prop in JS API
|
|
241
|
-
// because gmp-advanced-marker is an HTMLElement, not an AdvancedMarkerElement instance directly in the same way.
|
|
242
|
-
// However, gmp-advanced-marker has an .innerMarker property which IS the AdvancedMarkerElement.
|
|
243
|
-
|
|
244
|
-
// We need references to the marker elements.
|
|
245
|
-
// Since we are rendering them declaratively, we need to capture their refs or find them.
|
|
246
|
-
// A simple way is to query them or use a callback ref approach in the render loop.
|
|
247
|
-
// But here, selectedMarker is an index.
|
|
248
|
-
|
|
249
|
-
// Let's defer InfoWindow opening to the click handler of the marker itself
|
|
250
|
-
}, [selectedMarker, markers]);
|
|
251
|
-
|
|
252
|
-
const handleMarkerClick = (index: number, markerElement: any) => {
|
|
253
|
-
setSelectedMarker(index);
|
|
254
|
-
const map = mapRef.current;
|
|
255
|
-
if (!map) return;
|
|
256
|
-
|
|
257
|
-
const markerData = markers[index];
|
|
258
|
-
if (!markerData) return;
|
|
259
|
-
|
|
260
|
-
// Determine content to render
|
|
261
|
-
let contentToRender = markerData.richContent;
|
|
262
|
-
|
|
263
|
-
// If no richContent but title/info exists, create default standard content
|
|
264
|
-
if (!contentToRender && (markerData.title || markerData.info)) {
|
|
265
|
-
contentToRender = (
|
|
266
|
-
<div className="p-4 pr-8 min-w-[200px] max-w-[300px]">
|
|
267
|
-
{markerData.title && (
|
|
268
|
-
<h4 className="font-semibold text-base mb-1">{markerData.title}</h4>
|
|
269
|
-
)}
|
|
270
|
-
{markerData.info && <p className="text-sm text-muted-foreground">{markerData.info}</p>}
|
|
271
|
-
</div>
|
|
272
|
-
);
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
if (contentToRender && markerElement.innerMarker) {
|
|
276
|
-
if (!infoWindowRef.current) {
|
|
277
|
-
infoWindowRef.current = new google.maps.InfoWindow();
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
const container = document.createElement('div');
|
|
281
|
-
const root = createRoot(container);
|
|
282
|
-
root.render(contentToRender);
|
|
283
|
-
|
|
284
|
-
infoWindowRef.current.setContent(container);
|
|
285
|
-
infoWindowRef.current.open({
|
|
286
|
-
map,
|
|
287
|
-
anchor: markerElement.innerMarker,
|
|
288
|
-
});
|
|
289
|
-
|
|
290
|
-
const listener = infoWindowRef.current.addListener('closeclick', () => {
|
|
291
|
-
setSelectedMarker(null);
|
|
292
|
-
setTimeout(() => root.unmount(), 0);
|
|
293
|
-
});
|
|
294
|
-
// Cleanup logic for previous opens is tricky with this imperativeness mixed with declarative,
|
|
295
|
-
// but 'open' usually closes previous one if same instance.
|
|
296
|
-
}
|
|
297
|
-
};
|
|
298
|
-
|
|
299
|
-
if (loadError) {
|
|
300
|
-
return (
|
|
301
|
-
<div
|
|
302
|
-
ref={ref}
|
|
303
|
-
className={cn(
|
|
304
|
-
'relative rounded-[var(--radius-card)] border border-destructive/50 overflow-hidden bg-destructive/5',
|
|
305
|
-
className
|
|
306
|
-
)}
|
|
307
|
-
style={{ height }}
|
|
308
|
-
{...divProps}
|
|
309
|
-
>
|
|
310
|
-
<div className="absolute inset-0 flex items-center justify-center">
|
|
311
|
-
<div className="text-center space-y-2 p-6">
|
|
312
|
-
<div className="text-destructive">
|
|
313
|
-
<svg
|
|
314
|
-
className="w-12 h-12 mx-auto"
|
|
315
|
-
fill="none"
|
|
316
|
-
stroke="currentColor"
|
|
317
|
-
viewBox="0 0 24 24"
|
|
318
|
-
>
|
|
319
|
-
<path
|
|
320
|
-
strokeLinecap="round"
|
|
321
|
-
strokeLinejoin="round"
|
|
322
|
-
strokeWidth={2}
|
|
323
|
-
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
|
|
324
|
-
/>
|
|
325
|
-
</svg>
|
|
326
|
-
</div>
|
|
327
|
-
<p className="text-sm font-medium text-foreground">Failed to load Google Maps</p>
|
|
328
|
-
<p className="text-xs text-muted-foreground">Check API key in Settings</p>
|
|
329
|
-
</div>
|
|
330
|
-
</div>
|
|
331
|
-
</div>
|
|
332
|
-
);
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
if (!isLoaded) {
|
|
336
|
-
return (
|
|
337
|
-
<div
|
|
338
|
-
ref={ref}
|
|
339
|
-
className={cn(
|
|
340
|
-
'relative rounded-[var(--radius-card)] border border-border overflow-hidden bg-muted animate-pulse',
|
|
341
|
-
className
|
|
342
|
-
)}
|
|
343
|
-
style={{ height }}
|
|
344
|
-
{...divProps}
|
|
345
|
-
/>
|
|
346
|
-
);
|
|
347
|
-
}
|
|
348
|
-
|
|
349
|
-
return (
|
|
350
|
-
<div
|
|
351
|
-
ref={ref}
|
|
352
|
-
className={cn(
|
|
353
|
-
'relative rounded-[var(--radius-card)] border border-border overflow-hidden',
|
|
354
|
-
className
|
|
355
|
-
)}
|
|
356
|
-
style={{ height }} // Apply height to container
|
|
357
|
-
{...divProps}
|
|
358
|
-
>
|
|
359
|
-
{/* @ts-ignore - gmp-map is a custom element */}
|
|
360
|
-
<gmp-map
|
|
361
|
-
ref={gmpMapRef}
|
|
362
|
-
style={{ height: '100%', width: '100%', display: 'block' }}
|
|
363
|
-
map-id={props.mapId || 'DEMO_MAP_ID'}
|
|
364
|
-
>
|
|
365
|
-
{markers.map((markerData, idx) => {
|
|
366
|
-
const markerColor = markerData.customColor || 'var(--primary)';
|
|
367
|
-
const iconColor = markerData.iconColor || 'white';
|
|
368
|
-
|
|
369
|
-
// Generate icon/content
|
|
370
|
-
// Note: gmp-advanced-marker slot content is the custom marker content
|
|
371
|
-
return (
|
|
372
|
-
// @ts-ignore
|
|
373
|
-
<gmp-advanced-marker
|
|
374
|
-
key={idx}
|
|
375
|
-
title={markerData.title}
|
|
376
|
-
ref={(el: any) => {
|
|
377
|
-
if (el) {
|
|
378
|
-
el.position = markerData.position;
|
|
379
|
-
// Remove previous listener if any (useEffect cleanups handle component unmounts,
|
|
380
|
-
// but for ref callback we rely on it being called with null on unmount or re-render)
|
|
381
|
-
// However, adding listener repeatedly on re-renders might duplicate if not careful.
|
|
382
|
-
// React ref callback is called with null then value on updates if inline function changes.
|
|
383
|
-
// Ideally we should use a stable ref or effect, but this simple way works if we assume
|
|
384
|
-
// the element is recreated or we accept some overhead.
|
|
385
|
-
// Better: just add listener. standard addEventListener doesn't dedupe by default but
|
|
386
|
-
// with arrow function it will add new one.
|
|
387
|
-
// To be safe and simple: just set properties.
|
|
388
|
-
// For event listener, it's safer to not add it here if it's already there?
|
|
389
|
-
// Actually, let's just use the property 'onclick' if available? No, custom events.
|
|
390
|
-
// We will assume the element is fresh or we don't care too much about duplicate listeners for now
|
|
391
|
-
// AS LONG AS we don't create memory leaks.
|
|
392
|
-
// A better pattern is to use a Map<Element, Handler> to manage listeners but let's stick to simple first.
|
|
393
|
-
el.addEventListener('gmp-click', () => handleMarkerClick(idx, el));
|
|
394
|
-
}
|
|
395
|
-
}}
|
|
396
|
-
>
|
|
397
|
-
<div
|
|
398
|
-
className="flex items-center justify-center w-8 h-8 border-[3px] border-background shadow-lg cursor-pointer origin-center transition-transform duration-200"
|
|
399
|
-
style={{
|
|
400
|
-
borderRadius: '50% 50% 50% 0',
|
|
401
|
-
transform: 'rotate(-45deg)',
|
|
402
|
-
backgroundColor: markerColor,
|
|
403
|
-
}}
|
|
404
|
-
>
|
|
405
|
-
<div className="flex items-center justify-center rotate-45">
|
|
406
|
-
{markerData.iconSvg ? (
|
|
407
|
-
<div
|
|
408
|
-
dangerouslySetInnerHTML={{ __html: markerData.iconSvg }}
|
|
409
|
-
style={{ color: iconColor, width: 16, height: 16, fill: 'currentColor' }}
|
|
410
|
-
/>
|
|
411
|
-
) : (
|
|
412
|
-
<span className="font-semibold text-sm" style={{ color: iconColor }}>
|
|
413
|
-
{markerData.icon || markerData.label || ''}
|
|
414
|
-
</span>
|
|
415
|
-
)}
|
|
416
|
-
</div>
|
|
417
|
-
</div>
|
|
418
|
-
</gmp-advanced-marker>
|
|
419
|
-
);
|
|
420
|
-
})}
|
|
421
|
-
</gmp-map>
|
|
422
|
-
</div>
|
|
423
|
-
);
|
|
424
|
-
}
|
|
425
|
-
);
|
|
426
|
-
MapContent.displayName = 'MapContent';
|
|
427
|
-
|
|
428
|
-
/**
|
|
429
|
-
* Primary Google Maps component.
|
|
430
|
-
*
|
|
431
|
-
* @description
|
|
432
|
-
* Supports Advanced Markers, Circles, Polygons, and custom tile Layers.
|
|
433
|
-
* Automatically loads the Google Maps JavaScript API via `useGoogleMapsLoader`.
|
|
434
|
-
*
|
|
435
|
-
* @ai-rules
|
|
436
|
-
* 1. REQUIRED: Provide a valid `apiKey` via prop or the `VITE_GOOGLE_MAPS_API_KEY` environment variable.
|
|
437
|
-
* 2. Use the `markers` prop to render points of interest. Supports `richContent` for React-based InfoWindows.
|
|
438
|
-
* 3. Always set `height` explicitly (default is 400px) to ensure the map is visible in the layout.
|
|
439
|
-
*/
|
|
440
|
-
export const Map = React.forwardRef<HTMLDivElement, MapProps>((props, ref) => {
|
|
441
|
-
const { isLoaded, loadError } = useGoogleMapsLoader();
|
|
442
|
-
const effectiveApiKey =
|
|
443
|
-
props.apiKey ||
|
|
444
|
-
(typeof import.meta !== 'undefined' &&
|
|
445
|
-
import.meta.env &&
|
|
446
|
-
import.meta.env.VITE_GOOGLE_MAPS_API_KEY) ||
|
|
447
|
-
'';
|
|
448
|
-
|
|
449
|
-
const isValidKey =
|
|
450
|
-
effectiveApiKey &&
|
|
451
|
-
effectiveApiKey !== 'YOUR_GOOGLE_MAPS_API_KEY_HERE' &&
|
|
452
|
-
effectiveApiKey.startsWith('AIza');
|
|
453
|
-
|
|
454
|
-
if (isLoaded || isValidKey || loadError) {
|
|
455
|
-
return <MapContent ref={ref} {...props} apiKey={effectiveApiKey} />;
|
|
456
|
-
}
|
|
457
|
-
|
|
458
|
-
// Check if the script is injected in the DOM (loading via provider)
|
|
459
|
-
const isScriptInjected =
|
|
460
|
-
typeof document !== 'undefined' &&
|
|
461
|
-
!!document.querySelector('script[src*="maps.googleapis.com/maps/api/js"]');
|
|
462
|
-
|
|
463
|
-
if (isScriptInjected) {
|
|
464
|
-
// Let MapContent show the loading skeleton
|
|
465
|
-
return <MapContent ref={ref} {...props} apiKey={effectiveApiKey} />;
|
|
466
|
-
}
|
|
467
|
-
|
|
468
|
-
return (
|
|
469
|
-
<div
|
|
470
|
-
ref={ref}
|
|
471
|
-
className={cn(
|
|
472
|
-
'relative rounded-[var(--radius-card)] border border-border overflow-hidden bg-muted',
|
|
473
|
-
props.className
|
|
474
|
-
)}
|
|
475
|
-
style={{ height: props.height || '400px' }}
|
|
476
|
-
>
|
|
477
|
-
<div className="absolute inset-0 flex items-center justify-center bg-muted">
|
|
478
|
-
<div className="text-center space-y-3 p-6">
|
|
479
|
-
<div className="w-16 h-16 mx-auto bg-primary/10 rounded-full flex items-center justify-center">
|
|
480
|
-
<svg
|
|
481
|
-
className="w-8 h-8 text-primary"
|
|
482
|
-
fill="none"
|
|
483
|
-
stroke="currentColor"
|
|
484
|
-
viewBox="0 0 24 24"
|
|
485
|
-
>
|
|
486
|
-
<path
|
|
487
|
-
strokeLinecap="round"
|
|
488
|
-
strokeLinejoin="round"
|
|
489
|
-
strokeWidth={2}
|
|
490
|
-
d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"
|
|
491
|
-
/>
|
|
492
|
-
<path
|
|
493
|
-
strokeLinecap="round"
|
|
494
|
-
strokeLinejoin="round"
|
|
495
|
-
strokeWidth={2}
|
|
496
|
-
d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"
|
|
497
|
-
/>
|
|
498
|
-
</svg>
|
|
499
|
-
</div>
|
|
500
|
-
<p className="text-sm text-muted-foreground">Configure Google Maps API Key in Settings</p>
|
|
501
|
-
</div>
|
|
502
|
-
</div>
|
|
503
|
-
</div>
|
|
504
|
-
);
|
|
505
|
-
});
|
|
506
|
-
Map.displayName = 'Map';
|
|
1
|
+
import React, { useEffect, useRef, useState } from 'react';
|
|
2
|
+
import { createRoot } from 'react-dom/client';
|
|
3
|
+
import { cn } from '../../shared/utils';
|
|
4
|
+
import { useGoogleMapsLoader } from '../google-maps-loader';
|
|
5
|
+
import { useMapLayers, MapLayersConfig } from '../map-layers';
|
|
6
|
+
|
|
7
|
+
declare global {
|
|
8
|
+
namespace JSX {
|
|
9
|
+
interface IntrinsicElements {
|
|
10
|
+
'gmp-map': React.DetailedHTMLProps<React.HTMLAttributes<HTMLElement>, HTMLElement> & {
|
|
11
|
+
'map-id'?: string;
|
|
12
|
+
center?: string | google.maps.LatLng | google.maps.LatLngLiteral;
|
|
13
|
+
zoom?: number;
|
|
14
|
+
};
|
|
15
|
+
'gmp-advanced-marker': React.DetailedHTMLProps<
|
|
16
|
+
React.HTMLAttributes<HTMLElement>,
|
|
17
|
+
HTMLElement
|
|
18
|
+
> & {
|
|
19
|
+
title?: string;
|
|
20
|
+
position?: string | google.maps.LatLng | google.maps.LatLngLiteral;
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
export interface MapProps extends React.HTMLAttributes<HTMLDivElement> {
|
|
26
|
+
center?: { lat: number; lng: number };
|
|
27
|
+
zoom?: number;
|
|
28
|
+
mapTypeId?: string;
|
|
29
|
+
mapId?: string;
|
|
30
|
+
markers?: Array<{
|
|
31
|
+
position: { lat: number; lng: number };
|
|
32
|
+
label?: string;
|
|
33
|
+
title?: string;
|
|
34
|
+
info?: string;
|
|
35
|
+
customColor?: string;
|
|
36
|
+
icon?: string;
|
|
37
|
+
iconSvg?: string;
|
|
38
|
+
iconColor?: string;
|
|
39
|
+
infoWindowContent?: string; // Custom HTML content for InfoWindow
|
|
40
|
+
richContent?: React.ReactNode; // Custom React component for InfoWindow
|
|
41
|
+
}>;
|
|
42
|
+
circle?: {
|
|
43
|
+
center: { lat: number; lng: number };
|
|
44
|
+
radius: number;
|
|
45
|
+
fillColor?: string;
|
|
46
|
+
strokeColor?: string;
|
|
47
|
+
};
|
|
48
|
+
polygon?: {
|
|
49
|
+
paths: Array<{ lat: number; lng: number }>[];
|
|
50
|
+
fillColor?: string;
|
|
51
|
+
strokeColor?: string;
|
|
52
|
+
};
|
|
53
|
+
layers?: MapLayersConfig;
|
|
54
|
+
height?: string;
|
|
55
|
+
apiKey?: string;
|
|
56
|
+
mapContainerClassName?: string;
|
|
57
|
+
disableDefaultUI?: boolean;
|
|
58
|
+
zoomControl?: boolean;
|
|
59
|
+
streetViewControl?: boolean;
|
|
60
|
+
mapTypeControl?: boolean;
|
|
61
|
+
fullscreenControl?: boolean;
|
|
62
|
+
gestureHandling?: 'cooperative' | 'greedy' | 'none' | 'auto';
|
|
63
|
+
onMapLoad?: (map: google.maps.Map) => void;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const DEFAULT_CENTER = { lat: -23.5505, lng: -46.6333 };
|
|
67
|
+
const DEFAULT_ZOOM = 12;
|
|
68
|
+
|
|
69
|
+
const MapContent = React.forwardRef<HTMLDivElement, MapProps & { apiKey: string }>(
|
|
70
|
+
({ apiKey, ...props }, ref) => {
|
|
71
|
+
const { isLoaded, loadError, load } = useGoogleMapsLoader();
|
|
72
|
+
const {
|
|
73
|
+
center = DEFAULT_CENTER,
|
|
74
|
+
zoom = DEFAULT_ZOOM,
|
|
75
|
+
markers = [],
|
|
76
|
+
circle,
|
|
77
|
+
polygon,
|
|
78
|
+
layers,
|
|
79
|
+
height = '400px',
|
|
80
|
+
mapContainerClassName,
|
|
81
|
+
disableDefaultUI = false,
|
|
82
|
+
zoomControl = true,
|
|
83
|
+
streetViewControl = false,
|
|
84
|
+
mapTypeControl = false,
|
|
85
|
+
fullscreenControl = true,
|
|
86
|
+
gestureHandling = 'cooperative',
|
|
87
|
+
onMapLoad,
|
|
88
|
+
className,
|
|
89
|
+
...divProps
|
|
90
|
+
} = props;
|
|
91
|
+
|
|
92
|
+
const [selectedMarker, setSelectedMarker] = useState<number | null>(null);
|
|
93
|
+
const mapRef = useRef<google.maps.Map | null>(null);
|
|
94
|
+
const gmpMapRef = useRef<any>(null); // Ref for custom element
|
|
95
|
+
const infoWindowRef = useRef<google.maps.InfoWindow | null>(null);
|
|
96
|
+
const circleRef = useRef<google.maps.Circle | null>(null);
|
|
97
|
+
const polygonRef = useRef<google.maps.Polygon | null>(null);
|
|
98
|
+
|
|
99
|
+
// Resolve theme colors for Map shapes (Circle, Polygon)
|
|
100
|
+
const [themeColors, setThemeColors] = useState({
|
|
101
|
+
primary: '#4F46E5',
|
|
102
|
+
chart2: '#10B981',
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
useEffect(() => {
|
|
106
|
+
if (typeof window !== 'undefined') {
|
|
107
|
+
const styles = getComputedStyle(document.documentElement);
|
|
108
|
+
setThemeColors({
|
|
109
|
+
primary: styles.getPropertyValue('--primary').trim() || '#4F46E5',
|
|
110
|
+
chart2: styles.getPropertyValue('--chart-2').trim() || '#10B981',
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
}, []);
|
|
114
|
+
|
|
115
|
+
// Load API Key
|
|
116
|
+
useEffect(() => {
|
|
117
|
+
if (!isLoaded && apiKey && !loadError && load) {
|
|
118
|
+
load(apiKey).catch(console.error);
|
|
119
|
+
}
|
|
120
|
+
}, [isLoaded, apiKey, loadError, load]);
|
|
121
|
+
|
|
122
|
+
// Handle gmp-map initialization
|
|
123
|
+
useEffect(() => {
|
|
124
|
+
if (!isLoaded || !gmpMapRef.current) return;
|
|
125
|
+
|
|
126
|
+
const gmpMap = gmpMapRef.current;
|
|
127
|
+
|
|
128
|
+
// Access the underlying map instance
|
|
129
|
+
if (gmpMap.innerMap) {
|
|
130
|
+
mapRef.current = gmpMap.innerMap;
|
|
131
|
+
if (onMapLoad) {
|
|
132
|
+
onMapLoad(gmpMap.innerMap);
|
|
133
|
+
}
|
|
134
|
+
} else {
|
|
135
|
+
// Fallback or wait for it to be ready if needed, usually available immediately after upgrade
|
|
136
|
+
// Listener for 'gmp-map-load' doesn't exist standardly, but checking availability
|
|
137
|
+
const interval = setInterval(() => {
|
|
138
|
+
if (gmpMap.innerMap) {
|
|
139
|
+
mapRef.current = gmpMap.innerMap;
|
|
140
|
+
if (onMapLoad) {
|
|
141
|
+
onMapLoad(gmpMap.innerMap);
|
|
142
|
+
}
|
|
143
|
+
clearInterval(interval);
|
|
144
|
+
}
|
|
145
|
+
}, 100);
|
|
146
|
+
return () => clearInterval(interval);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
return () => {
|
|
150
|
+
mapRef.current = null;
|
|
151
|
+
};
|
|
152
|
+
}, [isLoaded]);
|
|
153
|
+
|
|
154
|
+
// Sync properties with gmp-map custom element
|
|
155
|
+
useEffect(() => {
|
|
156
|
+
if (gmpMapRef.current) {
|
|
157
|
+
if (center) gmpMapRef.current.center = center;
|
|
158
|
+
}
|
|
159
|
+
}, [center]);
|
|
160
|
+
|
|
161
|
+
useEffect(() => {
|
|
162
|
+
if (gmpMapRef.current) {
|
|
163
|
+
if (zoom !== undefined) gmpMapRef.current.zoom = zoom;
|
|
164
|
+
}
|
|
165
|
+
}, [zoom]);
|
|
166
|
+
|
|
167
|
+
useEffect(() => {
|
|
168
|
+
if (gmpMapRef.current) {
|
|
169
|
+
if (props.mapTypeId) gmpMapRef.current.mapTypeId = props.mapTypeId;
|
|
170
|
+
}
|
|
171
|
+
}, [props.mapTypeId]);
|
|
172
|
+
|
|
173
|
+
// mapId is now set declaratively on the gmp-map element
|
|
174
|
+
|
|
175
|
+
// Update Circle (Imperative approach still needed for shapes as there are no gmp-circle elements yet)
|
|
176
|
+
useEffect(() => {
|
|
177
|
+
const map = mapRef.current;
|
|
178
|
+
if (!map || !isLoaded) return;
|
|
179
|
+
|
|
180
|
+
circleRef.current?.setMap(null);
|
|
181
|
+
circleRef.current = null;
|
|
182
|
+
|
|
183
|
+
if (circle && circle.center && circle.radius) {
|
|
184
|
+
circleRef.current = new google.maps.Circle({
|
|
185
|
+
map,
|
|
186
|
+
center: circle.center,
|
|
187
|
+
radius: circle.radius,
|
|
188
|
+
fillColor: circle.fillColor || themeColors.primary,
|
|
189
|
+
fillOpacity: 0.2,
|
|
190
|
+
strokeColor: circle.strokeColor || themeColors.primary,
|
|
191
|
+
strokeOpacity: 0.8,
|
|
192
|
+
strokeWeight: 2,
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return () => {
|
|
197
|
+
circleRef.current?.setMap(null);
|
|
198
|
+
};
|
|
199
|
+
}, [circle, isLoaded, themeColors, mapRef.current]); // Added mapRef.current dependency
|
|
200
|
+
|
|
201
|
+
// Update Polygon (Imperative approach still needed)
|
|
202
|
+
useEffect(() => {
|
|
203
|
+
const map = mapRef.current;
|
|
204
|
+
if (!map || !isLoaded) return;
|
|
205
|
+
|
|
206
|
+
polygonRef.current?.setMap(null);
|
|
207
|
+
polygonRef.current = null;
|
|
208
|
+
|
|
209
|
+
if (polygon && polygon.paths) {
|
|
210
|
+
polygonRef.current = new google.maps.Polygon({
|
|
211
|
+
map,
|
|
212
|
+
paths: polygon.paths,
|
|
213
|
+
fillColor: polygon.fillColor || themeColors.chart2,
|
|
214
|
+
fillOpacity: 0.2,
|
|
215
|
+
strokeColor: polygon.strokeColor || themeColors.chart2,
|
|
216
|
+
strokeOpacity: 0.8,
|
|
217
|
+
strokeWeight: 2,
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return () => {
|
|
222
|
+
polygonRef.current?.setMap(null);
|
|
223
|
+
};
|
|
224
|
+
}, [polygon, isLoaded, themeColors, mapRef.current]);
|
|
225
|
+
|
|
226
|
+
// Layers
|
|
227
|
+
useMapLayers(mapRef.current, layers || {});
|
|
228
|
+
|
|
229
|
+
// InfoWindow Management
|
|
230
|
+
useEffect(() => {
|
|
231
|
+
const map = mapRef.current;
|
|
232
|
+
if (!map || selectedMarker === null) {
|
|
233
|
+
infoWindowRef.current?.close();
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
const markerData = markers[selectedMarker];
|
|
238
|
+
if (!markerData) return;
|
|
239
|
+
|
|
240
|
+
// Note: We can't easily attach InfoWindow to gmp-advanced-marker via `anchor` prop in JS API
|
|
241
|
+
// because gmp-advanced-marker is an HTMLElement, not an AdvancedMarkerElement instance directly in the same way.
|
|
242
|
+
// However, gmp-advanced-marker has an .innerMarker property which IS the AdvancedMarkerElement.
|
|
243
|
+
|
|
244
|
+
// We need references to the marker elements.
|
|
245
|
+
// Since we are rendering them declaratively, we need to capture their refs or find them.
|
|
246
|
+
// A simple way is to query them or use a callback ref approach in the render loop.
|
|
247
|
+
// But here, selectedMarker is an index.
|
|
248
|
+
|
|
249
|
+
// Let's defer InfoWindow opening to the click handler of the marker itself
|
|
250
|
+
}, [selectedMarker, markers]);
|
|
251
|
+
|
|
252
|
+
const handleMarkerClick = (index: number, markerElement: any) => {
|
|
253
|
+
setSelectedMarker(index);
|
|
254
|
+
const map = mapRef.current;
|
|
255
|
+
if (!map) return;
|
|
256
|
+
|
|
257
|
+
const markerData = markers[index];
|
|
258
|
+
if (!markerData) return;
|
|
259
|
+
|
|
260
|
+
// Determine content to render
|
|
261
|
+
let contentToRender = markerData.richContent;
|
|
262
|
+
|
|
263
|
+
// If no richContent but title/info exists, create default standard content
|
|
264
|
+
if (!contentToRender && (markerData.title || markerData.info)) {
|
|
265
|
+
contentToRender = (
|
|
266
|
+
<div className="p-4 pr-8 min-w-[200px] max-w-[300px]">
|
|
267
|
+
{markerData.title && (
|
|
268
|
+
<h4 className="font-semibold text-base mb-1">{markerData.title}</h4>
|
|
269
|
+
)}
|
|
270
|
+
{markerData.info && <p className="text-sm text-muted-foreground">{markerData.info}</p>}
|
|
271
|
+
</div>
|
|
272
|
+
);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (contentToRender && markerElement.innerMarker) {
|
|
276
|
+
if (!infoWindowRef.current) {
|
|
277
|
+
infoWindowRef.current = new google.maps.InfoWindow();
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const container = document.createElement('div');
|
|
281
|
+
const root = createRoot(container);
|
|
282
|
+
root.render(contentToRender);
|
|
283
|
+
|
|
284
|
+
infoWindowRef.current.setContent(container);
|
|
285
|
+
infoWindowRef.current.open({
|
|
286
|
+
map,
|
|
287
|
+
anchor: markerElement.innerMarker,
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
const listener = infoWindowRef.current.addListener('closeclick', () => {
|
|
291
|
+
setSelectedMarker(null);
|
|
292
|
+
setTimeout(() => root.unmount(), 0);
|
|
293
|
+
});
|
|
294
|
+
// Cleanup logic for previous opens is tricky with this imperativeness mixed with declarative,
|
|
295
|
+
// but 'open' usually closes previous one if same instance.
|
|
296
|
+
}
|
|
297
|
+
};
|
|
298
|
+
|
|
299
|
+
if (loadError) {
|
|
300
|
+
return (
|
|
301
|
+
<div
|
|
302
|
+
ref={ref}
|
|
303
|
+
className={cn(
|
|
304
|
+
'relative rounded-[var(--radius-card)] border border-destructive/50 overflow-hidden bg-destructive/5',
|
|
305
|
+
className
|
|
306
|
+
)}
|
|
307
|
+
style={{ height }}
|
|
308
|
+
{...divProps}
|
|
309
|
+
>
|
|
310
|
+
<div className="absolute inset-0 flex items-center justify-center">
|
|
311
|
+
<div className="text-center space-y-2 p-6">
|
|
312
|
+
<div className="text-destructive">
|
|
313
|
+
<svg
|
|
314
|
+
className="w-12 h-12 mx-auto"
|
|
315
|
+
fill="none"
|
|
316
|
+
stroke="currentColor"
|
|
317
|
+
viewBox="0 0 24 24"
|
|
318
|
+
>
|
|
319
|
+
<path
|
|
320
|
+
strokeLinecap="round"
|
|
321
|
+
strokeLinejoin="round"
|
|
322
|
+
strokeWidth={2}
|
|
323
|
+
d="M12 9v2m0 4h.01m-6.938 4h13.856c1.54 0 2.502-1.667 1.732-3L13.732 4c-.77-1.333-2.694-1.333-3.464 0L3.34 16c-.77 1.333.192 3 1.732 3z"
|
|
324
|
+
/>
|
|
325
|
+
</svg>
|
|
326
|
+
</div>
|
|
327
|
+
<p className="text-sm font-medium text-foreground">Failed to load Google Maps</p>
|
|
328
|
+
<p className="text-xs text-muted-foreground">Check API key in Settings</p>
|
|
329
|
+
</div>
|
|
330
|
+
</div>
|
|
331
|
+
</div>
|
|
332
|
+
);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
if (!isLoaded) {
|
|
336
|
+
return (
|
|
337
|
+
<div
|
|
338
|
+
ref={ref}
|
|
339
|
+
className={cn(
|
|
340
|
+
'relative rounded-[var(--radius-card)] border border-border overflow-hidden bg-muted animate-pulse',
|
|
341
|
+
className
|
|
342
|
+
)}
|
|
343
|
+
style={{ height }}
|
|
344
|
+
{...divProps}
|
|
345
|
+
/>
|
|
346
|
+
);
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
return (
|
|
350
|
+
<div
|
|
351
|
+
ref={ref}
|
|
352
|
+
className={cn(
|
|
353
|
+
'relative rounded-[var(--radius-card)] border border-border overflow-hidden',
|
|
354
|
+
className
|
|
355
|
+
)}
|
|
356
|
+
style={{ height }} // Apply height to container
|
|
357
|
+
{...divProps}
|
|
358
|
+
>
|
|
359
|
+
{/* @ts-ignore - gmp-map is a custom element */}
|
|
360
|
+
<gmp-map
|
|
361
|
+
ref={gmpMapRef}
|
|
362
|
+
style={{ height: '100%', width: '100%', display: 'block' }}
|
|
363
|
+
map-id={props.mapId || 'DEMO_MAP_ID'}
|
|
364
|
+
>
|
|
365
|
+
{markers.map((markerData, idx) => {
|
|
366
|
+
const markerColor = markerData.customColor || 'var(--primary)';
|
|
367
|
+
const iconColor = markerData.iconColor || 'white';
|
|
368
|
+
|
|
369
|
+
// Generate icon/content
|
|
370
|
+
// Note: gmp-advanced-marker slot content is the custom marker content
|
|
371
|
+
return (
|
|
372
|
+
// @ts-ignore
|
|
373
|
+
<gmp-advanced-marker
|
|
374
|
+
key={idx}
|
|
375
|
+
title={markerData.title}
|
|
376
|
+
ref={(el: any) => {
|
|
377
|
+
if (el) {
|
|
378
|
+
el.position = markerData.position;
|
|
379
|
+
// Remove previous listener if any (useEffect cleanups handle component unmounts,
|
|
380
|
+
// but for ref callback we rely on it being called with null on unmount or re-render)
|
|
381
|
+
// However, adding listener repeatedly on re-renders might duplicate if not careful.
|
|
382
|
+
// React ref callback is called with null then value on updates if inline function changes.
|
|
383
|
+
// Ideally we should use a stable ref or effect, but this simple way works if we assume
|
|
384
|
+
// the element is recreated or we accept some overhead.
|
|
385
|
+
// Better: just add listener. standard addEventListener doesn't dedupe by default but
|
|
386
|
+
// with arrow function it will add new one.
|
|
387
|
+
// To be safe and simple: just set properties.
|
|
388
|
+
// For event listener, it's safer to not add it here if it's already there?
|
|
389
|
+
// Actually, let's just use the property 'onclick' if available? No, custom events.
|
|
390
|
+
// We will assume the element is fresh or we don't care too much about duplicate listeners for now
|
|
391
|
+
// AS LONG AS we don't create memory leaks.
|
|
392
|
+
// A better pattern is to use a Map<Element, Handler> to manage listeners but let's stick to simple first.
|
|
393
|
+
el.addEventListener('gmp-click', () => handleMarkerClick(idx, el));
|
|
394
|
+
}
|
|
395
|
+
}}
|
|
396
|
+
>
|
|
397
|
+
<div
|
|
398
|
+
className="flex items-center justify-center w-8 h-8 border-[3px] border-background shadow-lg cursor-pointer origin-center transition-transform duration-200"
|
|
399
|
+
style={{
|
|
400
|
+
borderRadius: '50% 50% 50% 0',
|
|
401
|
+
transform: 'rotate(-45deg)',
|
|
402
|
+
backgroundColor: markerColor,
|
|
403
|
+
}}
|
|
404
|
+
>
|
|
405
|
+
<div className="flex items-center justify-center rotate-45">
|
|
406
|
+
{markerData.iconSvg ? (
|
|
407
|
+
<div
|
|
408
|
+
dangerouslySetInnerHTML={{ __html: markerData.iconSvg }}
|
|
409
|
+
style={{ color: iconColor, width: 16, height: 16, fill: 'currentColor' }}
|
|
410
|
+
/>
|
|
411
|
+
) : (
|
|
412
|
+
<span className="font-semibold text-sm" style={{ color: iconColor }}>
|
|
413
|
+
{markerData.icon || markerData.label || ''}
|
|
414
|
+
</span>
|
|
415
|
+
)}
|
|
416
|
+
</div>
|
|
417
|
+
</div>
|
|
418
|
+
</gmp-advanced-marker>
|
|
419
|
+
);
|
|
420
|
+
})}
|
|
421
|
+
</gmp-map>
|
|
422
|
+
</div>
|
|
423
|
+
);
|
|
424
|
+
}
|
|
425
|
+
);
|
|
426
|
+
MapContent.displayName = 'MapContent';
|
|
427
|
+
|
|
428
|
+
/**
|
|
429
|
+
* Primary Google Maps component.
|
|
430
|
+
*
|
|
431
|
+
* @description
|
|
432
|
+
* Supports Advanced Markers, Circles, Polygons, and custom tile Layers.
|
|
433
|
+
* Automatically loads the Google Maps JavaScript API via `useGoogleMapsLoader`.
|
|
434
|
+
*
|
|
435
|
+
* @ai-rules
|
|
436
|
+
* 1. REQUIRED: Provide a valid `apiKey` via prop or the `VITE_GOOGLE_MAPS_API_KEY` environment variable.
|
|
437
|
+
* 2. Use the `markers` prop to render points of interest. Supports `richContent` for React-based InfoWindows.
|
|
438
|
+
* 3. Always set `height` explicitly (default is 400px) to ensure the map is visible in the layout.
|
|
439
|
+
*/
|
|
440
|
+
export const Map = React.forwardRef<HTMLDivElement, MapProps>((props, ref) => {
|
|
441
|
+
const { isLoaded, loadError } = useGoogleMapsLoader();
|
|
442
|
+
const effectiveApiKey =
|
|
443
|
+
props.apiKey ||
|
|
444
|
+
(typeof import.meta !== 'undefined' &&
|
|
445
|
+
import.meta.env &&
|
|
446
|
+
import.meta.env.VITE_GOOGLE_MAPS_API_KEY) ||
|
|
447
|
+
'';
|
|
448
|
+
|
|
449
|
+
const isValidKey =
|
|
450
|
+
effectiveApiKey &&
|
|
451
|
+
effectiveApiKey !== 'YOUR_GOOGLE_MAPS_API_KEY_HERE' &&
|
|
452
|
+
effectiveApiKey.startsWith('AIza');
|
|
453
|
+
|
|
454
|
+
if (isLoaded || isValidKey || loadError) {
|
|
455
|
+
return <MapContent ref={ref} {...props} apiKey={effectiveApiKey} />;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// Check if the script is injected in the DOM (loading via provider)
|
|
459
|
+
const isScriptInjected =
|
|
460
|
+
typeof document !== 'undefined' &&
|
|
461
|
+
!!document.querySelector('script[src*="maps.googleapis.com/maps/api/js"]');
|
|
462
|
+
|
|
463
|
+
if (isScriptInjected) {
|
|
464
|
+
// Let MapContent show the loading skeleton
|
|
465
|
+
return <MapContent ref={ref} {...props} apiKey={effectiveApiKey} />;
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
return (
|
|
469
|
+
<div
|
|
470
|
+
ref={ref}
|
|
471
|
+
className={cn(
|
|
472
|
+
'relative rounded-[var(--radius-card)] border border-border overflow-hidden bg-muted',
|
|
473
|
+
props.className
|
|
474
|
+
)}
|
|
475
|
+
style={{ height: props.height || '400px' }}
|
|
476
|
+
>
|
|
477
|
+
<div className="absolute inset-0 flex items-center justify-center bg-muted">
|
|
478
|
+
<div className="text-center space-y-3 p-6">
|
|
479
|
+
<div className="w-16 h-16 mx-auto bg-primary/10 rounded-full flex items-center justify-center">
|
|
480
|
+
<svg
|
|
481
|
+
className="w-8 h-8 text-primary"
|
|
482
|
+
fill="none"
|
|
483
|
+
stroke="currentColor"
|
|
484
|
+
viewBox="0 0 24 24"
|
|
485
|
+
>
|
|
486
|
+
<path
|
|
487
|
+
strokeLinecap="round"
|
|
488
|
+
strokeLinejoin="round"
|
|
489
|
+
strokeWidth={2}
|
|
490
|
+
d="M17.657 16.657L13.414 20.9a1.998 1.998 0 01-2.827 0l-4.244-4.243a8 8 0 1111.314 0z"
|
|
491
|
+
/>
|
|
492
|
+
<path
|
|
493
|
+
strokeLinecap="round"
|
|
494
|
+
strokeLinejoin="round"
|
|
495
|
+
strokeWidth={2}
|
|
496
|
+
d="M15 11a3 3 0 11-6 0 3 3 0 016 0z"
|
|
497
|
+
/>
|
|
498
|
+
</svg>
|
|
499
|
+
</div>
|
|
500
|
+
<p className="text-sm text-muted-foreground">Configure Google Maps API Key in Settings</p>
|
|
501
|
+
</div>
|
|
502
|
+
</div>
|
|
503
|
+
</div>
|
|
504
|
+
);
|
|
505
|
+
});
|
|
506
|
+
Map.displayName = 'Map';
|