@joewinke/jatui 0.1.25 → 0.1.26

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 (242) hide show
  1. package/dist/actions/collapse.d.ts +80 -0
  2. package/dist/actions/collapse.js +134 -0
  3. package/dist/actions/railNav.d.ts +167 -0
  4. package/dist/actions/railNav.js +351 -0
  5. package/dist/components/AnnotationLayer.svelte +108 -0
  6. package/dist/components/AnnotationLayer.svelte.d.ts +12 -0
  7. package/dist/components/AnnotationPanel.svelte +319 -0
  8. package/dist/components/AnnotationPanel.svelte.d.ts +13 -0
  9. package/dist/components/AudioWaveform.svelte +701 -0
  10. package/dist/components/AudioWaveform.svelte.d.ts +19 -0
  11. package/dist/components/AvailabilityModal.svelte +177 -0
  12. package/dist/components/AvailabilityModal.svelte.d.ts +14 -0
  13. package/dist/components/AvatarUpload.svelte +234 -0
  14. package/dist/components/AvatarUpload.svelte.d.ts +23 -0
  15. package/dist/components/Badge.svelte +38 -0
  16. package/dist/components/Badge.svelte.d.ts +14 -0
  17. package/dist/components/BookingForm.svelte +278 -0
  18. package/dist/components/BookingForm.svelte.d.ts +37 -0
  19. package/dist/components/BurndownChart.svelte +778 -0
  20. package/dist/components/BurndownChart.svelte.d.ts +17 -0
  21. package/dist/components/Button.svelte +81 -0
  22. package/dist/components/Button.svelte.d.ts +21 -0
  23. package/dist/components/CalendarPicker.svelte +284 -0
  24. package/dist/components/CalendarPicker.svelte.d.ts +22 -0
  25. package/dist/components/Card.svelte +67 -0
  26. package/dist/components/Card.svelte.d.ts +17 -0
  27. package/dist/components/CharacterCounter.svelte +82 -0
  28. package/dist/components/CharacterCounter.svelte.d.ts +10 -0
  29. package/dist/components/ChipInput.svelte +609 -0
  30. package/dist/components/ChipInput.svelte.d.ts +55 -0
  31. package/dist/components/Collapse.svelte +91 -0
  32. package/dist/components/Collapse.svelte.d.ts +18 -0
  33. package/dist/components/ColorSelector.svelte +167 -0
  34. package/dist/components/ColorSelector.svelte.d.ts +9 -0
  35. package/dist/components/CommentThread.svelte +773 -0
  36. package/dist/components/CommentThread.svelte.d.ts +32 -0
  37. package/dist/components/ConfirmDialog.svelte +348 -0
  38. package/dist/components/ConfirmDialog.svelte.d.ts +3 -0
  39. package/dist/components/ConfirmModal.svelte +142 -0
  40. package/dist/components/ConfirmModal.svelte.d.ts +15 -0
  41. package/dist/components/ContextMenu.svelte +188 -0
  42. package/dist/components/ContextMenu.svelte.d.ts +31 -0
  43. package/dist/components/CountdownTimer.svelte +94 -0
  44. package/dist/components/CountdownTimer.svelte.d.ts +10 -0
  45. package/dist/components/DateRangePicker.svelte +194 -0
  46. package/dist/components/DateRangePicker.svelte.d.ts +10 -0
  47. package/dist/components/Drawer.svelte +143 -0
  48. package/dist/components/Drawer.svelte.d.ts +33 -0
  49. package/dist/components/EmojiPicker.svelte +159 -0
  50. package/dist/components/EmojiPicker.svelte.d.ts +8 -0
  51. package/dist/components/EntityPreviewCard.svelte +104 -0
  52. package/dist/components/EntityPreviewCard.svelte.d.ts +9 -0
  53. package/dist/components/FileDropzone.svelte +493 -0
  54. package/dist/components/FileDropzone.svelte.d.ts +40 -0
  55. package/dist/components/FilePicker.svelte +488 -0
  56. package/dist/components/FilePicker.svelte.d.ts +20 -0
  57. package/dist/components/FileThumbnail.svelte +80 -0
  58. package/dist/components/FileThumbnail.svelte.d.ts +12 -0
  59. package/dist/components/FilterDropdown.svelte +203 -0
  60. package/dist/components/FilterDropdown.svelte.d.ts +48 -0
  61. package/dist/components/FloatingActionBar.svelte +354 -0
  62. package/dist/components/FloatingActionBar.svelte.d.ts +51 -0
  63. package/dist/components/GPSTracker.svelte +202 -0
  64. package/dist/components/GPSTracker.svelte.d.ts +34 -0
  65. package/dist/components/HunkDiffView.svelte +348 -0
  66. package/dist/components/HunkDiffView.svelte.d.ts +9 -0
  67. package/dist/components/ImageLightbox.svelte +274 -0
  68. package/dist/components/ImageLightbox.svelte.d.ts +25 -0
  69. package/dist/components/ImageUpload.svelte +147 -0
  70. package/dist/components/ImageUpload.svelte.d.ts +10 -0
  71. package/dist/components/InlineEdit.svelte +291 -0
  72. package/dist/components/InlineEdit.svelte.d.ts +45 -0
  73. package/dist/components/InputDialog.svelte +327 -0
  74. package/dist/components/InputDialog.svelte.d.ts +3 -0
  75. package/dist/components/KeyboardShortcutsOverlay.svelte +296 -0
  76. package/dist/components/KeyboardShortcutsOverlay.svelte.d.ts +42 -0
  77. package/dist/components/LazyImage.svelte +123 -0
  78. package/dist/components/LazyImage.svelte.d.ts +14 -0
  79. package/dist/components/LinkShortener.svelte +274 -0
  80. package/dist/components/LinkShortener.svelte.d.ts +27 -0
  81. package/dist/components/LoadingSpinner.svelte +106 -0
  82. package/dist/components/LoadingSpinner.svelte.d.ts +21 -0
  83. package/dist/components/LocationMap.svelte +186 -0
  84. package/dist/components/LocationMap.svelte.d.ts +11 -0
  85. package/dist/components/MapView.svelte +341 -0
  86. package/dist/components/MapView.svelte.d.ts +20 -0
  87. package/dist/components/MarkupEditor.svelte +485 -0
  88. package/dist/components/MarkupEditor.svelte.d.ts +10 -0
  89. package/dist/components/MarkupOverlay.svelte +55 -0
  90. package/dist/components/MarkupOverlay.svelte.d.ts +8 -0
  91. package/dist/components/MediaWorkbench.svelte +871 -0
  92. package/dist/components/MediaWorkbench.svelte.d.ts +9 -0
  93. package/dist/components/MilestoneCard.svelte +98 -0
  94. package/dist/components/MilestoneCard.svelte.d.ts +16 -0
  95. package/dist/components/MilestoneTimeline.svelte +91 -0
  96. package/dist/components/MilestoneTimeline.svelte.d.ts +15 -0
  97. package/dist/components/Modal.svelte +244 -0
  98. package/dist/components/Modal.svelte.d.ts +35 -0
  99. package/dist/components/PDFViewer.svelte +105 -0
  100. package/dist/components/PDFViewer.svelte.d.ts +9 -0
  101. package/dist/components/PdfThumbnail.svelte +63 -0
  102. package/dist/components/PdfThumbnail.svelte.d.ts +9 -0
  103. package/dist/components/PhoneInput.svelte +212 -0
  104. package/dist/components/PhoneInput.svelte.d.ts +25 -0
  105. package/dist/components/ResizableDivider.svelte +307 -0
  106. package/dist/components/ResizableDivider.svelte.d.ts +26 -0
  107. package/dist/components/ResizablePanel.svelte +305 -0
  108. package/dist/components/ResizablePanel.svelte.d.ts +18 -0
  109. package/dist/components/SearchDropdown.svelte +421 -0
  110. package/dist/components/SearchDropdown.svelte.d.ts +24 -0
  111. package/dist/components/SelectInput.svelte +237 -0
  112. package/dist/components/SelectInput.svelte.d.ts +26 -0
  113. package/dist/components/SidebarUserFooter.svelte +34 -0
  114. package/dist/components/SidebarUserFooter.svelte.d.ts +16 -0
  115. package/dist/components/SignaturePad.svelte +175 -0
  116. package/dist/components/SignaturePad.svelte.d.ts +17 -0
  117. package/dist/components/SmartImageEditor.svelte +720 -0
  118. package/dist/components/SmartImageEditor.svelte.d.ts +24 -0
  119. package/dist/components/SortDropdown.svelte +156 -0
  120. package/dist/components/SortDropdown.svelte.d.ts +40 -0
  121. package/dist/components/Sparkline.svelte +116 -0
  122. package/dist/components/Sparkline.svelte.d.ts +12 -0
  123. package/dist/components/SpeechForm.svelte +114 -0
  124. package/dist/components/SpeechForm.svelte.d.ts +17 -0
  125. package/dist/components/StatusBadge.svelte +157 -0
  126. package/dist/components/StatusBadge.svelte.d.ts +24 -0
  127. package/dist/components/TaskTypeIcon.svelte +54 -0
  128. package/dist/components/TaskTypeIcon.svelte.d.ts +17 -0
  129. package/dist/components/TextArea.svelte +162 -0
  130. package/dist/components/TextArea.svelte.d.ts +30 -0
  131. package/dist/components/TextInput.svelte +131 -0
  132. package/dist/components/TextInput.svelte.d.ts +29 -0
  133. package/dist/components/ThemeSelector.svelte +206 -0
  134. package/dist/components/ThemeSelector.svelte.d.ts +14 -0
  135. package/dist/components/TimeSlotPicker.svelte +162 -0
  136. package/dist/components/TimeSlotPicker.svelte.d.ts +18 -0
  137. package/dist/components/UserAvatar.svelte +72 -0
  138. package/dist/components/UserAvatar.svelte.d.ts +11 -0
  139. package/dist/components/VariablePicker.svelte +170 -0
  140. package/dist/components/VariablePicker.svelte.d.ts +40 -0
  141. package/dist/components/VoicePlayer.svelte +422 -0
  142. package/dist/components/VoicePlayer.svelte.d.ts +18 -0
  143. package/dist/components/linked-columns/LinkedColumns.svelte +526 -0
  144. package/dist/components/linked-columns/LinkedColumns.svelte.d.ts +130 -0
  145. package/dist/components/markup.d.ts +47 -0
  146. package/dist/components/markup.js +201 -0
  147. package/dist/components/messaging/Avatar.svelte +81 -0
  148. package/dist/components/messaging/Avatar.svelte.d.ts +11 -0
  149. package/dist/components/messaging/ChannelInfoModal.svelte +163 -0
  150. package/dist/components/messaging/ChannelInfoModal.svelte.d.ts +18 -0
  151. package/dist/components/messaging/ChannelList.svelte +107 -0
  152. package/dist/components/messaging/ChannelList.svelte.d.ts +11 -0
  153. package/dist/components/messaging/ChannelMemberAvatarStack.svelte +69 -0
  154. package/dist/components/messaging/ChannelMemberAvatarStack.svelte.d.ts +11 -0
  155. package/dist/components/messaging/ChannelMembersModal.svelte +182 -0
  156. package/dist/components/messaging/ChannelMembersModal.svelte.d.ts +16 -0
  157. package/dist/components/messaging/CreateChannelModal.svelte +190 -0
  158. package/dist/components/messaging/CreateChannelModal.svelte.d.ts +14 -0
  159. package/dist/components/messaging/DirectMessageList.svelte +145 -0
  160. package/dist/components/messaging/DirectMessageList.svelte.d.ts +12 -0
  161. package/dist/components/messaging/EmojiSelector.svelte +261 -0
  162. package/dist/components/messaging/EmojiSelector.svelte.d.ts +19 -0
  163. package/dist/components/messaging/MentionAutocomplete.svelte +193 -0
  164. package/dist/components/messaging/MentionAutocomplete.svelte.d.ts +17 -0
  165. package/dist/components/messaging/MessageAttachment.svelte +273 -0
  166. package/dist/components/messaging/MessageAttachment.svelte.d.ts +13 -0
  167. package/dist/components/messaging/MessageAttachmentUpload.svelte +243 -0
  168. package/dist/components/messaging/MessageAttachmentUpload.svelte.d.ts +19 -0
  169. package/dist/components/messaging/MessageInput.svelte +452 -0
  170. package/dist/components/messaging/MessageInput.svelte.d.ts +28 -0
  171. package/dist/components/messaging/MessageItem.svelte +341 -0
  172. package/dist/components/messaging/MessageItem.svelte.d.ts +41 -0
  173. package/dist/components/messaging/MessageThread.svelte +306 -0
  174. package/dist/components/messaging/MessageThread.svelte.d.ts +30 -0
  175. package/dist/components/messaging/NotificationSettingsModal.svelte +234 -0
  176. package/dist/components/messaging/NotificationSettingsModal.svelte.d.ts +13 -0
  177. package/dist/components/messaging/QuotedMessageDisplay.svelte +123 -0
  178. package/dist/components/messaging/QuotedMessageDisplay.svelte.d.ts +10 -0
  179. package/dist/components/messaging/StartDMModal.svelte +100 -0
  180. package/dist/components/messaging/StartDMModal.svelte.d.ts +14 -0
  181. package/dist/components/messaging/ThreadPanel.svelte +153 -0
  182. package/dist/components/messaging/ThreadPanel.svelte.d.ts +16 -0
  183. package/dist/components/pipeline/DESIGN.md +361 -0
  184. package/dist/components/pipeline/Pipeline.svelte +391 -0
  185. package/dist/components/pipeline/Pipeline.svelte.d.ts +28 -0
  186. package/dist/components/pipeline/PipelineCard.svelte +94 -0
  187. package/dist/components/pipeline/PipelineCard.svelte.d.ts +13 -0
  188. package/dist/components/pipeline/PipelineColumn.svelte +163 -0
  189. package/dist/components/pipeline/PipelineColumn.svelte.d.ts +30 -0
  190. package/dist/components/replay/ChapterTimeline.svelte +326 -0
  191. package/dist/components/replay/ChapterTimeline.svelte.d.ts +39 -0
  192. package/dist/components/session-nav/transcriptModel.d.ts +132 -0
  193. package/dist/components/session-nav/transcriptModel.js +285 -0
  194. package/dist/data/emojis.d.ts +18 -0
  195. package/dist/data/emojis.js +1196 -0
  196. package/dist/index.d.ts +142 -0
  197. package/dist/index.js +142 -0
  198. package/dist/stores/confirmDialog.svelte.d.ts +21 -0
  199. package/dist/stores/confirmDialog.svelte.js +31 -0
  200. package/dist/stores/inputDialog.svelte.d.ts +24 -0
  201. package/dist/stores/inputDialog.svelte.js +32 -0
  202. package/dist/styles/rail.css +63 -0
  203. package/dist/types/annotation.d.ts +32 -0
  204. package/dist/types/annotation.js +3 -0
  205. package/dist/types/booking.d.ts +115 -0
  206. package/dist/types/booking.js +24 -0
  207. package/dist/types/comments.d.ts +65 -0
  208. package/dist/types/comments.js +22 -0
  209. package/dist/types/entityPreview.d.ts +42 -0
  210. package/dist/types/entityPreview.js +10 -0
  211. package/dist/types/filePicker.d.ts +15 -0
  212. package/dist/types/filePicker.js +1 -0
  213. package/dist/types/googleMaps.d.ts +51 -0
  214. package/dist/types/maps.d.ts +43 -0
  215. package/dist/types/maps.js +4 -0
  216. package/dist/types/messaging.d.ts +282 -0
  217. package/dist/types/messaging.js +100 -0
  218. package/dist/types/milestone.d.ts +20 -0
  219. package/dist/types/milestone.js +7 -0
  220. package/dist/types/pipeline.d.ts +115 -0
  221. package/dist/types/pipeline.js +15 -0
  222. package/dist/types/smartImageEditor.d.ts +42 -0
  223. package/dist/types/smartImageEditor.js +1 -0
  224. package/dist/types/templateVars.d.ts +34 -0
  225. package/dist/types/templateVars.js +12 -0
  226. package/dist/utils/currency.d.ts +8 -0
  227. package/dist/utils/currency.js +17 -0
  228. package/dist/utils/daisyuiColors.d.ts +37 -0
  229. package/dist/utils/daisyuiColors.js +152 -0
  230. package/dist/utils/dateFormatters.d.ts +69 -0
  231. package/dist/utils/dateFormatters.js +199 -0
  232. package/dist/utils/googleMapsLoader.d.ts +19 -0
  233. package/dist/utils/googleMapsLoader.js +121 -0
  234. package/dist/utils/mentionParser.d.ts +67 -0
  235. package/dist/utils/mentionParser.js +132 -0
  236. package/dist/utils/phone.d.ts +13 -0
  237. package/dist/utils/phone.js +67 -0
  238. package/dist/utils/phoneFormat.d.ts +26 -0
  239. package/dist/utils/phoneFormat.js +69 -0
  240. package/dist/utils/taskUtils.d.ts +37 -0
  241. package/dist/utils/taskUtils.js +71 -0
  242. package/package.json +3 -2
