@joewinke/jatui 0.1.23 → 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 -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,287 +0,0 @@
1
- // Markup drawing primitives for construction drawing redlining
2
- // Adapted from JAT feedback annotation engine with cloud bubble tool added
3
-
4
- export type MarkupTool =
5
- | "arrow"
6
- | "rectangle"
7
- | "ellipse"
8
- | "cloud"
9
- | "freehand"
10
- | "text"
11
-
12
- export interface Point {
13
- x: number
14
- y: number
15
- }
16
-
17
- export interface BaseShape {
18
- id: number
19
- type: MarkupTool
20
- color: string
21
- strokeWidth: number
22
- }
23
-
24
- export interface ArrowShape extends BaseShape {
25
- type: "arrow"
26
- start: Point
27
- end: Point
28
- }
29
-
30
- export interface RectangleShape extends BaseShape {
31
- type: "rectangle"
32
- start: Point
33
- end: Point
34
- }
35
-
36
- export interface EllipseShape extends BaseShape {
37
- type: "ellipse"
38
- start: Point
39
- end: Point
40
- }
41
-
42
- export interface CloudShape extends BaseShape {
43
- type: "cloud"
44
- start: Point
45
- end: Point
46
- }
47
-
48
- export interface FreehandShape extends BaseShape {
49
- type: "freehand"
50
- points: Point[]
51
- }
52
-
53
- export interface TextShape extends BaseShape {
54
- type: "text"
55
- position: Point
56
- content: string
57
- fontSize: number
58
- }
59
-
60
- export type MarkupShape =
61
- | ArrowShape
62
- | RectangleShape
63
- | EllipseShape
64
- | CloudShape
65
- | FreehandShape
66
- | TextShape
67
-
68
- export const MARKUP_COLORS = [
69
- "#ef4444",
70
- "#eab308",
71
- "#22c55e",
72
- "#3b82f6",
73
- "#ffffff",
74
- "#111827",
75
- ]
76
-
77
- export const DEFAULT_STROKE_WIDTH = 3
78
-
79
- let _nextId = 1
80
- export function nextShapeId(): number {
81
- return _nextId++
82
- }
83
-
84
- function drawArrow(ctx: CanvasRenderingContext2D, shape: ArrowShape) {
85
- const { start, end, color, strokeWidth } = shape
86
- ctx.strokeStyle = color
87
- ctx.lineWidth = strokeWidth
88
- ctx.lineCap = "round"
89
- ctx.lineJoin = "round"
90
-
91
- ctx.beginPath()
92
- ctx.moveTo(start.x, start.y)
93
- ctx.lineTo(end.x, end.y)
94
- ctx.stroke()
95
-
96
- // Arrowhead
97
- const angle = Math.atan2(end.y - start.y, end.x - start.x)
98
- const headLen = 14
99
- const headAngle = Math.PI / 7
100
- ctx.beginPath()
101
- ctx.moveTo(end.x, end.y)
102
- ctx.lineTo(
103
- end.x - headLen * Math.cos(angle - headAngle),
104
- end.y - headLen * Math.sin(angle - headAngle),
105
- )
106
- ctx.moveTo(end.x, end.y)
107
- ctx.lineTo(
108
- end.x - headLen * Math.cos(angle + headAngle),
109
- end.y - headLen * Math.sin(angle + headAngle),
110
- )
111
- ctx.stroke()
112
- }
113
-
114
- function drawRectangle(ctx: CanvasRenderingContext2D, shape: RectangleShape) {
115
- const { start, end, color, strokeWidth } = shape
116
- ctx.strokeStyle = color
117
- ctx.lineWidth = strokeWidth
118
- ctx.lineJoin = "round"
119
- const x = Math.min(start.x, end.x)
120
- const y = Math.min(start.y, end.y)
121
- const w = Math.abs(end.x - start.x)
122
- const h = Math.abs(end.y - start.y)
123
- ctx.strokeRect(x, y, w, h)
124
- }
125
-
126
- function drawEllipse(ctx: CanvasRenderingContext2D, shape: EllipseShape) {
127
- const { start, end, color, strokeWidth } = shape
128
- ctx.strokeStyle = color
129
- ctx.lineWidth = strokeWidth
130
- const cx = (start.x + end.x) / 2
131
- const cy = (start.y + end.y) / 2
132
- const rx = Math.abs(end.x - start.x) / 2
133
- const ry = Math.abs(end.y - start.y) / 2
134
- if (rx < 1 || ry < 1) return
135
- ctx.beginPath()
136
- ctx.ellipse(cx, cy, rx, ry, 0, 0, Math.PI * 2)
137
- ctx.stroke()
138
- }
139
-
140
- /**
141
- * Revision cloud — a series of small arcs along the perimeter of a rectangle.
142
- * Standard construction markup tool for highlighting areas of change.
143
- */
144
- function drawCloud(ctx: CanvasRenderingContext2D, shape: CloudShape) {
145
- const { start, end, color, strokeWidth } = shape
146
- ctx.strokeStyle = color
147
- ctx.lineWidth = strokeWidth
148
- ctx.lineCap = "round"
149
- ctx.lineJoin = "round"
150
-
151
- const x = Math.min(start.x, end.x)
152
- const y = Math.min(start.y, end.y)
153
- const w = Math.abs(end.x - start.x)
154
- const h = Math.abs(end.y - start.y)
155
- if (w < 4 || h < 4) return
156
-
157
- // Generate perimeter points, then draw arcs between them
158
- const arcSize = Math.min(w, h, 30) // arc diameter adapts to shape size
159
- const perimeter = 2 * (w + h)
160
- const numArcs = Math.max(8, Math.round(perimeter / arcSize))
161
-
162
- // Collect points along the rectangle perimeter
163
- const points: Point[] = []
164
- const segLengths = [w, h, w, h]
165
- const segDirs = [
166
- { dx: 1, dy: 0 },
167
- { dx: 0, dy: 1 },
168
- { dx: -1, dy: 0 },
169
- { dx: 0, dy: -1 },
170
- ]
171
- const segStarts: Point[] = [
172
- { x, y },
173
- { x: x + w, y },
174
- { x: x + w, y: y + h },
175
- { x, y: y + h },
176
- ]
177
-
178
- let totalLen = 0
179
- for (const l of segLengths) totalLen += l
180
- const step = totalLen / numArcs
181
-
182
- let dist = 0
183
- let segIdx = 0
184
- let segDist = 0
185
-
186
- for (let i = 0; i < numArcs; i++) {
187
- const target = i * step
188
- while (dist + segLengths[segIdx] - segDist < target && segIdx < 3) {
189
- dist += segLengths[segIdx] - segDist
190
- segIdx++
191
- segDist = 0
192
- }
193
- const remaining = target - dist
194
- points.push({
195
- x: segStarts[segIdx].x + segDirs[segIdx].dx * (segDist + remaining),
196
- y: segStarts[segIdx].y + segDirs[segIdx].dy * (segDist + remaining),
197
- })
198
- }
199
-
200
- // Draw arcs between consecutive points
201
- ctx.beginPath()
202
- for (let i = 0; i < points.length; i++) {
203
- const p1 = points[i]
204
- const p2 = points[(i + 1) % points.length]
205
- const mx = (p1.x + p2.x) / 2
206
- const my = (p1.y + p2.y) / 2
207
- // Bulge outward from center of rectangle
208
- const cx = x + w / 2
209
- const cy = y + h / 2
210
- const dx = mx - cx
211
- const dy = my - cy
212
- const len = Math.sqrt(dx * dx + dy * dy) || 1
213
- const bulge = arcSize * 0.35
214
- const ctrlX = mx + (dx / len) * bulge
215
- const ctrlY = my + (dy / len) * bulge
216
-
217
- if (i === 0) {
218
- ctx.moveTo(p1.x, p1.y)
219
- }
220
- ctx.quadraticCurveTo(ctrlX, ctrlY, p2.x, p2.y)
221
- }
222
- ctx.closePath()
223
- ctx.stroke()
224
- }
225
-
226
- function drawFreehand(ctx: CanvasRenderingContext2D, shape: FreehandShape) {
227
- const { points, color, strokeWidth } = shape
228
- if (points.length < 2) return
229
- ctx.strokeStyle = color
230
- ctx.lineWidth = strokeWidth
231
- ctx.lineCap = "round"
232
- ctx.lineJoin = "round"
233
- ctx.beginPath()
234
- ctx.moveTo(points[0].x, points[0].y)
235
- for (let i = 1; i < points.length; i++) {
236
- ctx.lineTo(points[i].x, points[i].y)
237
- }
238
- ctx.stroke()
239
- }
240
-
241
- function drawText(ctx: CanvasRenderingContext2D, shape: TextShape) {
242
- const { position, content, color, fontSize } = shape
243
- if (!content) return
244
- ctx.font = `bold ${fontSize}px sans-serif`
245
- ctx.textBaseline = "top"
246
- // Black outline for contrast on construction drawings
247
- ctx.strokeStyle = "#000000"
248
- ctx.lineWidth = 2
249
- ctx.lineJoin = "round"
250
- ctx.strokeText(content, position.x, position.y)
251
- ctx.fillStyle = color
252
- ctx.fillText(content, position.x, position.y)
253
- }
254
-
255
- export function renderShape(ctx: CanvasRenderingContext2D, shape: MarkupShape) {
256
- ctx.save()
257
- switch (shape.type) {
258
- case "arrow":
259
- drawArrow(ctx, shape)
260
- break
261
- case "rectangle":
262
- drawRectangle(ctx, shape)
263
- break
264
- case "ellipse":
265
- drawEllipse(ctx, shape)
266
- break
267
- case "cloud":
268
- drawCloud(ctx, shape)
269
- break
270
- case "freehand":
271
- drawFreehand(ctx, shape)
272
- break
273
- case "text":
274
- drawText(ctx, shape)
275
- break
276
- }
277
- ctx.restore()
278
- }
279
-
280
- export function renderAllShapes(
281
- ctx: CanvasRenderingContext2D,
282
- shapes: MarkupShape[],
283
- ) {
284
- for (const shape of shapes) {
285
- renderShape(ctx, shape)
286
- }
287
- }
@@ -1,81 +0,0 @@
1
- <!--
2
- Simple Avatar Component for Messaging
3
-
4
- Shows user avatar image with fallback to initials.
5
- Used internally by messaging components.
6
- -->
7
-
8
- <script lang="ts">
9
- interface Props {
10
- name: string
11
- avatarUrl?: string | null
12
- size?: 'xs' | 'small' | 'medium' | 'large'
13
- showStatus?: boolean
14
- status?: 'online' | 'offline' | 'away' | 'busy'
15
- class?: string
16
- }
17
-
18
- let {
19
- name,
20
- avatarUrl = null,
21
- size = 'small',
22
- showStatus = false,
23
- status = 'offline',
24
- class: className = '',
25
- }: Props = $props()
26
-
27
- const sizeClasses: Record<string, string> = {
28
- xs: 'w-5 h-5 text-[10px]',
29
- small: 'w-8 h-8 text-xs',
30
- medium: 'w-10 h-10 text-sm',
31
- large: 'w-12 h-12 text-base',
32
- }
33
-
34
- const statusDotSize: Record<string, string> = {
35
- xs: 'w-1.5 h-1.5',
36
- small: 'w-2 h-2',
37
- medium: 'w-2.5 h-2.5',
38
- large: 'w-3 h-3',
39
- }
40
-
41
- const statusColors: Record<string, string> = {
42
- online: 'bg-success',
43
- offline: 'bg-base-content/30',
44
- away: 'bg-warning',
45
- busy: 'bg-error',
46
- }
47
-
48
- function getInitials(fullName: string): string {
49
- return fullName
50
- .split(' ')
51
- .map((part) => part[0])
52
- .filter(Boolean)
53
- .slice(0, 2)
54
- .join('')
55
- .toUpperCase()
56
- }
57
-
58
- const initials = $derived(getInitials(name))
59
- </script>
60
-
61
- <div class="relative inline-flex flex-shrink-0 {className}">
62
- {#if avatarUrl}
63
- <img
64
- src={avatarUrl}
65
- alt={name}
66
- class="rounded-full object-cover {sizeClasses[size]}"
67
- />
68
- {:else}
69
- <div
70
- class="rounded-full bg-neutral text-neutral-content flex items-center justify-center font-medium {sizeClasses[size]}"
71
- >
72
- {initials}
73
- </div>
74
- {/if}
75
-
76
- {#if showStatus}
77
- <span
78
- class="absolute bottom-0 right-0 rounded-full border-2 border-base-100 {statusDotSize[size]} {statusColors[status]}"
79
- ></span>
80
- {/if}
81
- </div>
@@ -1,163 +0,0 @@
1
- <!--
2
- ChannelInfoModal Component
3
-
4
- Displays channel information and allows admins to edit channel details.
5
- All updates via callbacks.
6
- -->
7
-
8
- <script lang="ts">
9
- import type { Channel, ChannelCallbacks } from '../../types/messaging'
10
-
11
- interface Props {
12
- channel: Channel & { memberCount?: number; createdBy?: string; creatorName?: string }
13
- currentUserRole: 'admin' | 'moderator' | 'member'
14
- open: boolean
15
- channelCallbacks?: ChannelCallbacks
16
- onclose?: () => void
17
- onchannelupdate?: (updatedChannel: Channel) => void
18
- onchanneldelete?: (channelId: string) => void
19
- onmanagemembers?: () => void
20
- }
21
-
22
- let {
23
- channel,
24
- currentUserRole,
25
- open = false,
26
- channelCallbacks,
27
- onclose,
28
- onchannelupdate,
29
- onchanneldelete,
30
- onmanagemembers,
31
- }: Props = $props()
32
-
33
- let isEditing = $state(false)
34
- let editName = $state('')
35
- let editDescription = $state('')
36
- let saving = $state(false)
37
- let showDeleteConfirm = $state(false)
38
- let error = $state<string | null>(null)
39
-
40
- const isAdmin = $derived(currentUserRole === 'admin')
41
-
42
- function startEdit() {
43
- editName = channel.name
44
- editDescription = channel.description || ''
45
- isEditing = true
46
- }
47
-
48
- async function saveEdit() {
49
- if (!channelCallbacks || !editName.trim()) return
50
- saving = true
51
- error = null
52
- try {
53
- const updated = await channelCallbacks.updateChannel(channel.id, {
54
- name: editName.trim(),
55
- description: editDescription.trim() || undefined,
56
- })
57
- onchannelupdate?.(updated)
58
- isEditing = false
59
- } catch (err) {
60
- error = err instanceof Error ? err.message : 'Failed to update channel'
61
- } finally {
62
- saving = false
63
- }
64
- }
65
-
66
- async function handleDelete() {
67
- if (!channelCallbacks) return
68
- try {
69
- await channelCallbacks.deleteChannel(channel.id)
70
- onchanneldelete?.(channel.id)
71
- onclose?.()
72
- } catch (err) {
73
- error = err instanceof Error ? err.message : 'Failed to delete channel'
74
- }
75
- }
76
-
77
- function formatDate(dateString: string): string {
78
- return new Date(dateString).toLocaleDateString('en-US', { month: 'long', day: 'numeric', year: 'numeric' })
79
- }
80
- </script>
81
-
82
- {#if open}
83
- <dialog class="modal modal-open">
84
- <div class="modal-box max-w-md">
85
- <div class="flex items-center justify-between mb-4">
86
- <h3 class="font-semibold">
87
- {channel.emoji || '#'} {channel.name}
88
- </h3>
89
- <button class="btn btn-ghost btn-sm btn-circle" onclick={onclose}>✕</button>
90
- </div>
91
-
92
- {#if error}
93
- <div class="alert alert-error mb-4"><span class="text-sm">{error}</span></div>
94
- {/if}
95
-
96
- {#if isEditing}
97
- <div class="space-y-4">
98
- <div>
99
- <label for="channel-info-name" class="label"><span class="text-[0.8125rem] font-medium text-base-content/85">Name</span></label>
100
- <input id="channel-info-name" type="text" bind:value={editName} class="input input-bordered w-full" maxlength="80" />
101
- </div>
102
- <div>
103
- <label for="channel-info-description" class="label"><span class="text-[0.8125rem] font-medium text-base-content/85">Description</span></label>
104
- <textarea id="channel-info-description" bind:value={editDescription} class="textarea textarea-bordered w-full" rows="3" maxlength="250"></textarea>
105
- </div>
106
- <div class="flex gap-2">
107
- <button class="btn btn-primary btn-sm" onclick={saveEdit} disabled={saving || !editName.trim()}>
108
- {#if saving}<span class="loading loading-spinner loading-xs"></span>{/if}
109
- Save
110
- </button>
111
- <button class="btn btn-ghost btn-sm" onclick={() => (isEditing = false)}>Cancel</button>
112
- </div>
113
- </div>
114
- {:else}
115
- <div class="space-y-4">
116
- <div>
117
- <span class="text-[0.8125rem] text-base-content/45 tracking-[0.005em]">Type</span>
118
- <p class="text-[0.9375rem]">{channel.type === 'private' ? '🔒 Private' : '# Public'}</p>
119
- </div>
120
-
121
- {#if channel.description}
122
- <div>
123
- <span class="text-[0.8125rem] text-base-content/45 tracking-[0.005em]">Description</span>
124
- <p class="text-[0.9375rem]">{channel.description}</p>
125
- </div>
126
- {/if}
127
-
128
- <div>
129
- <span class="text-[0.8125rem] text-base-content/45 tracking-[0.005em]">Created</span>
130
- <p class="text-[0.9375rem]">{formatDate(channel.createdAt)}{channel.creatorName ? ` by ${channel.creatorName}` : ''}</p>
131
- </div>
132
-
133
- <div class="flex flex-wrap gap-2">
134
- {#if isAdmin}
135
- <button class="btn btn-sm btn-ghost" onclick={startEdit}>✏ Edit</button>
136
- {/if}
137
- {#if onmanagemembers}
138
- <button class="btn btn-sm btn-ghost" onclick={onmanagemembers}>👥 Members</button>
139
- {/if}
140
- {#if isAdmin}
141
- {#if showDeleteConfirm}
142
- <div class="flex items-center gap-2">
143
- <span class="text-sm text-error">Delete this channel?</span>
144
- <button class="btn btn-sm btn-error" onclick={handleDelete}>Confirm</button>
145
- <button class="btn btn-sm btn-ghost" onclick={() => (showDeleteConfirm = false)}>Cancel</button>
146
- </div>
147
- {:else}
148
- <button class="btn btn-sm btn-ghost text-error" onclick={() => (showDeleteConfirm = true)}>🗑 Delete</button>
149
- {/if}
150
- {/if}
151
- </div>
152
- </div>
153
- {/if}
154
-
155
- <div class="modal-action">
156
- <button class="btn btn-ghost" onclick={onclose}>Close</button>
157
- </div>
158
- </div>
159
- <form method="dialog" class="modal-backdrop">
160
- <button onclick={onclose}>close</button>
161
- </form>
162
- </dialog>
163
- {/if}
@@ -1,107 +0,0 @@
1
- <!--
2
- ChannelList Component
3
-
4
- Displays list of channels with unread counts.
5
- Purely presentational — receives channels via props.
6
- -->
7
-
8
- <script lang="ts">
9
- import type { Channel } from '../../types/messaging'
10
-
11
- interface Props {
12
- channels: Channel[]
13
- activeChannelId?: string | null
14
- onchannelselect?: (event: { channelId: string }) => void
15
- }
16
-
17
- let {
18
- channels = [],
19
- activeChannelId = null,
20
- onchannelselect,
21
- }: Props = $props()
22
-
23
- function selectChannel(channelId: string) {
24
- onchannelselect?.({ channelId })
25
- }
26
-
27
- function getChannelIconColor(type: string) {
28
- switch (type) {
29
- case 'private': return 'text-warning'
30
- case 'direct': return 'text-info'
31
- default: return 'text-base-content/50'
32
- }
33
- }
34
-
35
- const sortedChannels = $derived(
36
- [...(channels || [])].sort((a, b) => {
37
- const typeOrder = { public: 0, private: 1, direct: 2 } as Record<string, number>
38
- const aOrder = typeOrder[a.type] ?? 3
39
- const bOrder = typeOrder[b.type] ?? 3
40
- if (aOrder !== bOrder) return aOrder - bOrder
41
- return a.name.localeCompare(b.name)
42
- }),
43
- )
44
- </script>
45
-
46
- <div class="space-y-1">
47
- {#each sortedChannels as channel}
48
- {@const iconColor = getChannelIconColor(channel.type)}
49
-
50
- <button
51
- class="w-full flex items-center gap-2 px-2 py-1.5 text-left rounded hover:bg-base-300 transition-colors group"
52
- class:bg-primary={activeChannelId === channel.id}
53
- class:text-primary-content={activeChannelId === channel.id}
54
- class:font-medium={activeChannelId === channel.id}
55
- onclick={() => selectChannel(channel.id)}
56
- >
57
- <!-- Channel icon: emoji, lock, users, or # -->
58
- <span
59
- class="w-4 h-4 flex-shrink-0 text-sm flex items-center justify-center {activeChannelId === channel.id ? 'text-primary-content' : iconColor}"
60
- >
61
- {#if channel.type === 'private'}
62
- 🔒
63
- {:else if channel.type === 'direct'}
64
- 👥
65
- {:else}
66
- {channel.emoji || '#'}
67
- {/if}
68
- </span>
69
-
70
- <div class="flex-1 min-w-0">
71
- <div class="flex items-center justify-between">
72
- <span class="text-sm truncate">
73
- {channel.name}
74
- </span>
75
-
76
- {#if channel.unreadCount && channel.unreadCount > 0}
77
- <span class="badge badge-error badge-xs text-error-content">
78
- {channel.unreadCount > 99 ? '99+' : channel.unreadCount}
79
- </span>
80
- {/if}
81
- </div>
82
-
83
- {#if channel.description && activeChannelId !== channel.id}
84
- <div class="text-xs text-base-content/50 truncate">
85
- {channel.description}
86
- </div>
87
- {/if}
88
- </div>
89
-
90
- <!-- Admin indicator -->
91
- {#if channel.memberRole === 'admin'}
92
- <div
93
- class="w-2 h-2 rounded-full bg-warning opacity-0 group-hover:opacity-100 transition-opacity"
94
- title="Channel Admin"
95
- ></div>
96
- {/if}
97
- </button>
98
- {/each}
99
-
100
- {#if channels.length === 0}
101
- <div class="text-center py-8 text-base-content/50">
102
- <div class="text-2xl mb-2">#</div>
103
- <div class="text-sm">No channels yet</div>
104
- <div class="text-xs">Create one to get started</div>
105
- </div>
106
- {/if}
107
- </div>
@@ -1,69 +0,0 @@
1
- <!--
2
- ChannelMemberAvatarStack Component
3
-
4
- Displays a stack of member avatars with overflow count.
5
- Purely presentational — no backend dependencies.
6
- -->
7
-
8
- <script lang="ts">
9
- import type { ChannelMember, PresenceState } from '../../types/messaging'
10
- import Avatar from './Avatar.svelte'
11
-
12
- interface Props {
13
- members: ChannelMember[]
14
- presenceState?: PresenceState
15
- maxVisible?: number
16
- size?: 'xs' | 'small' | 'medium' | 'large'
17
- class?: string
18
- }
19
-
20
- let {
21
- members = [],
22
- presenceState = {},
23
- maxVisible = 4,
24
- size = 'small',
25
- class: className = '',
26
- }: Props = $props()
27
-
28
- const visibleMembers = $derived(members.slice(0, maxVisible))
29
- const remainingCount = $derived(Math.max(0, members.length - maxVisible))
30
-
31
- function getStatus(member: ChannelMember): 'online' | 'offline' | 'away' | 'busy' {
32
- const presence = presenceState[member.userId]
33
- if (presence?.status === 'online' || presence?.status === 'available') {
34
- return 'online'
35
- }
36
- return 'offline'
37
- }
38
-
39
- const overflowSize: Record<string, string> = {
40
- xs: 'w-5 h-5 text-[8px]',
41
- small: 'w-8 h-8 text-xs',
42
- medium: 'w-10 h-10 text-sm',
43
- large: 'w-12 h-12 text-base',
44
- }
45
- </script>
46
-
47
- <div class="flex -space-x-2 {className}">
48
- {#each visibleMembers as member (member.id)}
49
- <div class="border-2 border-base-100 rounded-full">
50
- <Avatar
51
- name={member.fullName}
52
- avatarUrl={member.avatarUrl}
53
- {size}
54
- showStatus={true}
55
- status={getStatus(member)}
56
- />
57
- </div>
58
- {/each}
59
-
60
- {#if remainingCount > 0}
61
- <div class="border-2 border-base-100 rounded-full">
62
- <div
63
- class="bg-neutral text-neutral-content rounded-full flex items-center justify-center {overflowSize[size]}"
64
- >
65
- <span class="font-bold">+{remainingCount}</span>
66
- </div>
67
- </div>
68
- {/if}
69
- </div>