@joewinke/jatui 0.1.23 → 0.1.25

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,182 +0,0 @@
1
- <!--
2
- ChannelMembersModal Component
3
-
4
- Manages channel members — view, add, remove, change roles.
5
- All operations via callbacks.
6
- -->
7
-
8
- <script lang="ts">
9
- import type { ChannelMember, OrgMember, ChannelCallbacks } from '../../types/messaging'
10
- import Avatar from './Avatar.svelte'
11
-
12
- interface Props {
13
- channelId: string
14
- channelName: string
15
- currentUserRole: 'admin' | 'moderator' | 'member'
16
- currentUserId: string
17
- open: boolean
18
- members?: ChannelMember[]
19
- orgMembers?: OrgMember[]
20
- channelCallbacks: ChannelCallbacks
21
- onclose?: () => void
22
- onmemberupdate?: () => void
23
- }
24
-
25
- let {
26
- channelId,
27
- channelName,
28
- currentUserRole,
29
- currentUserId,
30
- open = false,
31
- members = [],
32
- orgMembers = [],
33
- channelCallbacks,
34
- onclose,
35
- onmemberupdate,
36
- }: Props = $props()
37
-
38
- let searchQuery = $state('')
39
- let showAddMembers = $state(false)
40
- let loading = $state(false)
41
- let error = $state<string | null>(null)
42
-
43
- const isAdmin = $derived(currentUserRole === 'admin')
44
-
45
- const filteredMembers = $derived(
46
- members.filter((m) => !searchQuery || m.fullName.toLowerCase().includes(searchQuery.toLowerCase())),
47
- )
48
-
49
- const availableMembers = $derived(
50
- orgMembers
51
- .filter((m) => !members.some((cm) => cm.userId === m.id))
52
- .filter((m) => !searchQuery || m.full_name.toLowerCase().includes(searchQuery.toLowerCase())),
53
- )
54
-
55
- const roleIcons: Record<string, string> = { admin: '👑', moderator: '🛡', member: '👤' }
56
-
57
- async function addMember(userId: string) {
58
- loading = true; error = null
59
- try {
60
- await channelCallbacks.addMember(channelId, userId)
61
- onmemberupdate?.()
62
- } catch (err) {
63
- error = err instanceof Error ? err.message : 'Failed to add member'
64
- } finally { loading = false }
65
- }
66
-
67
- async function removeMember(userId: string) {
68
- if (!confirm('Remove this member from the channel?')) return
69
- loading = true; error = null
70
- try {
71
- await channelCallbacks.removeMember(channelId, userId)
72
- onmemberupdate?.()
73
- } catch (err) {
74
- error = err instanceof Error ? err.message : 'Failed to remove member'
75
- } finally { loading = false }
76
- }
77
-
78
- async function changeRole(userId: string, role: 'admin' | 'moderator' | 'member') {
79
- loading = true; error = null
80
- try {
81
- await channelCallbacks.updateMemberRole(channelId, userId, role)
82
- onmemberupdate?.()
83
- } catch (err) {
84
- error = err instanceof Error ? err.message : 'Failed to update role'
85
- } finally { loading = false }
86
- }
87
-
88
- async function leaveChannel() {
89
- if (!confirm(`Leave #${channelName}?`)) return
90
- try {
91
- await channelCallbacks.leaveChannel(channelId)
92
- onclose?.()
93
- } catch (err) {
94
- error = err instanceof Error ? err.message : 'Failed to leave channel'
95
- }
96
- }
97
- </script>
98
-
99
- {#if open}
100
- <dialog class="modal modal-open">
101
- <div class="modal-box max-w-md">
102
- <div class="flex items-center justify-between mb-4">
103
- <h3 class="font-semibold">Members of #{channelName}</h3>
104
- <button class="btn btn-ghost btn-sm btn-circle" onclick={onclose}>✕</button>
105
- </div>
106
-
107
- {#if error}
108
- <div class="alert alert-error mb-4"><span class="text-sm">{error}</span></div>
109
- {/if}
110
-
111
- <!-- Search -->
112
- <input type="text" bind:value={searchQuery} class="input input-bordered input-sm w-full mb-3" placeholder="Search members..." />
113
-
114
- <!-- Toggle: current members / add members -->
115
- {#if isAdmin}
116
- <div class="flex gap-2 mb-3">
117
- <button class="btn btn-sm {!showAddMembers ? 'btn-primary' : 'btn-ghost'}" onclick={() => (showAddMembers = false)}>
118
- Members ({members.length})
119
- </button>
120
- <button class="btn btn-sm {showAddMembers ? 'btn-primary' : 'btn-ghost'}" onclick={() => (showAddMembers = true)}>
121
- + Add
122
- </button>
123
- </div>
124
- {/if}
125
-
126
- <div class="max-h-72 overflow-y-auto space-y-1">
127
- {#if showAddMembers}
128
- {#each availableMembers as member}
129
- <div class="flex items-center gap-3 px-2 py-2 rounded hover:bg-base-200">
130
- <Avatar name={member.full_name} avatarUrl={member.avatar_url} size="small" />
131
- <div class="flex-1">
132
- <div class="text-sm font-medium">{member.full_name}</div>
133
- <div class="text-xs text-base-content/50 capitalize">{member.role}</div>
134
- </div>
135
- <button class="btn btn-xs btn-primary" onclick={() => addMember(member.id)} disabled={loading}>Add</button>
136
- </div>
137
- {/each}
138
- {#if availableMembers.length === 0}
139
- <div class="text-center py-4 text-base-content/50 text-sm">No more members to add</div>
140
- {/if}
141
- {:else}
142
- {#each filteredMembers as member}
143
- <div class="flex items-center gap-3 px-2 py-2 rounded hover:bg-base-200">
144
- <Avatar name={member.fullName} avatarUrl={member.avatarUrl} size="small" />
145
- <div class="flex-1">
146
- <div class="text-sm font-medium flex items-center gap-1">
147
- {member.fullName}
148
- <span class="text-xs">{roleIcons[member.role]}</span>
149
- </div>
150
- </div>
151
- {#if isAdmin && member.userId !== currentUserId}
152
- <div class="dropdown dropdown-end">
153
- <button tabindex="0" class="btn btn-ghost btn-xs">⋮</button>
154
- <ul tabindex="-1" class="dropdown-content menu p-1 shadow bg-base-100 rounded-box w-36 z-50">
155
- {#if member.role !== 'admin'}
156
- <li><button onclick={() => changeRole(member.userId, 'admin')}>Make Admin</button></li>
157
- {/if}
158
- {#if member.role !== 'moderator'}
159
- <li><button onclick={() => changeRole(member.userId, 'moderator')}>Make Moderator</button></li>
160
- {/if}
161
- {#if member.role !== 'member'}
162
- <li><button onclick={() => changeRole(member.userId, 'member')}>Make Member</button></li>
163
- {/if}
164
- <li><button class="text-error" onclick={() => removeMember(member.userId)}>Remove</button></li>
165
- </ul>
166
- </div>
167
- {/if}
168
- </div>
169
- {/each}
170
- {/if}
171
- </div>
172
-
173
- <div class="modal-action justify-between">
174
- <button class="btn btn-ghost btn-sm text-error" onclick={leaveChannel}>Leave Channel</button>
175
- <button class="btn btn-ghost" onclick={onclose}>Close</button>
176
- </div>
177
- </div>
178
- <form method="dialog" class="modal-backdrop">
179
- <button onclick={onclose}>close</button>
180
- </form>
181
- </dialog>
182
- {/if}
@@ -1,190 +0,0 @@
1
- <!--
2
- CreateChannelModal Component
3
-
4
- Modal for creating a new channel with name, description, type, emoji, and member selection.
5
- All operations via callbacks.
6
- -->
7
-
8
- <script lang="ts">
9
- import type { Channel, OrgMember, EmojiSelection, ChannelCallbacks } from '../../types/messaging'
10
- import Avatar from './Avatar.svelte'
11
- import EmojiSelector from './EmojiSelector.svelte'
12
-
13
- interface Props {
14
- open: boolean
15
- currentUserId: string
16
- orgMembers?: OrgMember[]
17
- channelCallbacks: ChannelCallbacks
18
- onclose?: () => void
19
- onchannelcreated?: (event: { channel: Channel }) => void
20
- }
21
-
22
- let {
23
- open = false,
24
- currentUserId,
25
- orgMembers = [],
26
- channelCallbacks,
27
- onclose,
28
- onchannelcreated,
29
- }: Props = $props()
30
-
31
- let channelName = $state('')
32
- let description = $state('')
33
- let channelType = $state<'public' | 'private'>('public')
34
- let selectedMembers = $state<string[]>([])
35
- let searchQuery = $state('')
36
- let selectedEmoji = $state<string>('💬')
37
- let showEmojiSelector = $state(false)
38
- let emojiSelectorPosition = $state({ x: 0, y: 0 })
39
- let submitting = $state(false)
40
- let error = $state<string | null>(null)
41
-
42
- const isValid = $derived(channelName.trim().length > 0 && channelName.trim().length <= 80)
43
-
44
- const filteredMembers = $derived(
45
- orgMembers
46
- .filter((m) => m.id !== currentUserId)
47
- .filter((m) => !searchQuery || m.full_name.toLowerCase().includes(searchQuery.toLowerCase())),
48
- )
49
-
50
- function toggleMember(memberId: string) {
51
- if (selectedMembers.includes(memberId)) {
52
- selectedMembers = selectedMembers.filter((id) => id !== memberId)
53
- } else {
54
- selectedMembers = [...selectedMembers, memberId]
55
- }
56
- }
57
-
58
- function handleEmojiSelect(selection: EmojiSelection) {
59
- if (selection.unicode) selectedEmoji = selection.unicode
60
- showEmojiSelector = false
61
- }
62
-
63
- function showEmojiPicker(event: MouseEvent) {
64
- const rect = (event.target as HTMLElement).getBoundingClientRect()
65
- emojiSelectorPosition = { x: rect.left, y: rect.bottom + 8 }
66
- showEmojiSelector = true
67
- }
68
-
69
- async function handleSubmit() {
70
- if (!isValid || submitting) return
71
- submitting = true
72
- error = null
73
-
74
- try {
75
- const channel = await channelCallbacks.createChannel({
76
- name: channelName.trim(),
77
- description: description.trim() || undefined,
78
- type: channelType,
79
- emoji: selectedEmoji,
80
- memberIds: selectedMembers,
81
- })
82
- onchannelcreated?.({ channel })
83
- resetForm()
84
- onclose?.()
85
- } catch (err) {
86
- error = err instanceof Error ? err.message : 'Failed to create channel'
87
- } finally {
88
- submitting = false
89
- }
90
- }
91
-
92
- function resetForm() {
93
- channelName = ''
94
- description = ''
95
- channelType = 'public'
96
- selectedMembers = []
97
- searchQuery = ''
98
- selectedEmoji = '💬'
99
- error = null
100
- }
101
-
102
- function handleClose() {
103
- resetForm()
104
- onclose?.()
105
- }
106
- </script>
107
-
108
- {#if open}
109
- <dialog class="modal modal-open">
110
- <div class="modal-box max-w-lg">
111
- <div class="flex items-center justify-between mb-4">
112
- <h3 class="font-semibold">Create Channel</h3>
113
- <button class="btn btn-ghost btn-sm btn-circle" onclick={handleClose}>✕</button>
114
- </div>
115
-
116
- {#if error}
117
- <div class="alert alert-error mb-4">
118
- <span class="text-sm">{error}</span>
119
- </div>
120
- {/if}
121
-
122
- <form onsubmit={(e) => { e.preventDefault(); handleSubmit() }} class="space-y-4">
123
- <!-- Channel name + emoji -->
124
- <div class="flex items-end gap-2">
125
- <button type="button" class="btn btn-ghost btn-lg btn-square text-2xl" onclick={showEmojiPicker} title="Choose emoji">
126
- {selectedEmoji}
127
- </button>
128
- <div class="flex-1">
129
- <label for="create-channel-name" class="label"><span class="label-text font-medium">Channel Name</span></label>
130
- <input id="create-channel-name" type="text" bind:value={channelName} class="input input-bordered w-full" placeholder="e.g., general, announcements" maxlength="80" />
131
- </div>
132
- </div>
133
-
134
- <!-- Description -->
135
- <div>
136
- <label for="create-channel-description" class="label"><span class="label-text font-medium">Description</span></label>
137
- <textarea id="create-channel-description" bind:value={description} class="textarea textarea-bordered w-full" placeholder="What's this channel about?" rows="2" maxlength="250"></textarea>
138
- </div>
139
-
140
- <!-- Channel type -->
141
- <div>
142
- <span class="label"><span class="label-text font-medium">Channel Type</span></span>
143
- <div class="flex gap-3">
144
- <label class="cursor-pointer flex items-center gap-2">
145
- <input type="radio" name="channelType" class="radio radio-primary" bind:group={channelType} value="public" />
146
- <span class="text-sm"># Public</span>
147
- </label>
148
- <label class="cursor-pointer flex items-center gap-2">
149
- <input type="radio" name="channelType" class="radio radio-primary" bind:group={channelType} value="private" />
150
- <span class="text-sm">🔒 Private</span>
151
- </label>
152
- </div>
153
- </div>
154
-
155
- <!-- Member selection -->
156
- <div>
157
- <label for="create-channel-member-search" class="label"><span class="label-text font-medium">Add Members ({selectedMembers.length})</span></label>
158
- <input id="create-channel-member-search" type="text" bind:value={searchQuery} class="input input-bordered input-sm w-full mb-2" placeholder="Search members..." />
159
- <div class="max-h-40 overflow-y-auto space-y-1">
160
- {#each filteredMembers as member}
161
- <button type="button" class="w-full flex items-center gap-3 px-2 py-1.5 rounded hover:bg-base-200 transition-colors" onclick={() => toggleMember(member.id)}>
162
- <Avatar name={member.full_name} avatarUrl={member.avatar_url} size="xs" />
163
- <span class="text-sm flex-1 text-left">{member.full_name}</span>
164
- {#if selectedMembers.includes(member.id)}
165
- <span class="text-primary">✓</span>
166
- {/if}
167
- </button>
168
- {/each}
169
- </div>
170
- </div>
171
-
172
- <!-- Submit -->
173
- <div class="modal-action">
174
- <button type="button" class="btn btn-ghost" onclick={handleClose}>Cancel</button>
175
- <button type="submit" class="btn btn-primary" disabled={!isValid || submitting}>
176
- {#if submitting}<span class="loading loading-spinner loading-sm"></span>{/if}
177
- Create Channel
178
- </button>
179
- </div>
180
- </form>
181
- </div>
182
- <form method="dialog" class="modal-backdrop">
183
- <button onclick={handleClose}>close</button>
184
- </form>
185
- </dialog>
186
- {/if}
187
-
188
- {#if showEmojiSelector}
189
- <EmojiSelector visible={showEmojiSelector} position={emojiSelectorPosition} onselect={handleEmojiSelect} onclose={() => (showEmojiSelector = false)} />
190
- {/if}
@@ -1,145 +0,0 @@
1
- <!--
2
- DirectMessageList Component
3
-
4
- Displays direct message conversations and available users.
5
- Shows online status and unread counts.
6
- -->
7
-
8
- <script lang="ts">
9
- import type { DirectConversation, OrgMember } from '../../types/messaging'
10
- import Avatar from './Avatar.svelte'
11
-
12
- interface Props {
13
- conversations: DirectConversation[]
14
- orgMembers: OrgMember[]
15
- activeDMUserId?: string | null
16
- onconversationselect?: (data: { userId: string }) => void
17
- }
18
-
19
- let {
20
- conversations = [],
21
- orgMembers = [],
22
- activeDMUserId = null,
23
- onconversationselect,
24
- }: Props = $props()
25
-
26
- function selectConversation(userId: string) {
27
- onconversationselect?.({ userId })
28
- }
29
-
30
- function formatLastMessageTime(dateString: string): string {
31
- const date = new Date(dateString)
32
- const now = new Date()
33
- const diffInMinutes = Math.floor((now.getTime() - date.getTime()) / (1000 * 60))
34
-
35
- if (diffInMinutes < 1) return 'now'
36
- if (diffInMinutes < 60) return `${diffInMinutes}m`
37
- if (diffInMinutes < 1440) return `${Math.floor(diffInMinutes / 60)}h`
38
- return `${Math.floor(diffInMinutes / 1440)}d`
39
- }
40
-
41
- interface CombinedUser {
42
- id: string
43
- name: string
44
- avatar?: string
45
- lastMessageAt: string
46
- unreadCount: number
47
- isOnline: boolean
48
- hasRecentConversation: boolean
49
- role?: string
50
- }
51
-
52
- let allUsers = $state<CombinedUser[]>([])
53
-
54
- $effect(() => {
55
- if (!conversations || !orgMembers) {
56
- allUsers = []
57
- return
58
- }
59
-
60
- const conversationUserIds = new Set(conversations.map((conv) => conv.id))
61
-
62
- const recentConvs: CombinedUser[] = conversations.map((conv) => {
63
- const orgMember = orgMembers.find((member) => member.id === conv.id)
64
- return {
65
- id: conv.id,
66
- name: conv.name,
67
- avatar: conv.avatar,
68
- lastMessageAt: conv.lastMessageAt,
69
- unreadCount: conv.unreadCount || 0,
70
- isOnline: orgMember?.active || false,
71
- hasRecentConversation: true,
72
- }
73
- })
74
-
75
- const otherMembers: CombinedUser[] = orgMembers
76
- .filter((member) => !conversationUserIds.has(member.id))
77
- .map((member) => ({
78
- id: member.id,
79
- name: member.full_name,
80
- avatar: member.avatar_url,
81
- lastMessageAt: '',
82
- unreadCount: 0,
83
- isOnline: member.active || false,
84
- hasRecentConversation: false,
85
- role: member.role,
86
- }))
87
- .sort((a, b) => a.name.localeCompare(b.name))
88
-
89
- allUsers = [...recentConvs, ...otherMembers]
90
- })
91
- </script>
92
-
93
- <div class="space-y-1">
94
- {#each allUsers as user}
95
- <button
96
- class="w-full flex items-center gap-3 px-2 py-2 text-left rounded hover:bg-base-300 transition-colors"
97
- class:bg-primary={activeDMUserId === user.id}
98
- class:text-primary-content={activeDMUserId === user.id}
99
- class:font-medium={activeDMUserId === user.id}
100
- onclick={() => selectConversation(user.id)}
101
- >
102
- <div class="flex-shrink-0">
103
- <Avatar
104
- name={user.name}
105
- avatarUrl={user.avatar}
106
- size="small"
107
- showStatus={true}
108
- status={user.isOnline ? 'online' : 'offline'}
109
- />
110
- </div>
111
-
112
- <div class="flex-1 min-w-0">
113
- <div class="flex items-center justify-between">
114
- <span class="text-sm truncate">{user.name}</span>
115
-
116
- <div class="flex items-center gap-1">
117
- {#if user.unreadCount > 0}
118
- <span class="badge badge-error badge-xs text-error-content">
119
- {user.unreadCount > 99 ? '99+' : user.unreadCount}
120
- </span>
121
- {/if}
122
-
123
- {#if user.lastMessageAt}
124
- <span class="text-xs text-base-content/50">
125
- {formatLastMessageTime(user.lastMessageAt)}
126
- </span>
127
- {/if}
128
- </div>
129
- </div>
130
-
131
- {#if !user.hasRecentConversation && user.role}
132
- <div class="text-xs text-base-content/50 capitalize">{user.role}</div>
133
- {/if}
134
- </div>
135
- </button>
136
- {/each}
137
-
138
- {#if allUsers.length === 0}
139
- <div class="text-center py-8 text-base-content/50">
140
- <div class="text-2xl mb-2">💬</div>
141
- <div class="text-sm">No conversations yet</div>
142
- <div class="text-xs">Start messaging your team</div>
143
- </div>
144
- {/if}
145
- </div>