@@ -0,0 +1,182 @@
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}
@@ -0,0 +1,16 @@
1
+ import type { ChannelMember, OrgMember, ChannelCallbacks } from '../../types/messaging';
2
+ interface Props {
3
+ channelId: string;
4
+ channelName: string;
5
+ currentUserRole: 'admin' | 'moderator' | 'member';
6
+ currentUserId: string;
7
+ open: boolean;
8
+ members?: ChannelMember[];
9
+ orgMembers?: OrgMember[];
10
+ channelCallbacks: ChannelCallbacks;
11
+ onclose?: () => void;
12
+ onmemberupdate?: () => void;
13
+ }
14
+ declare const ChannelMembersModal: import("svelte").Component<Props, {}, "">;
15
+ type ChannelMembersModal = ReturnType<typeof ChannelMembersModal>;
16
+ export default ChannelMembersModal;
@@ -0,0 +1,190 @@
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}
@@ -0,0 +1,14 @@
1
+ import type { Channel, OrgMember, ChannelCallbacks } from '../../types/messaging';
2
+ interface Props {
3
+ open: boolean;
4
+ currentUserId: string;
5
+ orgMembers?: OrgMember[];
6
+ channelCallbacks: ChannelCallbacks;
7
+ onclose?: () => void;
8
+ onchannelcreated?: (event: {
9
+ channel: Channel;
10
+ }) => void;
11
+ }
12
+ declare const CreateChannelModal: import("svelte").Component<Props, {}, "">;
13
+ type CreateChannelModal = ReturnType<typeof CreateChannelModal>;
14
+ export default CreateChannelModal;
@@ -0,0 +1,145 @@
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>
@@ -0,0 +1,12 @@
1
+ import type { DirectConversation, OrgMember } from '../../types/messaging';
2
+ interface Props {
3
+ conversations: DirectConversation[];
4
+ orgMembers: OrgMember[];
5
+ activeDMUserId?: string | null;
6
+ onconversationselect?: (data: {
7
+ userId: string;
8
+ }) => void;
9
+ }
10
+ declare const DirectMessageList: import("svelte").Component<Props, {}, "">;
11
+ type DirectMessageList = ReturnType<typeof DirectMessageList>;
12
+ export default DirectMessageList;