@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,234 +0,0 @@
1
- <!--
2
- NotificationSettingsModal Component
3
-
4
- Manages user notification preferences for messaging.
5
- Supports both global settings and per-channel preferences.
6
- All operations via callbacks.
7
- -->
8
-
9
- <script lang="ts">
10
- import type { NotificationCallbacks } from '../../types/messaging'
11
-
12
- interface GlobalSettings {
13
- enable_notifications: boolean
14
- enable_sound: boolean
15
- enable_desktop: boolean
16
- quiet_hours_enabled: boolean
17
- quiet_hours_start: string
18
- quiet_hours_end: string
19
- weekend_notifications: boolean
20
- show_typing_indicators: boolean
21
- show_read_receipts: boolean
22
- }
23
-
24
- interface ChannelPreferences {
25
- muted: boolean
26
- mute_until?: string
27
- notification_level: 'all' | 'mentions' | 'none'
28
- desktop_notifications: boolean
29
- sound_enabled: boolean
30
- }
31
-
32
- interface Props {
33
- type: 'global' | 'channel'
34
- channelId?: string
35
- channelName?: string
36
- open: boolean
37
- notificationCallbacks: NotificationCallbacks
38
- onclose?: () => void
39
- onsettingsupdate?: () => void
40
- }
41
-
42
- let {
43
- type,
44
- channelId,
45
- channelName,
46
- open = false,
47
- notificationCallbacks,
48
- onclose,
49
- onsettingsupdate,
50
- }: Props = $props()
51
-
52
- let loading = $state(true)
53
- let saving = $state(false)
54
- let error = $state<string | null>(null)
55
-
56
- // Global settings
57
- let globalSettings = $state<GlobalSettings>({
58
- enable_notifications: true,
59
- enable_sound: true,
60
- enable_desktop: false,
61
- quiet_hours_enabled: false,
62
- quiet_hours_start: '22:00',
63
- quiet_hours_end: '08:00',
64
- weekend_notifications: true,
65
- show_typing_indicators: true,
66
- show_read_receipts: true,
67
- })
68
-
69
- // Channel preferences
70
- let channelPrefs = $state<ChannelPreferences>({
71
- muted: false,
72
- notification_level: 'all',
73
- desktop_notifications: true,
74
- sound_enabled: true,
75
- })
76
-
77
- // Load settings when modal opens
78
- $effect(() => {
79
- if (open) {
80
- loadSettings()
81
- }
82
- })
83
-
84
- async function loadSettings() {
85
- loading = true
86
- error = null
87
- try {
88
- const settings = await notificationCallbacks.loadSettings({ type, channelId })
89
- if (type === 'global' && settings) {
90
- globalSettings = { ...globalSettings, ...settings } as unknown as GlobalSettings
91
- } else if (type === 'channel' && settings) {
92
- channelPrefs = { ...channelPrefs, ...settings } as unknown as ChannelPreferences
93
- }
94
- } catch (err) {
95
- error = err instanceof Error ? err.message : 'Failed to load settings'
96
- } finally {
97
- loading = false
98
- }
99
- }
100
-
101
- async function saveSettings() {
102
- saving = true
103
- error = null
104
- try {
105
- const settings = type === 'global' ? globalSettings : channelPrefs
106
- await notificationCallbacks.saveSettings(
107
- { type, channelId },
108
- settings as any,
109
- )
110
- onsettingsupdate?.()
111
- onclose?.()
112
- } catch (err) {
113
- error = err instanceof Error ? err.message : 'Failed to save settings'
114
- } finally {
115
- saving = false
116
- }
117
- }
118
- </script>
119
-
120
- {#if open}
121
- <dialog class="modal modal-open">
122
- <div class="modal-box max-w-md">
123
- <div class="flex items-center justify-between mb-4">
124
- <h3 class="font-semibold">
125
- {type === 'global' ? 'Notification Settings' : `Notifications: #${channelName}`}
126
- </h3>
127
- <button class="btn btn-ghost btn-sm btn-circle" onclick={onclose}>✕</button>
128
- </div>
129
-
130
- {#if error}
131
- <div class="alert alert-error mb-4"><span class="text-sm">{error}</span></div>
132
- {/if}
133
-
134
- {#if loading}
135
- <div class="flex justify-center py-8">
136
- <span class="loading loading-spinner loading-md"></span>
137
- </div>
138
- {:else if type === 'global'}
139
- <div class="space-y-4">
140
- <div class="form-control">
141
- <label class="label cursor-pointer">
142
- <span class="label-text">Enable Notifications</span>
143
- <input type="checkbox" bind:checked={globalSettings.enable_notifications} class="toggle toggle-primary" />
144
- </label>
145
- </div>
146
- <div class="form-control">
147
- <label class="label cursor-pointer">
148
- <span class="label-text">Sound</span>
149
- <input type="checkbox" bind:checked={globalSettings.enable_sound} class="toggle toggle-primary" />
150
- </label>
151
- </div>
152
- <div class="form-control">
153
- <label class="label cursor-pointer">
154
- <span class="label-text">Desktop Notifications</span>
155
- <input type="checkbox" bind:checked={globalSettings.enable_desktop} class="toggle toggle-primary" />
156
- </label>
157
- </div>
158
- <div class="divider text-xs text-base-content/50">Quiet Hours</div>
159
- <div class="form-control">
160
- <label class="label cursor-pointer">
161
- <span class="label-text">Enable Quiet Hours</span>
162
- <input type="checkbox" bind:checked={globalSettings.quiet_hours_enabled} class="toggle toggle-primary" />
163
- </label>
164
- </div>
165
- {#if globalSettings.quiet_hours_enabled}
166
- <div class="flex gap-3">
167
- <div class="flex-1">
168
- <label for="notif-quiet-hours-start" class="label"><span class="label-text text-xs">Start</span></label>
169
- <input id="notif-quiet-hours-start" type="time" bind:value={globalSettings.quiet_hours_start} class="input input-bordered input-sm w-full" />
170
- </div>
171
- <div class="flex-1">
172
- <label for="notif-quiet-hours-end" class="label"><span class="label-text text-xs">End</span></label>
173
- <input id="notif-quiet-hours-end" type="time" bind:value={globalSettings.quiet_hours_end} class="input input-bordered input-sm w-full" />
174
- </div>
175
- </div>
176
- {/if}
177
- <div class="divider text-xs text-base-content/50">Display</div>
178
- <div class="form-control">
179
- <label class="label cursor-pointer">
180
- <span class="label-text">Show Typing Indicators</span>
181
- <input type="checkbox" bind:checked={globalSettings.show_typing_indicators} class="toggle toggle-primary" />
182
- </label>
183
- </div>
184
- <div class="form-control">
185
- <label class="label cursor-pointer">
186
- <span class="label-text">Show Read Receipts</span>
187
- <input type="checkbox" bind:checked={globalSettings.show_read_receipts} class="toggle toggle-primary" />
188
- </label>
189
- </div>
190
- </div>
191
- {:else}
192
- <div class="space-y-4">
193
- <div class="form-control">
194
- <label class="label cursor-pointer">
195
- <span class="label-text">Mute Channel</span>
196
- <input type="checkbox" bind:checked={channelPrefs.muted} class="toggle toggle-warning" />
197
- </label>
198
- </div>
199
- <div>
200
- <label for="notif-channel-level" class="label"><span class="label-text font-medium">Notification Level</span></label>
201
- <select id="notif-channel-level" bind:value={channelPrefs.notification_level} class="select select-bordered w-full">
202
- <option value="all">All Messages</option>
203
- <option value="mentions">Mentions Only</option>
204
- <option value="none">None</option>
205
- </select>
206
- </div>
207
- <div class="form-control">
208
- <label class="label cursor-pointer">
209
- <span class="label-text">Desktop Notifications</span>
210
- <input type="checkbox" bind:checked={channelPrefs.desktop_notifications} class="toggle toggle-primary" />
211
- </label>
212
- </div>
213
- <div class="form-control">
214
- <label class="label cursor-pointer">
215
- <span class="label-text">Sound</span>
216
- <input type="checkbox" bind:checked={channelPrefs.sound_enabled} class="toggle toggle-primary" />
217
- </label>
218
- </div>
219
- </div>
220
- {/if}
221
-
222
- <div class="modal-action">
223
- <button class="btn btn-ghost" onclick={onclose}>Cancel</button>
224
- <button class="btn btn-primary" onclick={saveSettings} disabled={saving || loading}>
225
- {#if saving}<span class="loading loading-spinner loading-xs"></span>{/if}
226
- Save
227
- </button>
228
- </div>
229
- </div>
230
- <form method="dialog" class="modal-backdrop">
231
- <button onclick={onclose}>close</button>
232
- </form>
233
- </dialog>
234
- {/if}
@@ -1,123 +0,0 @@
1
- <!--
2
- QuotedMessageDisplay Component
3
-
4
- Displays a quoted message with sender info, truncated content, and timestamp.
5
- Used for both quote previews in input and quoted messages in conversation.
6
- -->
7
-
8
- <script lang="ts">
9
- import type { QuotedMessage } from '../../types/messaging'
10
- import { renderMentionsAsHTML } from '../../utils/mentionParser'
11
- import Avatar from './Avatar.svelte'
12
-
13
- interface Props {
14
- quotedMessage: QuotedMessage
15
- mode?: 'inline' | 'preview'
16
- class?: string
17
- onremove?: () => void
18
- }
19
-
20
- let {
21
- quotedMessage,
22
- mode = 'inline',
23
- class: className = '',
24
- onremove,
25
- }: Props = $props()
26
-
27
- function formatTime(dateString: string): string {
28
- const date = new Date(dateString)
29
- const now = new Date()
30
-
31
- if (date.toDateString() === now.toDateString()) {
32
- return date.toLocaleTimeString('en-US', {
33
- hour: 'numeric',
34
- minute: '2-digit',
35
- hour12: true,
36
- })
37
- }
38
-
39
- return date.toLocaleDateString('en-US', {
40
- month: 'short',
41
- day: 'numeric',
42
- })
43
- }
44
-
45
- function truncateContent(content: string, maxLength: number = 150): string {
46
- if (content.length <= maxLength) return content
47
- return content.substring(0, maxLength) + '...'
48
- }
49
-
50
- const renderedContent = $derived(
51
- renderMentionsAsHTML(truncateContent(quotedMessage.content)),
52
- )
53
- </script>
54
-
55
- <div class="quoted-message-display {mode} {className}">
56
- <div
57
- class="flex items-start gap-2 p-3 bg-base-200/50 rounded-lg relative"
58
- >
59
- {#if mode === 'preview' && onremove}
60
- <button
61
- class="absolute top-1 right-1 btn btn-ghost btn-xs btn-circle text-base-content/50 hover:text-error"
62
- onclick={onremove}
63
- title="Remove quote"
64
- >
65
- ✕
66
- </button>
67
- {/if}
68
-
69
- <svg class="w-3.5 h-3.5 text-primary/50 flex-shrink-0 mt-0.5" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
70
- <path d="M14.017 21v-7.391c0-5.704 3.731-9.57 8.983-10.609l.995 2.151c-2.432.917-3.995 3.293-3.995 5.849h4v10h-9.983zm-14.017 0v-7.391c0-5.704 3.748-9.57 9-10.609l.996 2.151c-2.433.917-3.996 3.293-3.996 5.849h3.983v10h-9.983z"/>
71
- </svg>
72
-
73
- <div class="flex-shrink-0">
74
- <Avatar
75
- name={quotedMessage.senderName}
76
- avatarUrl={quotedMessage.senderAvatar}
77
- size="xs"
78
- />
79
- </div>
80
-
81
- <div class="flex-1 min-w-0">
82
- <div class="flex items-center gap-2 mb-1">
83
- <span class="font-medium text-sm text-base-content/80">
84
- {quotedMessage.senderName}
85
- </span>
86
- <span class="text-xs text-base-content/50">
87
- {formatTime(quotedMessage.createdAt)}
88
- </span>
89
- </div>
90
-
91
- <div class="text-sm text-base-content/70 line-clamp-3 break-words message-content">
92
- {@html renderedContent}
93
- </div>
94
- </div>
95
- </div>
96
- </div>
97
-
98
- <style>
99
- .line-clamp-3 {
100
- display: -webkit-box;
101
- -webkit-line-clamp: 3;
102
- line-clamp: 3;
103
- -webkit-box-orient: vertical;
104
- overflow: hidden;
105
- }
106
-
107
- .quoted-message-display.preview {
108
- margin-bottom: 8px;
109
- }
110
-
111
- .quoted-message-display.inline {
112
- margin-bottom: 12px;
113
- }
114
-
115
- .message-content :global(.mention-link) {
116
- color: oklch(var(--p) / 0.6);
117
- transition: color 0.2s;
118
- }
119
-
120
- .message-content :global(.mention-link):hover {
121
- color: oklch(var(--p) / 0.8);
122
- }
123
- </style>
@@ -1,100 +0,0 @@
1
- <!--
2
- StartDMModal Component
3
-
4
- Modal for starting a new direct message conversation.
5
- Shows list of organization members to message.
6
- -->
7
-
8
- <script lang="ts">
9
- import type { OrgMember } from '../../types/messaging'
10
- import Avatar from './Avatar.svelte'
11
-
12
- interface Props {
13
- open: boolean
14
- currentUserId: string
15
- orgMembers?: OrgMember[]
16
- onclose?: () => void
17
- onstartconversation?: (data: { userId: string; userName: string }) => void
18
- }
19
-
20
- let {
21
- open = false,
22
- currentUserId,
23
- orgMembers = [],
24
- onclose,
25
- onstartconversation,
26
- }: Props = $props()
27
-
28
- let searchQuery = $state('')
29
-
30
- const filteredMembers = $derived(
31
- orgMembers
32
- .filter((m) => m.id !== currentUserId)
33
- .filter((m) => !searchQuery || m.full_name.toLowerCase().includes(searchQuery.toLowerCase()))
34
- .sort((a, b) => a.full_name.localeCompare(b.full_name)),
35
- )
36
-
37
- function handleSelect(member: OrgMember) {
38
- onstartconversation?.({ userId: member.id, userName: member.full_name })
39
- onclose?.()
40
- searchQuery = ''
41
- }
42
-
43
- function handleClose() {
44
- searchQuery = ''
45
- onclose?.()
46
- }
47
- </script>
48
-
49
- {#if open}
50
- <dialog class="modal modal-open">
51
- <div class="modal-box max-w-md">
52
- <div class="flex items-center justify-between mb-4">
53
- <h3 class="font-semibold">New Message</h3>
54
- <button class="btn btn-ghost btn-sm btn-circle" onclick={handleClose}>✕</button>
55
- </div>
56
-
57
- <!-- Search -->
58
- <input
59
- type="text"
60
- bind:value={searchQuery}
61
- class="input input-bordered w-full mb-4"
62
- placeholder="Search team members..."
63
- />
64
-
65
- <!-- Members list -->
66
- <div class="max-h-80 overflow-y-auto space-y-1">
67
- {#each filteredMembers as member}
68
- <button
69
- class="w-full flex items-center gap-3 px-3 py-2 rounded hover:bg-base-200 transition-colors"
70
- onclick={() => handleSelect(member)}
71
- >
72
- <Avatar
73
- name={member.full_name}
74
- avatarUrl={member.avatar_url}
75
- size="small"
76
- showStatus={true}
77
- status={member.active ? 'online' : 'offline'}
78
- />
79
- <div class="flex-1 text-left">
80
- <div class="text-sm font-medium">{member.full_name}</div>
81
- <div class="text-xs text-base-content/50 capitalize">{member.role}</div>
82
- </div>
83
- </button>
84
- {/each}
85
-
86
- {#if filteredMembers.length === 0}
87
- <div class="text-center py-8 text-base-content/50">
88
- <div class="text-2xl mb-2">💬</div>
89
- <div class="text-sm">
90
- {searchQuery ? `No members matching "${searchQuery}"` : 'No team members available'}
91
- </div>
92
- </div>
93
- {/if}
94
- </div>
95
- </div>
96
- <form method="dialog" class="modal-backdrop">
97
- <button onclick={handleClose}>close</button>
98
- </form>
99
- </dialog>
100
- {/if}
@@ -1,153 +0,0 @@
1
- <!--
2
- ThreadPanel Component
3
-
4
- Shows threaded replies to a parent message in a sidebar view.
5
- All data operations are via callbacks.
6
- -->
7
-
8
- <script lang="ts">
9
- import type {
10
- Message,
11
- PendingAttachment,
12
- ReactionSummary,
13
- StandardEmoji,
14
- MentionItem,
15
- ThreadCallbacks,
16
- ReactionCallbacks,
17
- AttachmentCallbacks,
18
- } from '../../types/messaging'
19
- import { renderMentionsAsHTML } from '../../utils/mentionParser'
20
- import MessageItem from './MessageItem.svelte'
21
- import MessageInput from './MessageInput.svelte'
22
- import Avatar from './Avatar.svelte'
23
-
24
- interface Props {
25
- parentMessage: Message
26
- replies?: Message[]
27
- currentUserId: string
28
- quickReactions?: StandardEmoji[]
29
- threadCallbacks: ThreadCallbacks
30
- reactionCallbacks?: ReactionCallbacks
31
- attachmentCallbacks?: AttachmentCallbacks
32
- getReactions?: (messageId: string) => ReactionSummary[]
33
- onsearchmentions?: (query: string) => Promise<MentionItem[]>
34
- onclose?: () => void
35
- }
36
-
37
- let {
38
- parentMessage,
39
- replies = [],
40
- currentUserId,
41
- quickReactions = [],
42
- threadCallbacks,
43
- reactionCallbacks,
44
- attachmentCallbacks,
45
- getReactions,
46
- onsearchmentions,
47
- onclose,
48
- }: Props = $props()
49
-
50
- let sending = $state(false)
51
- let messagesContainer: HTMLElement
52
-
53
- const renderedParentContent = $derived(renderMentionsAsHTML(parentMessage.content))
54
-
55
- function isConsecutive(msg: Message, index: number): boolean {
56
- if (index === 0) return false
57
- const prev = replies[index - 1]
58
- if (!prev || prev.senderId !== msg.senderId) return false
59
- const diff = new Date(msg.createdAt).getTime() - new Date(prev.createdAt).getTime()
60
- return diff < 5 * 60 * 1000
61
- }
62
-
63
- async function handleSend(data: { content: string; attachments?: PendingAttachment[] }) {
64
- if (sending) return
65
- sending = true
66
- try {
67
- await threadCallbacks.sendReply(parentMessage.id, data.content, data.attachments)
68
- scrollToBottom()
69
- } catch (err) {
70
- console.error('Failed to send reply:', err)
71
- } finally {
72
- sending = false
73
- }
74
- }
75
-
76
- function scrollToBottom() {
77
- setTimeout(() => {
78
- if (messagesContainer) messagesContainer.scrollTop = messagesContainer.scrollHeight
79
- }, 50)
80
- }
81
-
82
- function formatTime(dateString: string): string {
83
- const date = new Date(dateString)
84
- return date.toLocaleTimeString('en-US', { hour: 'numeric', minute: '2-digit', hour12: true })
85
- }
86
- </script>
87
-
88
- <div class="flex flex-col h-full">
89
- <!-- Header -->
90
- <div class="flex items-center justify-between px-4 py-3 border-b border-base-300">
91
- <h3 class="font-semibold text-sm">Thread</h3>
92
- {#if onclose}
93
- <button class="btn btn-ghost btn-xs btn-circle" onclick={onclose} title="Close thread">✕</button>
94
- {/if}
95
- </div>
96
-
97
- <!-- Parent message -->
98
- <div class="px-4 py-3 border-b border-base-300 bg-base-200/30">
99
- <div class="flex items-start gap-3">
100
- <Avatar name={parentMessage.senderName} avatarUrl={parentMessage.senderAvatar} size="small" />
101
- <div class="flex-1 min-w-0">
102
- <div class="flex items-center gap-2 mb-1">
103
- <span class="font-medium text-sm">{parentMessage.senderName}</span>
104
- <span class="text-xs text-base-content/50">{formatTime(parentMessage.createdAt)}</span>
105
- </div>
106
- <div class="text-sm whitespace-pre-wrap break-words message-content">
107
- {@html renderedParentContent}
108
- </div>
109
- </div>
110
- </div>
111
-
112
- {#if replies.length > 0}
113
- <div class="mt-2 text-xs text-base-content/50">
114
- {replies.length} {replies.length === 1 ? 'reply' : 'replies'}
115
- </div>
116
- {/if}
117
- </div>
118
-
119
- <!-- Replies -->
120
- <div bind:this={messagesContainer} class="flex-1 overflow-y-auto px-4 py-2">
121
- {#if replies.length === 0}
122
- <div class="flex flex-col items-center justify-center h-full text-base-content/50">
123
- <div class="text-2xl mb-2">💬</div>
124
- <p class="text-sm">No replies yet</p>
125
- </div>
126
- {:else}
127
- {#each replies as reply, index (reply.id)}
128
- <MessageItem
129
- message={reply}
130
- isOwn={reply.senderId === currentUserId}
131
- isConsecutive={isConsecutive(reply, index)}
132
- {currentUserId}
133
- reactions={getReactions?.(reply.id) || []}
134
- {quickReactions}
135
- {reactionCallbacks}
136
- {attachmentCallbacks}
137
- />
138
- {/each}
139
- {/if}
140
- </div>
141
-
142
- <!-- Reply input -->
143
- <div class="px-4 py-3 border-t border-base-300">
144
- <MessageInput
145
- placeholder="Reply in thread..."
146
- compact={true}
147
- disabled={sending}
148
- onsend={handleSend}
149
- {onsearchmentions}
150
- {attachmentCallbacks}
151
- />
152
- </div>
153
- </div>