@joewinke/jatui 0.1.11 → 0.1.20

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.
Files changed (100) hide show
  1. package/README.md +123 -0
  2. package/package.json +2 -1
  3. package/src/lib/actions/railNav.ts +473 -0
  4. package/src/lib/components/AnnotationLayer.svelte +108 -0
  5. package/src/lib/components/AnnotationPanel.svelte +319 -0
  6. package/src/lib/components/AudioWaveform.svelte +9 -5
  7. package/src/lib/components/AvailabilityModal.svelte +7 -3
  8. package/src/lib/components/AvatarUpload.svelte +27 -4
  9. package/src/lib/components/BookingForm.svelte +11 -9
  10. package/src/lib/components/BurndownChart.svelte +778 -0
  11. package/src/lib/components/Button.svelte +10 -1
  12. package/src/lib/components/CalendarPicker.svelte +3 -3
  13. package/src/lib/components/Card.svelte +2 -2
  14. package/src/lib/components/ChipInput.svelte +8 -3
  15. package/src/lib/components/ColorSelector.svelte +17 -13
  16. package/src/lib/components/CommentThread.svelte +773 -0
  17. package/src/lib/components/ConfirmDialog.svelte +348 -0
  18. package/src/lib/components/ConfirmModal.svelte +78 -11
  19. package/src/lib/components/ContextMenu.svelte +59 -19
  20. package/src/lib/components/CountdownTimer.svelte +1 -1
  21. package/src/lib/components/DateRangePicker.svelte +6 -4
  22. package/src/lib/components/Drawer.svelte +36 -3
  23. package/src/lib/components/EntityPreviewCard.svelte +104 -0
  24. package/src/lib/components/FileDropzone.svelte +493 -0
  25. package/src/lib/components/FilePicker.svelte +83 -14
  26. package/src/lib/components/FileThumbnail.svelte +80 -0
  27. package/src/lib/components/FilterDropdown.svelte +11 -11
  28. package/src/lib/components/GPSTracker.svelte +202 -0
  29. package/src/lib/components/HunkDiffView.svelte +348 -0
  30. package/src/lib/components/ImageLightbox.svelte +274 -0
  31. package/src/lib/components/ImageUpload.svelte +58 -9
  32. package/src/lib/components/InlineEdit.svelte +6 -2
  33. package/src/lib/components/InputDialog.svelte +327 -0
  34. package/src/lib/components/KeyboardShortcutsOverlay.svelte +296 -0
  35. package/src/lib/components/LazyImage.svelte +1 -0
  36. package/src/lib/components/LinkShortener.svelte +1 -1
  37. package/src/lib/components/LoadingSpinner.svelte +6 -2
  38. package/src/lib/components/LocationMap.svelte +186 -0
  39. package/src/lib/components/MapView.svelte +341 -0
  40. package/src/lib/components/MarkupEditor.svelte +485 -0
  41. package/src/lib/components/MarkupOverlay.svelte +55 -0
  42. package/src/lib/components/MediaWorkbench.svelte +871 -0
  43. package/src/lib/components/MilestoneCard.svelte +1 -1
  44. package/src/lib/components/MilestoneTimeline.svelte +1 -1
  45. package/src/lib/components/Modal.svelte +39 -4
  46. package/src/lib/components/PDFViewer.svelte +105 -0
  47. package/src/lib/components/PdfThumbnail.svelte +3 -1
  48. package/src/lib/components/PhoneInput.svelte +1 -1
  49. package/src/lib/components/ResizablePanel.svelte +4 -4
  50. package/src/lib/components/SearchDropdown.svelte +26 -13
  51. package/src/lib/components/SelectInput.svelte +26 -4
  52. package/src/lib/components/SidebarUserFooter.svelte +1 -1
  53. package/src/lib/components/SignaturePad.svelte +8 -4
  54. package/src/lib/components/SmartImageEditor.svelte +720 -0
  55. package/src/lib/components/SortDropdown.svelte +9 -3
  56. package/src/lib/components/Sparkline.svelte +9 -0
  57. package/src/lib/components/StatusBadge.svelte +20 -18
  58. package/src/lib/components/TextArea.svelte +24 -5
  59. package/src/lib/components/TextInput.svelte +29 -6
  60. package/src/lib/components/ThemeSelector.svelte +15 -4
  61. package/src/lib/components/TimeSlotPicker.svelte +7 -7
  62. package/src/lib/components/UserAvatar.svelte +14 -1
  63. package/src/lib/components/VariablePicker.svelte +170 -0
  64. package/src/lib/components/VoicePlayer.svelte +4 -3
  65. package/src/lib/components/linked-columns/LinkedColumns.svelte +520 -0
  66. package/src/lib/components/markup.ts +287 -0
  67. package/src/lib/components/messaging/ChannelInfoModal.svelte +9 -9
  68. package/src/lib/components/messaging/ChannelList.svelte +1 -1
  69. package/src/lib/components/messaging/ChannelMembersModal.svelte +1 -1
  70. package/src/lib/components/messaging/CreateChannelModal.svelte +1 -1
  71. package/src/lib/components/messaging/DirectMessageList.svelte +1 -1
  72. package/src/lib/components/messaging/EmojiSelector.svelte +2 -1
  73. package/src/lib/components/messaging/MentionAutocomplete.svelte +1 -1
  74. package/src/lib/components/messaging/MessageAttachment.svelte +3 -3
  75. package/src/lib/components/messaging/MessageAttachmentUpload.svelte +3 -3
  76. package/src/lib/components/messaging/MessageInput.svelte +1 -1
  77. package/src/lib/components/messaging/MessageItem.svelte +6 -3
  78. package/src/lib/components/messaging/NotificationSettingsModal.svelte +1 -1
  79. package/src/lib/components/messaging/QuotedMessageDisplay.svelte +6 -1
  80. package/src/lib/components/messaging/StartDMModal.svelte +1 -1
  81. package/src/lib/components/pipeline/Pipeline.svelte +4 -4
  82. package/src/lib/components/pipeline/PipelineCard.svelte +1 -1
  83. package/src/lib/components/pipeline/PipelineColumn.svelte +8 -3
  84. package/src/lib/components/replay/ChapterTimeline.svelte +326 -0
  85. package/src/lib/components/session-nav/transcriptModel.ts +352 -0
  86. package/src/lib/index.ts +138 -0
  87. package/src/lib/stores/confirmDialog.svelte.ts +48 -0
  88. package/src/lib/stores/inputDialog.svelte.ts +51 -0
  89. package/src/lib/styles/rail.css +63 -0
  90. package/src/lib/types/annotation.ts +38 -0
  91. package/src/lib/types/comments.ts +97 -0
  92. package/src/lib/types/entityPreview.ts +45 -0
  93. package/src/lib/types/filePicker.ts +2 -0
  94. package/src/lib/types/googleMaps.d.ts +51 -0
  95. package/src/lib/types/maps.ts +43 -0
  96. package/src/lib/types/smartImageEditor.ts +39 -0
  97. package/src/lib/types/templateVars.ts +36 -0
  98. package/src/lib/utils/dateFormatters.ts +12 -10
  99. package/src/lib/utils/googleMapsLoader.ts +84 -0
  100. package/src/lib/utils/taskUtils.ts +21 -7
