@joewinke/jatui 0.1.23 → 0.1.24

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 (124) hide show
  1. package/CLAUDE.md +244 -0
  2. package/package.json +23 -10
  3. package/src/lib/actions/collapse.ts +0 -136
  4. package/src/lib/actions/railNav.ts +0 -473
  5. package/src/lib/components/AnnotationLayer.svelte +0 -108
  6. package/src/lib/components/AnnotationPanel.svelte +0 -319
  7. package/src/lib/components/AudioWaveform.svelte +0 -698
  8. package/src/lib/components/AvailabilityModal.svelte +0 -177
  9. package/src/lib/components/AvatarUpload.svelte +0 -234
  10. package/src/lib/components/Badge.svelte +0 -38
  11. package/src/lib/components/BookingForm.svelte +0 -278
  12. package/src/lib/components/BurndownChart.svelte +0 -778
  13. package/src/lib/components/Button.svelte +0 -81
  14. package/src/lib/components/CalendarPicker.svelte +0 -284
  15. package/src/lib/components/Card.svelte +0 -67
  16. package/src/lib/components/CharacterCounter.svelte +0 -82
  17. package/src/lib/components/ChipInput.svelte +0 -609
  18. package/src/lib/components/Collapse.svelte +0 -82
  19. package/src/lib/components/ColorSelector.svelte +0 -167
  20. package/src/lib/components/CommentThread.svelte +0 -773
  21. package/src/lib/components/ConfirmDialog.svelte +0 -348
  22. package/src/lib/components/ConfirmModal.svelte +0 -142
  23. package/src/lib/components/ContextMenu.svelte +0 -188
  24. package/src/lib/components/CountdownTimer.svelte +0 -94
  25. package/src/lib/components/DateRangePicker.svelte +0 -194
  26. package/src/lib/components/Drawer.svelte +0 -143
  27. package/src/lib/components/EmojiPicker.svelte +0 -159
  28. package/src/lib/components/EntityPreviewCard.svelte +0 -104
  29. package/src/lib/components/FileDropzone.svelte +0 -493
  30. package/src/lib/components/FilePicker.svelte +0 -488
  31. package/src/lib/components/FileThumbnail.svelte +0 -80
  32. package/src/lib/components/FilterDropdown.svelte +0 -203
  33. package/src/lib/components/FloatingActionBar.svelte +0 -354
  34. package/src/lib/components/GPSTracker.svelte +0 -202
  35. package/src/lib/components/HunkDiffView.svelte +0 -348
  36. package/src/lib/components/ImageLightbox.svelte +0 -274
  37. package/src/lib/components/ImageUpload.svelte +0 -147
  38. package/src/lib/components/InlineEdit.svelte +0 -291
  39. package/src/lib/components/InputDialog.svelte +0 -327
  40. package/src/lib/components/KeyboardShortcutsOverlay.svelte +0 -296
  41. package/src/lib/components/LazyImage.svelte +0 -123
  42. package/src/lib/components/LinkShortener.svelte +0 -274
  43. package/src/lib/components/LoadingSpinner.svelte +0 -106
  44. package/src/lib/components/LocationMap.svelte +0 -186
  45. package/src/lib/components/MapView.svelte +0 -341
  46. package/src/lib/components/MarkupEditor.svelte +0 -485
  47. package/src/lib/components/MarkupOverlay.svelte +0 -55
  48. package/src/lib/components/MediaWorkbench.svelte +0 -871
  49. package/src/lib/components/MilestoneCard.svelte +0 -98
  50. package/src/lib/components/MilestoneTimeline.svelte +0 -91
  51. package/src/lib/components/Modal.svelte +0 -244
  52. package/src/lib/components/PDFViewer.svelte +0 -105
  53. package/src/lib/components/PdfThumbnail.svelte +0 -63
  54. package/src/lib/components/PhoneInput.svelte +0 -212
  55. package/src/lib/components/ResizableDivider.svelte +0 -307
  56. package/src/lib/components/ResizablePanel.svelte +0 -305
  57. package/src/lib/components/SearchDropdown.svelte +0 -421
  58. package/src/lib/components/SelectInput.svelte +0 -237
  59. package/src/lib/components/SidebarUserFooter.svelte +0 -34
  60. package/src/lib/components/SignaturePad.svelte +0 -175
  61. package/src/lib/components/SmartImageEditor.svelte +0 -720
  62. package/src/lib/components/SortDropdown.svelte +0 -156
  63. package/src/lib/components/Sparkline.svelte +0 -116
  64. package/src/lib/components/SpeechForm.svelte +0 -114
  65. package/src/lib/components/StatusBadge.svelte +0 -157
  66. package/src/lib/components/TaskTypeIcon.svelte +0 -54
  67. package/src/lib/components/TextArea.svelte +0 -162
  68. package/src/lib/components/TextInput.svelte +0 -131
  69. package/src/lib/components/ThemeSelector.svelte +0 -206
  70. package/src/lib/components/TimeSlotPicker.svelte +0 -162
  71. package/src/lib/components/UserAvatar.svelte +0 -72
  72. package/src/lib/components/VariablePicker.svelte +0 -170
  73. package/src/lib/components/VoicePlayer.svelte +0 -422
  74. package/src/lib/components/linked-columns/LinkedColumns.svelte +0 -526
  75. package/src/lib/components/markup.ts +0 -287
  76. package/src/lib/components/messaging/Avatar.svelte +0 -81
  77. package/src/lib/components/messaging/ChannelInfoModal.svelte +0 -163
  78. package/src/lib/components/messaging/ChannelList.svelte +0 -107
  79. package/src/lib/components/messaging/ChannelMemberAvatarStack.svelte +0 -69
  80. package/src/lib/components/messaging/ChannelMembersModal.svelte +0 -182
  81. package/src/lib/components/messaging/CreateChannelModal.svelte +0 -190
  82. package/src/lib/components/messaging/DirectMessageList.svelte +0 -145
  83. package/src/lib/components/messaging/EmojiSelector.svelte +0 -261
  84. package/src/lib/components/messaging/MentionAutocomplete.svelte +0 -193
  85. package/src/lib/components/messaging/MessageAttachment.svelte +0 -273
  86. package/src/lib/components/messaging/MessageAttachmentUpload.svelte +0 -243
  87. package/src/lib/components/messaging/MessageInput.svelte +0 -452
  88. package/src/lib/components/messaging/MessageItem.svelte +0 -341
  89. package/src/lib/components/messaging/MessageThread.svelte +0 -306
  90. package/src/lib/components/messaging/NotificationSettingsModal.svelte +0 -234
  91. package/src/lib/components/messaging/QuotedMessageDisplay.svelte +0 -123
  92. package/src/lib/components/messaging/StartDMModal.svelte +0 -100
  93. package/src/lib/components/messaging/ThreadPanel.svelte +0 -153
  94. package/src/lib/components/pipeline/DESIGN.md +0 -361
  95. package/src/lib/components/pipeline/Pipeline.svelte +0 -391
  96. package/src/lib/components/pipeline/PipelineCard.svelte +0 -94
  97. package/src/lib/components/pipeline/PipelineColumn.svelte +0 -163
  98. package/src/lib/components/replay/ChapterTimeline.svelte +0 -326
  99. package/src/lib/components/session-nav/transcriptModel.ts +0 -352
  100. package/src/lib/data/emojis.ts +0 -1208
  101. package/src/lib/index.ts +0 -405
  102. package/src/lib/stores/confirmDialog.svelte.ts +0 -48
  103. package/src/lib/stores/inputDialog.svelte.ts +0 -51
  104. package/src/lib/styles/rail.css +0 -63
  105. package/src/lib/types/annotation.ts +0 -38
  106. package/src/lib/types/booking.ts +0 -143
  107. package/src/lib/types/comments.ts +0 -97
  108. package/src/lib/types/entityPreview.ts +0 -45
  109. package/src/lib/types/filePicker.ts +0 -16
  110. package/src/lib/types/googleMaps.d.ts +0 -51
  111. package/src/lib/types/maps.ts +0 -43
  112. package/src/lib/types/messaging.ts +0 -459
  113. package/src/lib/types/milestone.ts +0 -22
  114. package/src/lib/types/pipeline.ts +0 -150
  115. package/src/lib/types/smartImageEditor.ts +0 -39
  116. package/src/lib/types/templateVars.ts +0 -36
  117. package/src/lib/utils/currency.ts +0 -20
  118. package/src/lib/utils/daisyuiColors.ts +0 -243
  119. package/src/lib/utils/dateFormatters.ts +0 -196
  120. package/src/lib/utils/googleMapsLoader.ts +0 -84
  121. package/src/lib/utils/mentionParser.ts +0 -188
  122. package/src/lib/utils/phone.ts +0 -80
  123. package/src/lib/utils/phoneFormat.ts +0 -74
  124. package/src/lib/utils/taskUtils.ts +0 -75
