@joewinke/jatui 0.1.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/package.json +46 -0
- package/src/lib/components/AudioWaveform.svelte +694 -0
- package/src/lib/components/AvailabilityModal.svelte +173 -0
- package/src/lib/components/Badge.svelte +38 -0
- package/src/lib/components/BookingForm.svelte +276 -0
- package/src/lib/components/Button.svelte +72 -0
- package/src/lib/components/CalendarPicker.svelte +284 -0
- package/src/lib/components/Card.svelte +67 -0
- package/src/lib/components/CharacterCounter.svelte +82 -0
- package/src/lib/components/ChipInput.svelte +596 -0
- package/src/lib/components/ColorSelector.svelte +163 -0
- package/src/lib/components/ConfirmModal.svelte +75 -0
- package/src/lib/components/CountdownTimer.svelte +94 -0
- package/src/lib/components/DateRangePicker.svelte +192 -0
- package/src/lib/components/Drawer.svelte +110 -0
- package/src/lib/components/FilterDropdown.svelte +202 -0
- package/src/lib/components/ImageUpload.svelte +97 -0
- package/src/lib/components/InlineEdit.svelte +283 -0
- package/src/lib/components/LazyImage.svelte +122 -0
- package/src/lib/components/LoadingSpinner.svelte +102 -0
- package/src/lib/components/Modal.svelte +208 -0
- package/src/lib/components/PhoneInput.svelte +92 -0
- package/src/lib/components/ResizableDivider.svelte +305 -0
- package/src/lib/components/ResizablePanel.svelte +302 -0
- package/src/lib/components/SearchDropdown.svelte +341 -0
- package/src/lib/components/SelectInput.svelte +215 -0
- package/src/lib/components/SignaturePad.svelte +171 -0
- package/src/lib/components/SortDropdown.svelte +148 -0
- package/src/lib/components/Sparkline.svelte +107 -0
- package/src/lib/components/SpeechForm.svelte +114 -0
- package/src/lib/components/StatusBadge.svelte +155 -0
- package/src/lib/components/TextArea.svelte +143 -0
- package/src/lib/components/TextInput.svelte +108 -0
- package/src/lib/components/ThemeSelector.svelte +195 -0
- package/src/lib/components/TimeSlotPicker.svelte +162 -0
- package/src/lib/components/VoicePlayer.svelte +420 -0
- package/src/lib/components/messaging/Avatar.svelte +81 -0
- package/src/lib/components/messaging/ChannelInfoModal.svelte +163 -0
- package/src/lib/components/messaging/ChannelList.svelte +107 -0
- package/src/lib/components/messaging/ChannelMemberAvatarStack.svelte +69 -0
- package/src/lib/components/messaging/ChannelMembersModal.svelte +182 -0
- package/src/lib/components/messaging/CreateChannelModal.svelte +190 -0
- package/src/lib/components/messaging/DirectMessageList.svelte +145 -0
- package/src/lib/components/messaging/EmojiSelector.svelte +260 -0
- package/src/lib/components/messaging/MentionAutocomplete.svelte +193 -0
- package/src/lib/components/messaging/MessageAttachment.svelte +270 -0
- package/src/lib/components/messaging/MessageAttachmentUpload.svelte +243 -0
- package/src/lib/components/messaging/MessageInput.svelte +451 -0
- package/src/lib/components/messaging/MessageItem.svelte +338 -0
- package/src/lib/components/messaging/MessageThread.svelte +306 -0
- package/src/lib/components/messaging/NotificationSettingsModal.svelte +234 -0
- package/src/lib/components/messaging/QuotedMessageDisplay.svelte +118 -0
- package/src/lib/components/messaging/StartDMModal.svelte +100 -0
- package/src/lib/components/messaging/ThreadPanel.svelte +153 -0
- package/src/lib/index.ts +185 -0
- package/src/lib/types/booking.ts +143 -0
- package/src/lib/types/messaging.ts +459 -0
- package/src/lib/utils/currency.ts +20 -0
- package/src/lib/utils/daisyuiColors.ts +243 -0
- package/src/lib/utils/dateFormatters.ts +153 -0
- package/src/lib/utils/mentionParser.ts +188 -0
- package/src/lib/utils/phoneFormat.ts +74 -0
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
EmojiSelector Component
|
|
3
|
+
|
|
4
|
+
Emoji picker with categories, search, skin tones, and custom emoji support.
|
|
5
|
+
Custom emojis and preferences are loaded via optional callbacks.
|
|
6
|
+
-->
|
|
7
|
+
|
|
8
|
+
<script lang="ts">
|
|
9
|
+
import { onMount, onDestroy } from 'svelte'
|
|
10
|
+
import type {
|
|
11
|
+
EmojiSelection,
|
|
12
|
+
EmojiCategory,
|
|
13
|
+
SkinTone,
|
|
14
|
+
StandardEmoji,
|
|
15
|
+
CustomEmoji,
|
|
16
|
+
EmojiPreferences,
|
|
17
|
+
EmojiCallbacks,
|
|
18
|
+
} from '../../types/messaging'
|
|
19
|
+
import { EMOJI_CATEGORIES, SKIN_TONES } from '../../types/messaging'
|
|
20
|
+
|
|
21
|
+
interface Props {
|
|
22
|
+
visible: boolean
|
|
23
|
+
position: { x: number; y: number }
|
|
24
|
+
onselect: (selection: EmojiSelection) => void
|
|
25
|
+
onclose: () => void
|
|
26
|
+
emojis?: StandardEmoji[]
|
|
27
|
+
customEmojis?: CustomEmoji[]
|
|
28
|
+
preferences?: EmojiPreferences
|
|
29
|
+
callbacks?: EmojiCallbacks
|
|
30
|
+
searchPlaceholder?: string
|
|
31
|
+
maxRecent?: number
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
let {
|
|
35
|
+
visible = false,
|
|
36
|
+
position = { x: 0, y: 0 },
|
|
37
|
+
onselect,
|
|
38
|
+
onclose,
|
|
39
|
+
emojis = [],
|
|
40
|
+
customEmojis = [],
|
|
41
|
+
preferences,
|
|
42
|
+
callbacks,
|
|
43
|
+
searchPlaceholder = 'Search emojis...',
|
|
44
|
+
maxRecent = 24,
|
|
45
|
+
}: Props = $props()
|
|
46
|
+
|
|
47
|
+
let searchQuery = $state('')
|
|
48
|
+
let activeCategory: EmojiCategory = $state('smileys_people')
|
|
49
|
+
let selectedSkinTone: SkinTone = $state(preferences?.skinTone || 'default')
|
|
50
|
+
let hoveredEmoji = $state<{ emoji: string; name: string; shortcode: string } | null>(null)
|
|
51
|
+
let loading = $state(false)
|
|
52
|
+
let selectorElement = $state<HTMLElement | null>(null)
|
|
53
|
+
let searchInput = $state<HTMLInputElement | null>(null)
|
|
54
|
+
|
|
55
|
+
// Category icons using emoji characters
|
|
56
|
+
const categoryEmojis: Record<string, string> = {
|
|
57
|
+
search: '🔍',
|
|
58
|
+
frequently_used: '🕐',
|
|
59
|
+
smileys_people: '😀',
|
|
60
|
+
animals_nature: '🐻',
|
|
61
|
+
food_drink: '🍎',
|
|
62
|
+
travel_places: '🏠',
|
|
63
|
+
activities: '⚽',
|
|
64
|
+
objects: '🔨',
|
|
65
|
+
symbols: '❤️',
|
|
66
|
+
flags: '🏳️',
|
|
67
|
+
custom: '🎨',
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Filter emojis by category
|
|
71
|
+
function getEmojisByCategory(category: EmojiCategory): StandardEmoji[] {
|
|
72
|
+
return emojis.filter((e) => e.category === category)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Search emojis
|
|
76
|
+
function searchEmojis(query: string): StandardEmoji[] {
|
|
77
|
+
const q = query.toLowerCase()
|
|
78
|
+
return emojis.filter(
|
|
79
|
+
(e) =>
|
|
80
|
+
e.name.toLowerCase().includes(q) ||
|
|
81
|
+
e.shortcode.toLowerCase().includes(q) ||
|
|
82
|
+
e.keywords.some((k) => k.toLowerCase().includes(q)),
|
|
83
|
+
)
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const displayEmojis = $derived(
|
|
87
|
+
searchQuery ? searchEmojis(searchQuery) : getEmojisByCategory(activeCategory),
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
function handleSelect(emoji: StandardEmoji) {
|
|
91
|
+
const selection: EmojiSelection = {
|
|
92
|
+
type: 'standard',
|
|
93
|
+
unicode: emoji.unicode,
|
|
94
|
+
name: emoji.name,
|
|
95
|
+
shortcode: emoji.shortcode,
|
|
96
|
+
skinTone: selectedSkinTone,
|
|
97
|
+
}
|
|
98
|
+
onselect(selection)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
function handleCustomSelect(custom: CustomEmoji) {
|
|
102
|
+
const selection: EmojiSelection = {
|
|
103
|
+
type: 'custom',
|
|
104
|
+
customEmojiId: custom.id,
|
|
105
|
+
name: custom.displayName,
|
|
106
|
+
shortcode: custom.name,
|
|
107
|
+
}
|
|
108
|
+
onselect(selection)
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Close on click outside
|
|
112
|
+
function handleClickOutside(event: MouseEvent) {
|
|
113
|
+
if (selectorElement && !selectorElement.contains(event.target as Node)) {
|
|
114
|
+
onclose()
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Close on escape
|
|
119
|
+
function handleKeydown(event: KeyboardEvent) {
|
|
120
|
+
if (event.key === 'Escape') onclose()
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
// Load custom emojis and preferences from callbacks on mount
|
|
124
|
+
$effect(() => {
|
|
125
|
+
if (visible && callbacks) {
|
|
126
|
+
loading = true
|
|
127
|
+
Promise.all([
|
|
128
|
+
callbacks.loadCustomEmojis?.().catch(() => []),
|
|
129
|
+
callbacks.loadPreferences?.().catch(() => null),
|
|
130
|
+
]).then(([loadedCustom, loadedPrefs]) => {
|
|
131
|
+
if (loadedCustom?.length) customEmojis = loadedCustom
|
|
132
|
+
if (loadedPrefs) {
|
|
133
|
+
selectedSkinTone = loadedPrefs.skinTone || 'default'
|
|
134
|
+
}
|
|
135
|
+
loading = false
|
|
136
|
+
})
|
|
137
|
+
}
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
$effect(() => {
|
|
141
|
+
if (visible) {
|
|
142
|
+
document.addEventListener('click', handleClickOutside)
|
|
143
|
+
document.addEventListener('keydown', handleKeydown)
|
|
144
|
+
// Focus search on open
|
|
145
|
+
setTimeout(() => searchInput?.focus(), 100)
|
|
146
|
+
return () => {
|
|
147
|
+
document.removeEventListener('click', handleClickOutside)
|
|
148
|
+
document.removeEventListener('keydown', handleKeydown)
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
// Visible categories (skip search and custom if no custom emojis)
|
|
154
|
+
const visibleCategories = $derived(
|
|
155
|
+
EMOJI_CATEGORIES.filter((cat) => {
|
|
156
|
+
if (cat.id === 'search') return false
|
|
157
|
+
if (cat.id === 'custom' && customEmojis.length === 0) return false
|
|
158
|
+
if (cat.id === 'frequently_used') return false // TODO: track frequent
|
|
159
|
+
return true
|
|
160
|
+
}),
|
|
161
|
+
)
|
|
162
|
+
</script>
|
|
163
|
+
|
|
164
|
+
{#if visible}
|
|
165
|
+
<div
|
|
166
|
+
bind:this={selectorElement}
|
|
167
|
+
class="fixed z-50 bg-base-100 border border-base-300 rounded-box shadow-xl w-88 flex flex-col"
|
|
168
|
+
style="left: {position.x}px; top: {position.y}px; max-height: 420px;"
|
|
169
|
+
>
|
|
170
|
+
<!-- Search -->
|
|
171
|
+
<div class="p-2 border-b border-base-300">
|
|
172
|
+
<input
|
|
173
|
+
bind:this={searchInput}
|
|
174
|
+
bind:value={searchQuery}
|
|
175
|
+
type="text"
|
|
176
|
+
placeholder={searchPlaceholder}
|
|
177
|
+
class="input input-sm input-bordered w-full"
|
|
178
|
+
/>
|
|
179
|
+
</div>
|
|
180
|
+
|
|
181
|
+
<!-- Category tabs -->
|
|
182
|
+
{#if !searchQuery}
|
|
183
|
+
<div class="flex items-center gap-0.5 px-2 py-1 border-b border-base-300 overflow-x-auto">
|
|
184
|
+
{#each visibleCategories as cat}
|
|
185
|
+
<button
|
|
186
|
+
class="btn btn-ghost btn-xs btn-square {activeCategory === cat.id ? 'bg-primary/10 text-primary' : ''}"
|
|
187
|
+
onclick={() => (activeCategory = cat.id)}
|
|
188
|
+
title={cat.name}
|
|
189
|
+
>
|
|
190
|
+
{categoryEmojis[cat.id] || '❓'}
|
|
191
|
+
</button>
|
|
192
|
+
{/each}
|
|
193
|
+
</div>
|
|
194
|
+
{/if}
|
|
195
|
+
|
|
196
|
+
<!-- Emoji grid -->
|
|
197
|
+
<div class="flex-1 overflow-y-auto p-2 min-h-[200px]">
|
|
198
|
+
{#if loading}
|
|
199
|
+
<div class="flex items-center justify-center h-full">
|
|
200
|
+
<span class="loading loading-spinner loading-sm"></span>
|
|
201
|
+
</div>
|
|
202
|
+
{:else if searchQuery && displayEmojis.length === 0}
|
|
203
|
+
<div class="flex items-center justify-center h-full text-base-content/50 text-sm">
|
|
204
|
+
No emojis found
|
|
205
|
+
</div>
|
|
206
|
+
{:else if activeCategory === 'custom' && !searchQuery}
|
|
207
|
+
<!-- Custom emojis grid -->
|
|
208
|
+
<div class="grid grid-cols-8 gap-1">
|
|
209
|
+
{#each customEmojis as custom}
|
|
210
|
+
<button
|
|
211
|
+
class="w-8 h-8 flex items-center justify-center rounded hover:bg-base-200 transition-colors text-lg"
|
|
212
|
+
onclick={() => handleCustomSelect(custom)}
|
|
213
|
+
onmouseenter={() => (hoveredEmoji = { emoji: '🎨', name: custom.displayName, shortcode: custom.name })}
|
|
214
|
+
onmouseleave={() => (hoveredEmoji = null)}
|
|
215
|
+
title={custom.displayName}
|
|
216
|
+
>
|
|
217
|
+
🎨
|
|
218
|
+
</button>
|
|
219
|
+
{/each}
|
|
220
|
+
</div>
|
|
221
|
+
{:else}
|
|
222
|
+
<div class="grid grid-cols-8 gap-1">
|
|
223
|
+
{#each displayEmojis as emoji}
|
|
224
|
+
<button
|
|
225
|
+
class="w-8 h-8 flex items-center justify-center rounded hover:bg-base-200 transition-colors text-lg"
|
|
226
|
+
onclick={() => handleSelect(emoji)}
|
|
227
|
+
onmouseenter={() => (hoveredEmoji = { emoji: emoji.unicode, name: emoji.name, shortcode: emoji.shortcode })}
|
|
228
|
+
onmouseleave={() => (hoveredEmoji = null)}
|
|
229
|
+
title="{emoji.name} :{emoji.shortcode}:"
|
|
230
|
+
>
|
|
231
|
+
{emoji.unicode}
|
|
232
|
+
</button>
|
|
233
|
+
{/each}
|
|
234
|
+
</div>
|
|
235
|
+
{/if}
|
|
236
|
+
</div>
|
|
237
|
+
|
|
238
|
+
<!-- Skin tone selector -->
|
|
239
|
+
<div class="flex items-center justify-between px-2 py-1 border-t border-base-300">
|
|
240
|
+
<div class="flex items-center gap-1">
|
|
241
|
+
{#each SKIN_TONES as tone}
|
|
242
|
+
<button
|
|
243
|
+
class="w-6 h-6 flex items-center justify-center rounded text-sm {selectedSkinTone === tone.id ? 'bg-primary/20 ring-1 ring-primary' : 'hover:bg-base-200'}"
|
|
244
|
+
onclick={() => (selectedSkinTone = tone.id)}
|
|
245
|
+
title={tone.name}
|
|
246
|
+
>
|
|
247
|
+
{tone.preview}
|
|
248
|
+
</button>
|
|
249
|
+
{/each}
|
|
250
|
+
</div>
|
|
251
|
+
|
|
252
|
+
<!-- Hovered emoji info -->
|
|
253
|
+
{#if hoveredEmoji}
|
|
254
|
+
<div class="text-xs text-base-content/60 truncate max-w-[150px]">
|
|
255
|
+
{hoveredEmoji.emoji} :{hoveredEmoji.shortcode}:
|
|
256
|
+
</div>
|
|
257
|
+
{/if}
|
|
258
|
+
</div>
|
|
259
|
+
</div>
|
|
260
|
+
{/if}
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
MentionAutocomplete Component
|
|
3
|
+
|
|
4
|
+
Shows search results for @-mention autocomplete.
|
|
5
|
+
Receives search results via callback — no direct API calls.
|
|
6
|
+
-->
|
|
7
|
+
|
|
8
|
+
<script lang="ts">
|
|
9
|
+
import { onMount, onDestroy } from 'svelte'
|
|
10
|
+
import type { MentionItem } from '../../types/messaging'
|
|
11
|
+
|
|
12
|
+
interface Props {
|
|
13
|
+
visible: boolean
|
|
14
|
+
query: string
|
|
15
|
+
position: { x: number; y: number }
|
|
16
|
+
onsearch: (query: string) => Promise<MentionItem[]>
|
|
17
|
+
onselect?: (data: { result: MentionItem }) => void
|
|
18
|
+
onclose?: () => void
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
let { visible, query, position, onsearch, onselect, onclose }: Props = $props()
|
|
22
|
+
|
|
23
|
+
let results = $state<MentionItem[]>([])
|
|
24
|
+
let loading = $state(false)
|
|
25
|
+
let selectedIndex = $state(0)
|
|
26
|
+
let dropdownElement = $state<HTMLElement | undefined>(undefined)
|
|
27
|
+
let searchTimeout: ReturnType<typeof setTimeout>
|
|
28
|
+
|
|
29
|
+
// Debounced search
|
|
30
|
+
$effect(() => {
|
|
31
|
+
if (visible && query && query.length >= 2) {
|
|
32
|
+
clearTimeout(searchTimeout)
|
|
33
|
+
searchTimeout = setTimeout(async () => {
|
|
34
|
+
loading = true
|
|
35
|
+
try {
|
|
36
|
+
results = await onsearch(query)
|
|
37
|
+
selectedIndex = 0
|
|
38
|
+
} catch {
|
|
39
|
+
results = []
|
|
40
|
+
} finally {
|
|
41
|
+
loading = false
|
|
42
|
+
}
|
|
43
|
+
}, 300)
|
|
44
|
+
} else if (!visible || query.length < 2) {
|
|
45
|
+
results = []
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return () => clearTimeout(searchTimeout)
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
function handleKeyDown(event: KeyboardEvent) {
|
|
52
|
+
if (!visible || results.length === 0) return
|
|
53
|
+
|
|
54
|
+
switch (event.key) {
|
|
55
|
+
case 'ArrowDown':
|
|
56
|
+
event.preventDefault()
|
|
57
|
+
selectedIndex = Math.min(selectedIndex + 1, results.length - 1)
|
|
58
|
+
break
|
|
59
|
+
case 'ArrowUp':
|
|
60
|
+
event.preventDefault()
|
|
61
|
+
selectedIndex = Math.max(selectedIndex - 1, 0)
|
|
62
|
+
break
|
|
63
|
+
case 'Enter':
|
|
64
|
+
case 'Tab':
|
|
65
|
+
event.preventDefault()
|
|
66
|
+
if (results[selectedIndex]) {
|
|
67
|
+
onselect?.({ result: results[selectedIndex] })
|
|
68
|
+
}
|
|
69
|
+
break
|
|
70
|
+
case 'Escape':
|
|
71
|
+
event.preventDefault()
|
|
72
|
+
onclose?.()
|
|
73
|
+
break
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function handleSelect(result: MentionItem) {
|
|
78
|
+
onselect?.({ result })
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function handleClickOutside(event: MouseEvent) {
|
|
82
|
+
const target = event.target as Node
|
|
83
|
+
if (dropdownElement?.contains(target)) return
|
|
84
|
+
if (
|
|
85
|
+
target instanceof HTMLTextAreaElement ||
|
|
86
|
+
target instanceof HTMLInputElement ||
|
|
87
|
+
(target as Element).closest?.('[contenteditable]')
|
|
88
|
+
) {
|
|
89
|
+
return
|
|
90
|
+
}
|
|
91
|
+
onclose?.()
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
onMount(() => {
|
|
95
|
+
document.addEventListener('keydown', handleKeyDown)
|
|
96
|
+
})
|
|
97
|
+
|
|
98
|
+
onDestroy(() => {
|
|
99
|
+
document.removeEventListener('keydown', handleKeyDown)
|
|
100
|
+
document.removeEventListener('click', handleClickOutside)
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
$effect(() => {
|
|
104
|
+
if (visible) {
|
|
105
|
+
const timeoutId = setTimeout(() => {
|
|
106
|
+
document.addEventListener('click', handleClickOutside)
|
|
107
|
+
}, 100)
|
|
108
|
+
return () => {
|
|
109
|
+
clearTimeout(timeoutId)
|
|
110
|
+
document.removeEventListener('click', handleClickOutside)
|
|
111
|
+
}
|
|
112
|
+
} else {
|
|
113
|
+
document.removeEventListener('click', handleClickOutside)
|
|
114
|
+
}
|
|
115
|
+
})
|
|
116
|
+
|
|
117
|
+
let dropdownStyle = $state('')
|
|
118
|
+
|
|
119
|
+
$effect(() => {
|
|
120
|
+
if (!visible) {
|
|
121
|
+
dropdownStyle = 'display: none;'
|
|
122
|
+
return
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const maxHeight = 400
|
|
126
|
+
const itemHeight = 60
|
|
127
|
+
const footerHeight = results.length > 0 ? 40 : 0
|
|
128
|
+
const estimatedHeight = Math.min(
|
|
129
|
+
maxHeight,
|
|
130
|
+
Math.max(120, results.length * itemHeight + footerHeight + 20),
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
let top = position.y - estimatedHeight - 24
|
|
134
|
+
const left = position.x
|
|
135
|
+
|
|
136
|
+
if (top < 10) {
|
|
137
|
+
top = position.y + 20
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
dropdownStyle = `position: fixed; top: ${top}px; left: ${left}px; z-index: 1000; max-height: ${maxHeight}px; width: 320px;`
|
|
141
|
+
})
|
|
142
|
+
</script>
|
|
143
|
+
|
|
144
|
+
{#if visible}
|
|
145
|
+
<div
|
|
146
|
+
bind:this={dropdownElement}
|
|
147
|
+
class="bg-base-100 border border-base-300 rounded-box shadow-lg overflow-y-auto overflow-x-hidden flex flex-col"
|
|
148
|
+
style={dropdownStyle}
|
|
149
|
+
>
|
|
150
|
+
{#if loading}
|
|
151
|
+
<div class="p-4 flex items-center justify-center">
|
|
152
|
+
<span class="loading loading-spinner loading-sm mr-2"></span>
|
|
153
|
+
<span class="text-sm text-base-content/70">Searching...</span>
|
|
154
|
+
</div>
|
|
155
|
+
{:else if results.length === 0}
|
|
156
|
+
<div class="p-4 text-center">
|
|
157
|
+
<span class="text-sm text-base-content/50">
|
|
158
|
+
{query.length < 2 ? 'Type to search...' : `No results found for "${query}"`}
|
|
159
|
+
</span>
|
|
160
|
+
</div>
|
|
161
|
+
{:else}
|
|
162
|
+
{#each results as result, index}
|
|
163
|
+
<button
|
|
164
|
+
type="button"
|
|
165
|
+
class="flex items-center gap-3 p-4 text-left hover:bg-base-200 transition-colors w-full border-b border-base-200 last:border-b-0 min-h-[60px] flex-shrink-0
|
|
166
|
+
{selectedIndex === index ? 'bg-primary/10 text-primary' : 'text-base-content'}"
|
|
167
|
+
onclick={() => handleSelect(result)}
|
|
168
|
+
>
|
|
169
|
+
<div class="flex-1 min-w-0">
|
|
170
|
+
<div class="font-medium text-sm truncate">{result.name}</div>
|
|
171
|
+
{#if result.secondary}
|
|
172
|
+
<div class="text-xs text-base-content/60 truncate">{result.secondary}</div>
|
|
173
|
+
{/if}
|
|
174
|
+
</div>
|
|
175
|
+
|
|
176
|
+
<div class="flex-shrink-0">
|
|
177
|
+
<span class="badge badge-ghost badge-xs">{result.type}</span>
|
|
178
|
+
</div>
|
|
179
|
+
</button>
|
|
180
|
+
{/each}
|
|
181
|
+
{/if}
|
|
182
|
+
|
|
183
|
+
{#if results.length > 0}
|
|
184
|
+
<div class="px-3 py-2 border-t border-base-300 bg-base-50 flex-shrink-0">
|
|
185
|
+
<div class="text-xs text-base-content/50 flex justify-between">
|
|
186
|
+
<span>↑↓ Navigate</span>
|
|
187
|
+
<span>Tab/Enter Select</span>
|
|
188
|
+
<span>Esc Close</span>
|
|
189
|
+
</div>
|
|
190
|
+
</div>
|
|
191
|
+
{/if}
|
|
192
|
+
</div>
|
|
193
|
+
{/if}
|