@@ -0,0 +1,296 @@
1
+ <script lang="ts">
2
+ /**
3
+ * KeyboardShortcutsOverlay — press `?` to toggle a modal listing the
4
+ * keyboard shortcuts available on the current page. Press `?` again or
5
+ * `Escape` to dismiss. Input/contenteditable fields and Ctrl/Meta/Alt
6
+ * combos are ignored so the shortcut never interferes with typing.
7
+ *
8
+ * Usage:
9
+ *
10
+ * <KeyboardShortcutsOverlay shortcuts={[
11
+ * { key: 'j / ↓', description: 'Focus next item' },
12
+ * { key: 'k / ↑', description: 'Focus previous item' },
13
+ * { key: 'Enter', description: 'Open detail' },
14
+ * ]} />
15
+ */
16
+
17
+ export interface KeyboardShortcut {
18
+ key: string;
19
+ description: string;
20
+ }
21
+
22
+ export interface SectionEntry {
23
+ key: string;
24
+ description: string;
25
+ /** Optional oklch color to tint the key badge — used for legend entries */
26
+ color?: string;
27
+ }
28
+
29
+ export interface ShortcutSection {
30
+ title: string;
31
+ shortcuts: SectionEntry[];
32
+ }
33
+
34
+ let {
35
+ shortcuts = [],
36
+ sections,
37
+ title = 'Keyboard Shortcuts',
38
+ open = $bindable(false)
39
+ }: {
40
+ shortcuts?: KeyboardShortcut[];
41
+ /**
42
+ * Optional grouped sections (e.g. mode-specific shortcut tables for
43
+ * /inbox: list / detail / compose). When provided, takes
44
+ * precedence over the flat `shortcuts` prop.
45
+ */
46
+ sections?: ShortcutSection[];
47
+ title?: string;
48
+ open?: boolean;
49
+ } = $props();
50
+
51
+ let isOpen = $state(open);
52
+
53
+ $effect(() => {
54
+ isOpen = open;
55
+ });
56
+ $effect(() => {
57
+ open = isOpen;
58
+ });
59
+
60
+ function isTypingTarget(target: EventTarget | null): boolean {
61
+ if (!(target instanceof HTMLElement)) return false;
62
+ const tag = target.tagName;
63
+ if (tag === 'INPUT' || tag === 'TEXTAREA' || tag === 'SELECT') return true;
64
+ return target.isContentEditable;
65
+ }
66
+
67
+ function onKeydown(e: KeyboardEvent) {
68
+ if (e.ctrlKey || e.metaKey || e.altKey) return;
69
+
70
+ if (e.key === '?') {
71
+ // `?` is always Shift+/. Don't trigger while typing — would swallow the char.
72
+ if (isTypingTarget(e.target)) return;
73
+ e.preventDefault();
74
+ isOpen = !isOpen;
75
+ return;
76
+ }
77
+
78
+ if (isOpen && e.key === 'Escape') {
79
+ e.preventDefault();
80
+ isOpen = false;
81
+ }
82
+ }
83
+
84
+ $effect(() => {
85
+ window.addEventListener('keydown', onKeydown);
86
+ return () => window.removeEventListener('keydown', onKeydown);
87
+ });
88
+ </script>
89
+
90
+ {#if isOpen}
91
+ <div
92
+ class="kso-backdrop"
93
+ role="presentation"
94
+ onclick={() => (isOpen = false)}
95
+ >
96
+ <div
97
+ class="kso-panel animate-scale-in"
98
+ role="dialog"
99
+ aria-modal="true"
100
+ aria-label={title}
101
+ onclick={(e) => e.stopPropagation()}
102
+ onkeydown={(e) => e.stopPropagation()}
103
+ tabindex="-1"
104
+ >
105
+ <div class="kso-header">
106
+ <h2 class="kso-title">{title}</h2>
107
+ <button
108
+ type="button"
109
+ class="kso-close"
110
+ onclick={() => (isOpen = false)}
111
+ aria-label="Close keyboard shortcuts"
112
+ >
113
+ ×
114
+ </button>
115
+ </div>
116
+ <div class="kso-list">
117
+ {#if sections && sections.length > 0}
118
+ {#each sections as section, sIdx (section.title + '::' + sIdx)}
119
+ <div class="kso-section-title">{section.title}</div>
120
+ {#each section.shortcuts as { key, description, color }, i (section.title + '::' + key + '::' + i)}
121
+ <div class="kso-row">
122
+ <kbd
123
+ class="kso-key"
124
+ style={color
125
+ ? `color:${color};border-color:color-mix(in oklch,${color} 50%,oklch(0.35 0.02 250));background:color-mix(in oklch,${color} 12%,oklch(0.25 0.02 250))`
126
+ : ''}
127
+ >
128
+ {key}
129
+ </kbd>
130
+ <span class="kso-description">{description}</span>
131
+ </div>
132
+ {/each}
133
+ {/each}
134
+ {:else}
135
+ {#each shortcuts as { key, description }, i (key + '::' + i)}
136
+ <div class="kso-row">
137
+ <kbd class="kso-key">{key}</kbd>
138
+ <span class="kso-description">{description}</span>
139
+ </div>
140
+ {/each}
141
+ {/if}
142
+ <div class="kso-row kso-row-meta">
143
+ <kbd class="kso-key">?</kbd>
144
+ <span class="kso-description">Toggle this overlay</span>
145
+ </div>
146
+ <div class="kso-row kso-row-meta">
147
+ <kbd class="kso-key">Esc</kbd>
148
+ <span class="kso-description">Close overlay</span>
149
+ </div>
150
+ </div>
151
+ </div>
152
+ </div>
153
+ {/if}
154
+
155
+ <style>
156
+ .kso-backdrop {
157
+ position: fixed;
158
+ inset: 0;
159
+ background: oklch(0 0 0 / 0.6);
160
+ backdrop-filter: blur(2px);
161
+ z-index: 60;
162
+ display: flex;
163
+ align-items: center;
164
+ justify-content: center;
165
+ padding: 1.5rem;
166
+ animation: kso-fade 0.15s ease-out;
167
+ }
168
+
169
+ .kso-panel {
170
+ background: oklch(0.18 0.02 250);
171
+ border: 1px solid oklch(0.30 0.03 250);
172
+ border-radius: 0.75rem;
173
+ box-shadow: 0 20px 60px oklch(0 0 0 / 0.6);
174
+ max-width: 34rem;
175
+ width: 100%;
176
+ max-height: 80vh;
177
+ overflow: hidden;
178
+ display: flex;
179
+ flex-direction: column;
180
+ }
181
+
182
+ .kso-header {
183
+ display: flex;
184
+ align-items: center;
185
+ justify-content: space-between;
186
+ padding: 0.9rem 1.25rem;
187
+ border-bottom: 1px solid oklch(0.25 0.02 250);
188
+ flex-shrink: 0;
189
+ }
190
+
191
+ .kso-title {
192
+ font-size: 0.95rem;
193
+ font-weight: 600;
194
+ color: oklch(0.92 0.02 250);
195
+ margin: 0;
196
+ letter-spacing: 0.01em;
197
+ }
198
+
199
+ .kso-close {
200
+ background: transparent;
201
+ border: none;
202
+ color: oklch(0.65 0.02 250);
203
+ font-size: 1.5rem;
204
+ line-height: 1;
205
+ cursor: pointer;
206
+ padding: 0.15rem 0.5rem;
207
+ border-radius: 0.35rem;
208
+ transition:
209
+ background 0.12s ease,
210
+ color 0.12s ease;
211
+ }
212
+
213
+ .kso-close:hover {
214
+ color: oklch(0.94 0.02 250);
215
+ background: oklch(0.25 0.02 250);
216
+ }
217
+
218
+ .kso-list {
219
+ padding: 0.75rem 1.25rem 1.25rem;
220
+ display: flex;
221
+ flex-direction: column;
222
+ gap: 0.3rem;
223
+ overflow-y: auto;
224
+ }
225
+
226
+ .kso-row {
227
+ display: flex;
228
+ align-items: center;
229
+ gap: 1rem;
230
+ padding: 0.4rem 0.25rem;
231
+ }
232
+
233
+ .kso-section-title {
234
+ font-size: 0.7rem;
235
+ text-transform: uppercase;
236
+ letter-spacing: 0.08em;
237
+ color: oklch(0.62 0.05 240);
238
+ padding: 0.55rem 0.25rem 0.2rem;
239
+ border-bottom: 1px solid oklch(0.25 0.02 250);
240
+ margin-top: 0.25rem;
241
+ }
242
+
243
+ .kso-section-title:first-of-type {
244
+ margin-top: 0;
245
+ padding-top: 0.1rem;
246
+ }
247
+
248
+ .kso-row-meta {
249
+ opacity: 0.7;
250
+ border-top: 1px dashed oklch(0.25 0.02 250);
251
+ margin-top: 0.25rem;
252
+ padding-top: 0.5rem;
253
+ }
254
+
255
+ .kso-row-meta:not(:first-of-type) {
256
+ border-top: none;
257
+ margin-top: 0;
258
+ }
259
+
260
+ .kso-key {
261
+ font-family: ui-monospace, 'SF Mono', Menlo, Consolas, monospace;
262
+ font-size: 0.78rem;
263
+ background: oklch(0.25 0.02 250);
264
+ border: 1px solid oklch(0.35 0.02 250);
265
+ border-bottom-width: 2px;
266
+ border-radius: 0.35rem;
267
+ padding: 0.18rem 0.55rem;
268
+ min-width: 3.25rem;
269
+ text-align: center;
270
+ color: oklch(0.93 0.02 250);
271
+ flex-shrink: 0;
272
+ white-space: nowrap;
273
+ }
274
+
275
+ .kso-description {
276
+ font-size: 0.85rem;
277
+ color: oklch(0.78 0.02 250);
278
+ line-height: 1.35;
279
+ }
280
+
281
+ @keyframes kso-fade {
282
+ from {
283
+ opacity: 0;
284
+ }
285
+ to {
286
+ opacity: 1;
287
+ }
288
+ }
289
+
290
+ @media (prefers-reduced-motion: reduce) {
291
+ .kso-backdrop,
292
+ .kso-panel {
293
+ animation: none !important;
294
+ }
295
+ }
296
+ </style>
@@ -25,6 +25,7 @@
25
25
 
26
26
  let loaded = $state(false)
27
27
  let error = $state(false)
28
+ // svelte-ignore state_referenced_locally
28
29
  let shouldLoad = $state(preload || priority)
29
30
  let containerRef: HTMLDivElement
30
31
 
@@ -77,7 +77,7 @@
77
77
  <div class="space-y-4 {className}">
78
78
  {#if showHeader}
79
79
  <div class="flex items-center justify-between">
80
- <h2 class="text-xl font-bold">Short Links</h2>
80
+ <h2 class="text-lg font-semibold">Short Links</h2>
81
81
  {#if !showForm}
82
82
  <button class="btn btn-primary btn-sm" onclick={() => showForm = true}>
83
83
  <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
@@ -61,7 +61,7 @@
61
61
 
62
62
  const containerClass = $derived(
63
63
  [
64
- overlay ? 'fixed inset-0 bg-black/50 flex items-center justify-center z-50' : '',
64
+ overlay ? 'fixed inset-0 flex items-center justify-center z-50' : '',
65
65
  centered && !overlay ? 'flex items-center justify-center' : '',
66
66
  textPosition === 'left' || textPosition === 'right' ? 'flex items-center' : '',
67
67
  textPosition === 'top' || textPosition === 'bottom' ? 'flex flex-col items-center' : ''
@@ -70,6 +70,10 @@
70
70
  .join(' ')
71
71
  );
72
72
 
73
+ const containerStyle = $derived(
74
+ overlay ? 'background: color-mix(in oklch, #180042 55%, transparent)' : ''
75
+ );
76
+
73
77
  const textClass = $derived(
74
78
  ['text-sm text-base-content/70', { top: 'mb-2', bottom: 'mt-2', left: 'mr-2', right: 'ml-2' }[textPosition]]
75
79
  .filter(Boolean)
@@ -80,7 +84,7 @@
80
84
  </script>
81
85
 
82
86
  {#if visible}
83
- <div class={containerClass} aria-label={ariaLabel} role="status" aria-live="polite">
87
+ <div class={containerClass} style={containerStyle} aria-label={ariaLabel} role="status" aria-live="polite">
84
88
  {#if text && textPosition === 'top'}
85
89
  <div class={textClass}>{text}</div>
86
90
  {/if}
@@ -0,0 +1,186 @@
1
+ <script lang="ts">
2
+ /**
3
+ * LocationMap — single-location Google Maps embed with a pulsing dot marker.
4
+ *
5
+ * Shows the current GPS position on a map. While waiting for coords, renders
6
+ * a "Waiting for GPS signal…" placeholder.
7
+ *
8
+ * API key is passed explicitly — jatui has no $env dependency.
9
+ *
10
+ * Usage:
11
+ *
12
+ * <LocationMap
13
+ * apiKey={PUBLIC_GOOGLE_MAPS_API_KEY}
14
+ * latitude={coords.latitude}
15
+ * longitude={coords.longitude}
16
+ * accuracy={coords.accuracy}
17
+ * />
18
+ *
19
+ * Ported from flush technician/LocationMap (190L) — steelbridge fork capture (jst-e0d1u.4).
20
+ */
21
+ import { onMount, onDestroy } from 'svelte';
22
+ import { loadGoogleMapsAPI, isGoogleMapsLoaded } from '../utils/googleMapsLoader';
23
+
24
+ interface Props {
25
+ apiKey: string;
26
+ latitude?: number;
27
+ longitude?: number;
28
+ accuracy?: number;
29
+ showAccuracyCircle?: boolean;
30
+ height?: string;
31
+ }
32
+
33
+ let {
34
+ apiKey,
35
+ latitude,
36
+ longitude,
37
+ accuracy,
38
+ showAccuracyCircle = true,
39
+ height = '200px'
40
+ }: Props = $props();
41
+
42
+ let mapContainer: HTMLElement;
43
+ let map: google.maps.Map;
44
+ let marker: any;
45
+ let accuracyCircle: google.maps.Circle;
46
+ let AdvancedMarkerElement: any;
47
+
48
+ onMount(async () => {
49
+ if (!apiKey) {
50
+ console.warn('LocationMap: No Google Maps API key provided');
51
+ return;
52
+ }
53
+ try {
54
+ await loadGoogleMapsAPI({ apiKey, libraries: ['marker'] });
55
+ if (isGoogleMapsLoaded() && window.google?.maps?.marker) {
56
+ AdvancedMarkerElement = window.google.maps.marker.AdvancedMarkerElement;
57
+ initializeMap();
58
+ }
59
+ } catch (e) {
60
+ console.warn('LocationMap: Failed to load Google Maps API', e);
61
+ }
62
+ });
63
+
64
+ onDestroy(() => {
65
+ if (marker) marker.map = null;
66
+ if (accuracyCircle) accuracyCircle.setMap(null);
67
+ });
68
+
69
+ function initializeMap() {
70
+ const position = { lat: latitude ?? 26.3683, lng: longitude ?? -80.1289 };
71
+
72
+ map = new google.maps.Map(mapContainer, {
73
+ zoom: 16,
74
+ center: position,
75
+ mapId: 'jatui-location-map',
76
+ disableDefaultUI: true,
77
+ zoomControl: true,
78
+ mapTypeControl: false,
79
+ streetViewControl: false,
80
+ fullscreenControl: false
81
+ });
82
+
83
+ updateMarker();
84
+ }
85
+
86
+ function updateMarker() {
87
+ if (!map || !AdvancedMarkerElement) return;
88
+
89
+ const position = { lat: latitude ?? 26.3683, lng: longitude ?? -80.1289 };
90
+
91
+ if (marker) marker.map = null;
92
+
93
+ const el = document.createElement('div');
94
+ el.className = 'lm-current-location';
95
+ el.innerHTML = '<div class="lm-pulse-ring"></div><div class="lm-dot"></div>';
96
+
97
+ marker = new AdvancedMarkerElement({
98
+ position,
99
+ map,
100
+ content: el,
101
+ title: 'Current location'
102
+ });
103
+
104
+ if (accuracyCircle) accuracyCircle.setMap(null);
105
+
106
+ if (showAccuracyCircle && accuracy) {
107
+ accuracyCircle = new google.maps.Circle({
108
+ strokeColor: '#4285F4',
109
+ strokeOpacity: 0.8,
110
+ strokeWeight: 1,
111
+ fillColor: '#4285F4',
112
+ fillOpacity: 0.2,
113
+ map,
114
+ center: position,
115
+ radius: accuracy
116
+ });
117
+ }
118
+
119
+ map.setCenter(position);
120
+ }
121
+
122
+ $effect(() => {
123
+ if (map && latitude && longitude) updateMarker();
124
+ });
125
+ </script>
126
+
127
+ <div
128
+ class="relative rounded-lg overflow-hidden border border-base-300"
129
+ style="height: {height}"
130
+ >
131
+ <div bind:this={mapContainer} class="w-full h-full"></div>
132
+
133
+ {#if !latitude || !longitude}
134
+ <div class="absolute inset-0 flex items-center justify-center bg-base-200">
135
+ <div class="text-center">
136
+ <div class="loading loading-spinner loading-md text-primary"></div>
137
+ <p class="text-sm mt-2 text-base-content/70">Waiting for GPS signal…</p>
138
+ </div>
139
+ </div>
140
+ {/if}
141
+ </div>
142
+
143
+ <style>
144
+ :global(.lm-current-location) {
145
+ position: relative;
146
+ width: 20px;
147
+ height: 20px;
148
+ }
149
+
150
+ :global(.lm-dot) {
151
+ position: absolute;
152
+ top: 50%;
153
+ left: 50%;
154
+ transform: translate(-50%, -50%);
155
+ width: 12px;
156
+ height: 12px;
157
+ background-color: #4285f4;
158
+ border: 2px solid white;
159
+ border-radius: 50%;
160
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.3);
161
+ }
162
+
163
+ :global(.lm-pulse-ring) {
164
+ position: absolute;
165
+ top: 50%;
166
+ left: 50%;
167
+ transform: translate(-50%, -50%);
168
+ width: 20px;
169
+ height: 20px;
170
+ background-color: #4285f4;
171
+ border-radius: 50%;
172
+ opacity: 0.3;
173
+ animation: lm-pulse 2s infinite;
174
+ }
175
+
176
+ @keyframes lm-pulse {
177
+ 0% {
178
+ transform: translate(-50%, -50%) scale(1);
179
+ opacity: 0.3;
180
+ }
181
+ 100% {
182
+ transform: translate(-50%, -50%) scale(3);
183
+ opacity: 0;
184
+ }
185
+ }
186
+ </style>