@@ -1,123 +0,0 @@
1
- <script lang="ts">
2
- import { onMount, onDestroy } from "svelte"
3
-
4
- let {
5
- src = "",
6
- alt = "",
7
- className = "",
8
- aspectRatio = null,
9
- fit = "cover",
10
- preload = false,
11
- priority = false,
12
- width = null,
13
- height = null,
14
- } = $props<{
15
- src: string
16
- alt: string
17
- className?: string
18
- aspectRatio?: string | null
19
- fit?: "cover" | "contain" | "fill"
20
- preload?: boolean
21
- priority?: boolean
22
- width?: number | null
23
- height?: number | null
24
- }>()
25
-
26
- let loaded = $state(false)
27
- let error = $state(false)
28
- // svelte-ignore state_referenced_locally
29
- let shouldLoad = $state(preload || priority)
30
- let containerRef: HTMLDivElement
31
-
32
- function startLoadingImage() {
33
- if (loaded || !src) return
34
- shouldLoad = true
35
- }
36
-
37
- onMount(() => {
38
- if (priority || preload) {
39
- startLoadingImage()
40
- return
41
- }
42
-
43
- if (containerRef) {
44
- const observer = new IntersectionObserver(
45
- (entries) => {
46
- for (const entry of entries) {
47
- if (entry.isIntersecting) {
48
- startLoadingImage()
49
- observer.unobserve(entry.target)
50
- }
51
- }
52
- },
53
- { root: null, rootMargin: "200px", threshold: 0.01 }
54
- )
55
- observer.observe(containerRef)
56
- return () => observer.disconnect()
57
- }
58
- })
59
-
60
- $effect(() => {
61
- if (src) {
62
- loaded = false
63
- error = false
64
- }
65
- })
66
- </script>
67
-
68
- <div
69
- bind:this={containerRef}
70
- class="relative overflow-hidden {className}"
71
- style={aspectRatio ? `aspect-ratio: ${aspectRatio};` : ""}
72
- data-src={src}
73
- >
74
- {#if !loaded && shouldLoad}
75
- <div class="skeleton absolute inset-0 w-full h-full"></div>
76
- {/if}
77
-
78
- {#if shouldLoad}
79
- <img
80
- {src}
81
- {alt}
82
- {width}
83
- {height}
84
- class="w-full h-full object-{fit} transition-opacity duration-300 {loaded ? 'opacity-100' : 'opacity-0'}"
85
- onload={(e) => {
86
- loaded = true
87
- if (!width || !height) {
88
- const img = e.target as HTMLImageElement
89
- width = img.naturalWidth
90
- height = img.naturalHeight
91
- }
92
- }}
93
- onerror={() => (error = true)}
94
- loading={priority ? "eager" : "lazy"}
95
- decoding="async"
96
- />
97
- {/if}
98
-
99
- {#if !shouldLoad}
100
- <div
101
- class="w-full h-full bg-base-200"
102
- style={width && height ? `aspect-ratio: ${width / height};` : ""}
103
- ></div>
104
- {/if}
105
-
106
- {#if error && shouldLoad}
107
- <div class="absolute inset-0 flex items-center justify-center bg-base-200">
108
- <div class="flex flex-col items-center text-base-content/70">
109
- <svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="mb-2">
110
- <rect width="18" height="18" x="3" y="3" rx="2" ry="2"></rect>
111
- <circle cx="9" cy="9" r="2"></circle>
112
- <path d="m21 15-3.086-3.086a2 2 0 0 0-2.828 0L6 21"></path>
113
- </svg>
114
- <span class="text-xs">Failed to load image</span>
115
- </div>
116
- </div>
117
- {/if}
118
- </div>
119
-
120
- <style>
121
- img { will-change: opacity; }
122
- .skeleton { transform: translateZ(0); will-change: opacity; }
123
- </style>
@@ -1,274 +0,0 @@
1
- <script lang="ts">
2
- export interface ShortenedLink {
3
- id: string
4
- slug: string
5
- destination_url: string
6
- title?: string | null
7
- click_count: number
8
- created_at: string
9
- expires_at?: string | null
10
- is_active: boolean
11
- }
12
-
13
- interface Props {
14
- links: ShortenedLink[]
15
- baseUrl?: string
16
- showHeader?: boolean
17
- onCreate?: (data: { title: string; destination_url: string; slug: string; expires_at: string | null }) => void
18
- onDelete?: (id: string) => void
19
- onToggle?: (id: string, is_active: boolean) => void
20
- class?: string
21
- }
22
-
23
- let { links, baseUrl = '', showHeader = true, onCreate, onDelete, onToggle, class: className = '' }: Props = $props()
24
-
25
- let showForm = $state(false)
26
- let title = $state('')
27
- let destinationUrl = $state('')
28
- let customSlug = $state('')
29
- let expiresAt = $state('')
30
- let copiedId = $state<string | null>(null)
31
- let submitting = $state(false)
32
-
33
- function generateSlug(): string {
34
- const chars = 'abcdefghijklmnopqrstuvwxyz0123456789'
35
- let result = ''
36
- for (let i = 0; i < 6; i++) {
37
- result += chars.charAt(Math.floor(Math.random() * chars.length))
38
- }
39
- return result
40
- }
41
-
42
- function handleSubmit(e: Event) {
43
- e.preventDefault()
44
- if (!destinationUrl || submitting) return
45
-
46
- submitting = true
47
- const slug = customSlug.trim() || generateSlug()
48
-
49
- onCreate?.({
50
- title: title.trim(),
51
- destination_url: destinationUrl.trim(),
52
- slug,
53
- expires_at: expiresAt || null
54
- })
55
-
56
- title = ''
57
- destinationUrl = ''
58
- customSlug = ''
59
- expiresAt = ''
60
- showForm = false
61
- submitting = false
62
- }
63
-
64
- async function copyUrl(link: ShortenedLink) {
65
- const url = `${baseUrl}/l/${link.slug}`
66
- await navigator.clipboard.writeText(url)
67
- copiedId = link.id
68
- setTimeout(() => { copiedId = null }, 2000)
69
- }
70
-
71
- function isExpired(link: ShortenedLink): boolean {
72
- if (!link.expires_at) return false
73
- return new Date(link.expires_at) < new Date()
74
- }
75
- </script>
76
-
77
- <div class="space-y-4 {className}">
78
- {#if showHeader}
79
- <div class="flex items-center justify-between">
80
- <h2 class="text-lg font-semibold">Short Links</h2>
81
- {#if !showForm}
82
- <button class="btn btn-primary btn-sm" onclick={() => showForm = true}>
83
- <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
84
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
85
- </svg>
86
- New Link
87
- </button>
88
- {/if}
89
- </div>
90
- {:else if !showForm}
91
- <div class="flex justify-end">
92
- <button class="btn btn-primary btn-sm" onclick={() => showForm = true}>
93
- <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
94
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
95
- </svg>
96
- New Link
97
- </button>
98
- </div>
99
- {/if}
100
-
101
- {#if showForm}
102
- <form class="card bg-base-200" onsubmit={handleSubmit}>
103
- <div class="card-body gap-4">
104
- <div class="grid sm:grid-cols-2 gap-3">
105
- <div class="flex flex-col gap-1.5">
106
- <label class="text-sm font-medium" for="link-title">Title</label>
107
- <input
108
- id="link-title"
109
- type="text"
110
- class="input input-bordered input-sm w-full"
111
- placeholder="My Campaign Link"
112
- bind:value={title}
113
- />
114
- </div>
115
- <div class="flex flex-col gap-1.5">
116
- <label class="text-sm font-medium" for="link-destination">
117
- Destination URL <span class="text-error">*</span>
118
- </label>
119
- <input
120
- id="link-destination"
121
- type="url"
122
- class="input input-bordered input-sm w-full"
123
- placeholder="https://example.com/page"
124
- bind:value={destinationUrl}
125
- required
126
- />
127
- </div>
128
- </div>
129
-
130
- <div class="grid sm:grid-cols-2 gap-3">
131
- <div class="flex flex-col gap-1.5">
132
- <label class="text-sm font-medium" for="link-slug">Custom Slug</label>
133
- <div class="join w-full">
134
- <span class="join-item flex items-center px-3 bg-base-300 text-xs text-base-content/50 border border-base-content/20">/l/</span>
135
- <input
136
- id="link-slug"
137
- type="text"
138
- class="input input-bordered input-sm join-item flex-1 min-w-0"
139
- placeholder="auto-generated"
140
- bind:value={customSlug}
141
- pattern="[a-zA-Z0-9_-]+"
142
- />
143
- </div>
144
- </div>
145
- <div class="flex flex-col gap-1.5">
146
- <label class="text-sm font-medium" for="link-expires">Expires</label>
147
- <input
148
- id="link-expires"
149
- type="date"
150
- class="input input-bordered input-sm w-full"
151
- bind:value={expiresAt}
152
- />
153
- </div>
154
- </div>
155
-
156
- <div class="flex justify-end gap-2 pt-1">
157
- <button type="button" class="btn btn-ghost btn-sm" onclick={() => showForm = false}>Cancel</button>
158
- <button type="submit" class="btn btn-primary btn-sm" disabled={!destinationUrl || submitting}>
159
- {#if submitting}
160
- <span class="loading loading-spinner loading-xs"></span>
161
- {/if}
162
- Create Link
163
- </button>
164
- </div>
165
- </div>
166
- </form>
167
- {/if}
168
-
169
- {#if links.length === 0 && !showForm}
170
- <div class="card bg-base-200">
171
- <div class="card-body text-center py-8">
172
- <p class="text-base-content/60">No short links yet. Create your first one above.</p>
173
- </div>
174
- </div>
175
- {:else if links.length > 0}
176
- <div class="overflow-x-auto">
177
- <table class="table">
178
- <thead>
179
- <tr>
180
- <th>Link</th>
181
- <th>Destination</th>
182
- <th class="text-center">Clicks</th>
183
- <th class="text-center">Status</th>
184
- <th class="text-right">Actions</th>
185
- </tr>
186
- </thead>
187
- <tbody>
188
- {#each links as link (link.id)}
189
- <tr class="hover">
190
- <td>
191
- <div>
192
- {#if link.title}
193
- <div class="font-medium text-sm">{link.title}</div>
194
- {/if}
195
- <div class="text-primary text-sm font-mono">/l/{link.slug}</div>
196
- </div>
197
- </td>
198
- <td>
199
- <a
200
- href={link.destination_url}
201
- target="_blank"
202
- rel="noopener noreferrer"
203
- class="link link-hover text-sm max-w-xs truncate block"
204
- title={link.destination_url}
205
- >
206
- {link.destination_url}
207
- </a>
208
- </td>
209
- <td class="text-center">
210
- <span class="badge badge-ghost badge-sm">{link.click_count}</span>
211
- </td>
212
- <td class="text-center">
213
- {#if isExpired(link)}
214
- <span class="badge badge-error badge-sm">Expired</span>
215
- {:else if !link.is_active}
216
- <span class="badge badge-ghost badge-sm">Inactive</span>
217
- {:else}
218
- <span class="badge badge-success badge-sm">Active</span>
219
- {/if}
220
- </td>
221
- <td>
222
- <div class="flex items-center justify-end gap-1">
223
- <button
224
- class="btn btn-ghost btn-xs"
225
- title="Copy short URL"
226
- onclick={() => copyUrl(link)}
227
- >
228
- {#if copiedId === link.id}
229
- <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 text-success" fill="none" viewBox="0 0 24 24" stroke="currentColor">
230
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
231
- </svg>
232
- {:else}
233
- <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
234
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M8 16H6a2 2 0 01-2-2V6a2 2 0 012-2h8a2 2 0 012 2v2m-6 12h8a2 2 0 002-2v-8a2 2 0 00-2-2h-8a2 2 0 00-2 2v8a2 2 0 002 2z" />
235
- </svg>
236
- {/if}
237
- </button>
238
- {#if onToggle}
239
- <button
240
- class="btn btn-ghost btn-xs"
241
- title={link.is_active ? 'Deactivate' : 'Activate'}
242
- onclick={() => onToggle(link.id, !link.is_active)}
243
- >
244
- {#if link.is_active}
245
- <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
246
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M18.364 18.364A9 9 0 005.636 5.636m12.728 12.728A9 9 0 015.636 5.636m12.728 12.728L5.636 5.636" />
247
- </svg>
248
- {:else}
249
- <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
250
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z" />
251
- </svg>
252
- {/if}
253
- </button>
254
- {/if}
255
- {#if onDelete}
256
- <button
257
- class="btn btn-ghost btn-xs text-error"
258
- title="Delete link"
259
- onclick={() => onDelete(link.id)}
260
- >
261
- <svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4" fill="none" viewBox="0 0 24 24" stroke="currentColor">
262
- <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
263
- </svg>
264
- </button>
265
- {/if}
266
- </div>
267
- </td>
268
- </tr>
269
- {/each}
270
- </tbody>
271
- </table>
272
- </div>
273
- {/if}
274
- </div>
@@ -1,106 +0,0 @@
1
- <script lang="ts">
2
- /**
3
- * LoadingSpinner Component
4
- *
5
- * Customizable DaisyUI loading spinner with text, overlay, and speed options.
6
- */
7
-
8
- interface Props {
9
- type?: 'spinner' | 'dots' | 'ring' | 'ball' | 'bars' | 'infinity' | 'pulse';
10
- size?: 'xs' | 'sm' | 'md' | 'lg' | 'xl';
11
- text?: string;
12
- textPosition?: 'top' | 'bottom' | 'left' | 'right';
13
- overlay?: boolean;
14
- centered?: boolean;
15
- color?: string;
16
- ariaLabel?: string;
17
- visible?: boolean;
18
- speed?: 'slow' | 'normal' | 'fast';
19
- class?: string;
20
- }
21
-
22
- let {
23
- type = 'spinner',
24
- size = 'md',
25
- text = '',
26
- textPosition = 'bottom',
27
- overlay = false,
28
- centered = false,
29
- color = '',
30
- ariaLabel = 'Loading',
31
- visible = true,
32
- speed = 'normal',
33
- class: className = ''
34
- }: Props = $props();
35
-
36
- const sizeClass = $derived(
37
- { xs: 'loading-xs', sm: 'loading-sm', md: 'loading-md', lg: 'loading-lg', xl: 'loading-xl' }[size]
38
- );
39
-
40
- const typeClass = $derived(
41
- {
42
- spinner: 'loading-spinner',
43
- dots: 'loading-dots',
44
- ring: 'loading-ring',
45
- ball: 'loading-ball',
46
- bars: 'loading-bars',
47
- infinity: 'loading-infinity',
48
- pulse: 'loading-pulse'
49
- }[type]
50
- );
51
-
52
- const speedClass = $derived(
53
- { slow: '[animation-duration:2s]', normal: '', fast: '[animation-duration:0.5s]' }[speed]
54
- );
55
-
56
- const spinnerClass = $derived(
57
- ['loading', typeClass, sizeClass, speedClass, color ? '' : 'text-primary', className]
58
- .filter(Boolean)
59
- .join(' ')
60
- );
61
-
62
- const containerClass = $derived(
63
- [
64
- overlay ? 'fixed inset-0 flex items-center justify-center z-50' : '',
65
- centered && !overlay ? 'flex items-center justify-center' : '',
66
- textPosition === 'left' || textPosition === 'right' ? 'flex items-center' : '',
67
- textPosition === 'top' || textPosition === 'bottom' ? 'flex flex-col items-center' : ''
68
- ]
69
- .filter(Boolean)
70
- .join(' ')
71
- );
72
-
73
- const containerStyle = $derived(
74
- overlay ? 'background: color-mix(in oklch, #180042 55%, transparent)' : ''
75
- );
76
-
77
- const textClass = $derived(
78
- ['text-sm text-base-content/70', { top: 'mb-2', bottom: 'mt-2', left: 'mr-2', right: 'ml-2' }[textPosition]]
79
- .filter(Boolean)
80
- .join(' ')
81
- );
82
-
83
- const spinnerStyle = $derived(color ? `color: ${color}` : '');
84
- </script>
85
-
86
- {#if visible}
87
- <div class={containerClass} style={containerStyle} aria-label={ariaLabel} role="status" aria-live="polite">
88
- {#if text && textPosition === 'top'}
89
- <div class={textClass}>{text}</div>
90
- {/if}
91
- {#if text && textPosition === 'left'}
92
- <div class={textClass}>{text}</div>
93
- {/if}
94
-
95
- <div class={spinnerClass} style={spinnerStyle}></div>
96
-
97
- {#if text && textPosition === 'right'}
98
- <div class={textClass}>{text}</div>
99
- {/if}
100
- {#if text && textPosition === 'bottom'}
101
- <div class={textClass}>{text}</div>
102
- {/if}
103
-
104
- <span class="sr-only">{ariaLabel}</span>
105
- </div>
106
- {/if}
@@ -1,186 +0,0 @@
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>