@joewinke/jatui 0.1.22 → 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 -134
  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,452 +0,0 @@
1
- <!--
2
- MessageInput Component
3
-
4
- Rich text input for composing and sending messages.
5
- Supports mentions, attachments, and drag/drop.
6
- All searches and uploads are via callbacks.
7
- -->
8
-
9
- <script lang="ts">
10
- import type { QuotedMessage, PendingAttachment, MentionItem, AttachmentCallbacks } from '../../types/messaging'
11
- import {
12
- findPartialMention,
13
- createMentionString,
14
- replaceMentionInContent,
15
- renderMentionsAsHTML,
16
- extractPlainText,
17
- parseMentions,
18
- } from '../../utils/mentionParser'
19
- import MentionAutocomplete from './MentionAutocomplete.svelte'
20
- import QuotedMessageDisplay from './QuotedMessageDisplay.svelte'
21
-
22
- interface Props {
23
- placeholder?: string
24
- disabled?: boolean
25
- maxLength?: number
26
- compact?: boolean
27
- quotedMessage?: QuotedMessage
28
- showPrioritySelector?: boolean
29
- attachmentCallbacks?: AttachmentCallbacks
30
- organizationId?: string
31
- onsend?: (data: {
32
- content: string
33
- priority?: string
34
- quotedMessageId?: string
35
- attachments?: PendingAttachment[]
36
- }) => void
37
- ontyping?: (data: { isTyping: boolean }) => void
38
- onquoteremove?: () => void
39
- onsearchmentions?: (query: string) => Promise<MentionItem[]>
40
- }
41
-
42
- let {
43
- placeholder = 'Type a message...',
44
- disabled = false,
45
- maxLength = 4000,
46
- compact = false,
47
- quotedMessage,
48
- showPrioritySelector = false,
49
- attachmentCallbacks,
50
- organizationId,
51
- onsend,
52
- ontyping,
53
- onquoteremove,
54
- onsearchmentions,
55
- }: Props = $props()
56
-
57
- let selectedPriority = $state<'low' | 'normal' | 'high' | 'urgent'>('normal')
58
- let attachments = $state<PendingAttachment[]>([])
59
- let isDragging = $state(false)
60
- let fileInputRef = $state<HTMLInputElement | null>(null)
61
- let inputText = $state('')
62
- let contentEditableDiv: HTMLDivElement
63
- let isTyping = $state(false)
64
- let typingTimeout: ReturnType<typeof setTimeout>
65
-
66
- let showMentionAutocomplete = $state(false)
67
- let mentionQuery = $state('')
68
- let mentionStartIndex = $state(0)
69
- let cursorPosition = $state(0)
70
- let autocompletePosition = $state({ x: 0, y: 0 })
71
- let isInternalUpdate = false
72
-
73
- function autoResize() {
74
- if (contentEditableDiv) {
75
- const maxHeight = compact ? 120 : 200
76
- if (contentEditableDiv.scrollHeight > maxHeight) {
77
- contentEditableDiv.style.maxHeight = maxHeight + 'px'
78
- contentEditableDiv.style.overflowY = 'auto'
79
- } else {
80
- contentEditableDiv.style.maxHeight = 'none'
81
- contentEditableDiv.style.overflowY = 'visible'
82
- }
83
- }
84
- }
85
-
86
- export function focus() {
87
- if (contentEditableDiv) {
88
- contentEditableDiv.focus()
89
- const selection = window.getSelection()
90
- if (selection) {
91
- const range = document.createRange()
92
- range.selectNodeContents(contentEditableDiv)
93
- range.collapse(false)
94
- selection.removeAllRanges()
95
- selection.addRange(range)
96
- }
97
- }
98
- }
99
-
100
- export async function addFiles(files: FileList | File[]) {
101
- await processFiles(files)
102
- }
103
-
104
- function handleInput() {
105
- if (!contentEditableDiv || isInternalUpdate) return
106
-
107
- const newText = contentEditableDiv.innerText || ''
108
- const updatedInputText = reconstructMarkdownWithNewText(inputText, newText)
109
- inputText = updatedInputText
110
-
111
- const hasMentions = inputText.includes('[@')
112
- if (hasMentions) {
113
- isInternalUpdate = true
114
- const renderedHTML = renderMentionsWithDegradedStyling(inputText)
115
- if (contentEditableDiv.innerHTML !== renderedHTML) {
116
- const selection = window.getSelection()
117
- const cursorOffset = selection?.rangeCount ? getAbsoluteCursorPosition(selection.getRangeAt(0)) : 0
118
- contentEditableDiv.innerHTML = renderedHTML
119
- setContentEditableCursor(cursorOffset)
120
- }
121
- isInternalUpdate = false
122
- }
123
-
124
- autoResize()
125
- updateCursorPosition()
126
- checkForMentions()
127
-
128
- if (!isTyping && inputText.trim()) {
129
- isTyping = true
130
- ontyping?.({ isTyping: true })
131
- }
132
- if (typingTimeout) clearTimeout(typingTimeout)
133
- typingTimeout = setTimeout(() => { isTyping = false; ontyping?.({ isTyping: false }) }, 3000)
134
- }
135
-
136
- function reconstructMarkdownWithNewText(currentMarkdown: string, newPlainText: string): string {
137
- const existingMentions = parseMentions(currentMarkdown)
138
- if (existingMentions.length === 0) return newPlainText
139
-
140
- const currentPlainText = extractPlainText(currentMarkdown)
141
- if (newPlainText.startsWith(currentPlainText)) return currentMarkdown + newPlainText.substring(currentPlainText.length)
142
- if (currentPlainText.startsWith(newPlainText)) {
143
- const deletedLength = currentPlainText.length - newPlainText.length
144
- return currentMarkdown.substring(0, currentMarkdown.length - deletedLength)
145
- }
146
-
147
- // Complex edit — try to preserve mentions
148
- const mentionMap = new Map<string, string>()
149
- existingMentions.forEach((m) => {
150
- mentionMap.set(`@${m.displayName}`, `[@${m.displayName}](${m.type}:${m.id})`)
151
- })
152
- let result = newPlainText
153
- mentionMap.forEach((markdownSyntax, displayName) => {
154
- if (result.includes(displayName)) result = result.replace(displayName, markdownSyntax)
155
- })
156
- return result
157
- }
158
-
159
- function updateCursorPosition() {
160
- if (contentEditableDiv) {
161
- const selection = window.getSelection()
162
- if (selection && selection.rangeCount > 0) cursorPosition = getAbsoluteCursorPosition(selection.getRangeAt(0))
163
- }
164
- }
165
-
166
- function getAbsoluteCursorPosition(range: Range): number {
167
- const walker = document.createTreeWalker(contentEditableDiv, NodeFilter.SHOW_TEXT)
168
- let pos = 0
169
- let node = walker.nextNode()
170
- while (node) {
171
- if (node === range.startContainer) return pos + range.startOffset
172
- pos += node.textContent?.length || 0
173
- node = walker.nextNode()
174
- }
175
- return pos
176
- }
177
-
178
- function checkForMentions() {
179
- const partialMention = findPartialMention(inputText, cursorPosition)
180
- if (partialMention) {
181
- mentionStartIndex = partialMention.start
182
- mentionQuery = partialMention.query
183
- updateAutocompletePosition()
184
- showMentionAutocomplete = true
185
- } else {
186
- showMentionAutocomplete = false
187
- }
188
- }
189
-
190
- function renderMentionsWithDegradedStyling(content: string): string {
191
- let result = renderMentionsAsHTML(content)
192
- const degradedMentionRegex = /(?<!>)[@$#!~>/+]\w+(?![^<]*<\/a>)/g
193
- result = result.replace(degradedMentionRegex, (match) => `<span class="degraded-mention">${match}</span>`)
194
- return result
195
- }
196
-
197
- function updateAutocompletePosition() {
198
- if (!contentEditableDiv) return
199
- const selection = window.getSelection()
200
- if (!selection || selection.rangeCount === 0) return
201
- try {
202
- const range = selection.getRangeAt(0)
203
- const rect = range.getBoundingClientRect()
204
- autocompletePosition = { x: rect.left, y: rect.bottom + window.scrollY }
205
- } catch {
206
- const rect = contentEditableDiv.getBoundingClientRect()
207
- autocompletePosition = { x: rect.left, y: rect.bottom + window.scrollY }
208
- }
209
- }
210
-
211
- function handleKeyDown(event: KeyboardEvent) {
212
- if (showMentionAutocomplete && ['ArrowUp', 'ArrowDown', 'Tab', 'Enter', 'Escape'].includes(event.key)) return
213
- if (event.key === 'Enter' && !event.shiftKey) { event.preventDefault(); sendMessage() }
214
- }
215
-
216
- function handleMentionSelect(event: { result: MentionItem }) {
217
- const { result } = event
218
- const mentionString = createMentionString(result.type, result.id, result.name) + ' '
219
- const endIndex = mentionStartIndex + 1 + mentionQuery.length
220
- const { newContent, newCursorPosition } = replaceMentionInContent(inputText, mentionStartIndex, endIndex, mentionString)
221
-
222
- isInternalUpdate = true
223
- inputText = newContent
224
- if (contentEditableDiv) {
225
- contentEditableDiv.innerHTML = renderMentionsWithDegradedStyling(inputText)
226
- setTimeout(() => {
227
- setContentEditableCursor(newCursorPosition)
228
- contentEditableDiv.focus()
229
- setTimeout(() => { isInternalUpdate = false; updateCursorPosition() }, 10)
230
- }, 0)
231
- }
232
- showMentionAutocomplete = false
233
- }
234
-
235
- function setContentEditableCursor(position: number) {
236
- if (!contentEditableDiv) return
237
- const textContent = contentEditableDiv.innerText || ''
238
- if (position >= textContent.length - 1) {
239
- const range = document.createRange()
240
- const selection = window.getSelection()
241
- try { range.selectNodeContents(contentEditableDiv); range.collapse(false); selection?.removeAllRanges(); selection?.addRange(range) } catch {}
242
- return
243
- }
244
- const walker = document.createTreeWalker(contentEditableDiv, NodeFilter.SHOW_TEXT)
245
- let currentPos = 0
246
- while (walker.nextNode()) {
247
- const node = walker.currentNode
248
- const nodeLength = node.textContent?.length || 0
249
- if (currentPos + nodeLength >= position) {
250
- const range = document.createRange()
251
- const selection = window.getSelection()
252
- try {
253
- const offset = Math.min(position - currentPos, nodeLength)
254
- range.setStart(node, offset); range.setEnd(node, offset)
255
- selection?.removeAllRanges(); selection?.addRange(range)
256
- } catch {}
257
- return
258
- }
259
- currentPos += nodeLength
260
- }
261
- }
262
-
263
- function sendMessage() {
264
- const content = inputText.trim()
265
- if ((!content && attachments.length === 0) || disabled) return
266
-
267
- onsend?.({
268
- content,
269
- priority: showPrioritySelector ? selectedPriority : undefined,
270
- quotedMessageId: quotedMessage?.id,
271
- attachments: attachments.length > 0 ? attachments : undefined,
272
- })
273
-
274
- inputText = ''
275
- selectedPriority = 'normal'
276
- attachments = []
277
- if (contentEditableDiv) contentEditableDiv.innerHTML = ''
278
- autoResize()
279
- if (quotedMessage) onquoteremove?.()
280
- if (isTyping) { isTyping = false; ontyping?.({ isTyping: false }) }
281
- if (typingTimeout) clearTimeout(typingTimeout)
282
- }
283
-
284
- function handleFileUpload() { fileInputRef?.click() }
285
-
286
- function handleFileSelect(e: Event) {
287
- const input = e.target as HTMLInputElement
288
- if (input.files && input.files.length > 0) { processFiles(input.files); input.value = '' }
289
- }
290
-
291
- async function processFiles(files: FileList | File[]) {
292
- if (disabled) return
293
- const fileArray = Array.from(files)
294
- const acceptedTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp', 'video/mp4', 'video/webm', 'video/quicktime', 'application/pdf']
295
- const maxSize = 25 * 1024 * 1024
296
-
297
- for (const file of fileArray) {
298
- if (!acceptedTypes.includes(file.type) || file.size > maxSize) continue
299
- let preview: string | undefined
300
- if (file.type.startsWith('image/')) {
301
- preview = await new Promise<string | undefined>((resolve) => {
302
- const reader = new FileReader()
303
- reader.onload = (e) => resolve(e.target?.result as string)
304
- reader.onerror = () => resolve(undefined)
305
- reader.readAsDataURL(file)
306
- })
307
- }
308
- attachments = [...attachments, { id: crypto.randomUUID(), file, preview, uploading: false, progress: 0 }]
309
- }
310
- }
311
-
312
- function removeAttachment(id: string) { attachments = attachments.filter((a) => a.id !== id) }
313
-
314
- function handleDragEnter(e: DragEvent) { e.preventDefault(); e.stopPropagation(); isDragging = true }
315
- function handleDragLeave(e: DragEvent) {
316
- e.preventDefault(); e.stopPropagation()
317
- const rect = (e.currentTarget as HTMLElement).getBoundingClientRect()
318
- if (e.clientX <= rect.left || e.clientX >= rect.right || e.clientY <= rect.top || e.clientY >= rect.bottom) isDragging = false
319
- }
320
- function handleDragOver(e: DragEvent) { e.preventDefault(); e.stopPropagation() }
321
- async function handleDrop(e: DragEvent) {
322
- e.preventDefault(); e.stopPropagation(); isDragging = false
323
- if (e.dataTransfer?.files && e.dataTransfer.files.length > 0) await processFiles(e.dataTransfer.files)
324
- }
325
-
326
- function getFileTypeIcon(file: File): string { if (file.type.startsWith('image/')) return '🖼'; if (file.type.startsWith('video/')) return '🎬'; return '📄' }
327
-
328
- const characterCount = $derived(inputText.length)
329
- const showCharacterCount = $derived(characterCount > maxLength * 0.8)
330
- const isOverLimit = $derived(characterCount > maxLength)
331
-
332
- $effect(() => { if (inputText) autoResize() })
333
- </script>
334
-
335
- <input bind:this={fileInputRef} type="file" accept="image/jpeg,image/png,image/gif,image/webp,video/mp4,video/webm,video/quicktime,application/pdf" multiple class="hidden" onchange={handleFileSelect} {disabled} />
336
-
337
- <!-- svelte-ignore a11y_no_static_element_interactions -->
338
- <div class="flex flex-col gap-2 relative" ondragenter={handleDragEnter} ondragleave={handleDragLeave} ondragover={handleDragOver} ondrop={handleDrop}>
339
- {#if isDragging}
340
- <div class="absolute inset-0 z-50 bg-primary/10 border-2 border-dashed border-primary rounded-lg flex items-center justify-center pointer-events-none">
341
- <div class="text-center">
342
- <div class="text-2xl mb-2">📎</div>
343
- <p class="text-sm font-medium text-primary">Drop files to attach</p>
344
- </div>
345
- </div>
346
- {/if}
347
-
348
- {#if quotedMessage}
349
- <QuotedMessageDisplay {quotedMessage} mode="preview" onremove={onquoteremove} />
350
- {/if}
351
-
352
- {#if attachments.length > 0}
353
- <div class="flex flex-wrap gap-2 p-2 bg-base-200 rounded-lg">
354
- {#each attachments as att (att.id)}
355
- <div class="relative group bg-base-100 rounded-lg overflow-hidden border border-base-300 w-16 h-16">
356
- {#if att.preview}
357
- <img src={att.preview} alt={att.file.name} class="w-full h-full object-cover" />
358
- {:else}
359
- <div class="w-full h-full flex items-center justify-center bg-base-200">
360
- <span class="text-xl">{getFileTypeIcon(att.file)}</span>
361
- </div>
362
- {/if}
363
- <button type="button" class="absolute top-0.5 right-0.5 btn btn-circle btn-xs bg-base-100/80 hover:bg-error hover:text-error-content opacity-0 group-hover:opacity-100 transition-opacity" onclick={() => removeAttachment(att.id)} title="Remove">✕</button>
364
- {#if att.uploading}
365
- <div class="absolute inset-0 bg-[color-mix(in_oklch,#180042_55%,transparent)] flex items-center justify-center"><span class="loading loading-spinner loading-xs text-white"></span></div>
366
- {/if}
367
- </div>
368
- {/each}
369
- </div>
370
- {/if}
371
-
372
- {#if showCharacterCount}
373
- <div class="text-right">
374
- <span class="text-xs" class:text-warning={characterCount > maxLength * 0.9} class:text-error={isOverLimit}>{characterCount} / {maxLength}</span>
375
- </div>
376
- {/if}
377
-
378
- <div class="flex items-end gap-2 {compact ? 'p-2' : 'p-3'} border border-base-300 rounded-lg focus-within:border-primary transition-colors">
379
- {#if !compact}
380
- <button type="button" class="btn btn-ghost btn-sm btn-circle flex-shrink-0" onclick={handleFileUpload} title="Attach file" {disabled}>📎</button>
381
- {/if}
382
-
383
- <div
384
- bind:this={contentEditableDiv}
385
- contenteditable={!disabled}
386
- class="flex-1 bg-transparent border-none outline-none {compact ? 'min-h-[20px]' : 'min-h-[24px]'} leading-6 message-content"
387
- class:text-error={isOverLimit}
388
- class:opacity-50={disabled}
389
- class:pointer-events-none={disabled}
390
- data-placeholder={placeholder}
391
- style="max-height: {compact ? '120px' : '200px'}; overflow-y: auto;"
392
- oninput={handleInput}
393
- onkeydown={handleKeyDown}
394
- onclick={updateCursorPosition}
395
- onkeyup={updateCursorPosition}
396
- onpaste={(e) => {
397
- e.preventDefault()
398
- const text = e.clipboardData?.getData('text/plain') || ''
399
- const selection = window.getSelection()
400
- if (selection && selection.rangeCount > 0) {
401
- const range = selection.getRangeAt(0)
402
- range.deleteContents()
403
- range.insertNode(document.createTextNode(text))
404
- range.collapse(false)
405
- }
406
- handleInput()
407
- }}
408
- aria-label={placeholder}
409
- role="textbox"
410
- aria-multiline="true"
411
- tabindex="0"
412
- ></div>
413
-
414
- {#if showPrioritySelector}
415
- <select bind:value={selectedPriority} class="select select-sm {selectedPriority === 'urgent' ? 'select-error' : selectedPriority === 'high' ? 'select-warning' : 'select-ghost'} flex-shrink-0 max-w-[100px]" {disabled}>
416
- <option value="normal">Normal</option>
417
- <option value="high">High</option>
418
- <option value="urgent">Urgent</option>
419
- </select>
420
- {/if}
421
-
422
- <button type="button" class="btn btn-primary {compact ? 'btn-xs' : 'btn-sm'} btn-circle flex-shrink-0" onclick={sendMessage} disabled={disabled || (!inputText.trim() && attachments.length === 0) || isOverLimit} title="Send message (Enter)">
423
- ➤
424
- </button>
425
- </div>
426
-
427
- <div class="text-xs text-base-content/50">
428
- <span class="font-medium">Enter</span> to send •
429
- <span class="font-medium">Shift + Enter</span> for new line •
430
- <span class="font-medium">@ $ # +</span> to mention
431
- </div>
432
- </div>
433
-
434
- {#if onsearchmentions}
435
- <MentionAutocomplete
436
- query={mentionQuery}
437
- position={autocompletePosition}
438
- visible={showMentionAutocomplete}
439
- onsearch={onsearchmentions}
440
- onselect={handleMentionSelect}
441
- onclose={() => (showMentionAutocomplete = false)}
442
- />
443
- {/if}
444
-
445
- <style>
446
- div[contenteditable='true'] { white-space: pre-wrap; word-wrap: break-word; overflow-wrap: break-word; }
447
- div[contenteditable='true']:empty:before { content: attr(data-placeholder); color: oklch(var(--bc) / 0.5); pointer-events: none; }
448
- div[contenteditable='true']:focus:empty:before { content: attr(data-placeholder); color: oklch(var(--bc) / 0.3); }
449
- div[contenteditable='true']::-webkit-scrollbar { width: 6px; }
450
- div[contenteditable='true']::-webkit-scrollbar-track { background: transparent; }
451
- div[contenteditable='true']::-webkit-scrollbar-thumb { background-color: oklch(var(--bc) / 0.2); border-radius: 3px; }
452
- </style>