@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.
- package/CLAUDE.md +244 -0
- package/package.json +23 -10
- package/src/lib/actions/collapse.ts +0 -136
- package/src/lib/actions/railNav.ts +0 -473
- package/src/lib/components/AnnotationLayer.svelte +0 -108
- package/src/lib/components/AnnotationPanel.svelte +0 -319
- package/src/lib/components/AudioWaveform.svelte +0 -698
- package/src/lib/components/AvailabilityModal.svelte +0 -177
- package/src/lib/components/AvatarUpload.svelte +0 -234
- package/src/lib/components/Badge.svelte +0 -38
- package/src/lib/components/BookingForm.svelte +0 -278
- package/src/lib/components/BurndownChart.svelte +0 -778
- package/src/lib/components/Button.svelte +0 -81
- package/src/lib/components/CalendarPicker.svelte +0 -284
- package/src/lib/components/Card.svelte +0 -67
- package/src/lib/components/CharacterCounter.svelte +0 -82
- package/src/lib/components/ChipInput.svelte +0 -609
- package/src/lib/components/Collapse.svelte +0 -82
- package/src/lib/components/ColorSelector.svelte +0 -167
- package/src/lib/components/CommentThread.svelte +0 -773
- package/src/lib/components/ConfirmDialog.svelte +0 -348
- package/src/lib/components/ConfirmModal.svelte +0 -142
- package/src/lib/components/ContextMenu.svelte +0 -188
- package/src/lib/components/CountdownTimer.svelte +0 -94
- package/src/lib/components/DateRangePicker.svelte +0 -194
- package/src/lib/components/Drawer.svelte +0 -143
- package/src/lib/components/EmojiPicker.svelte +0 -159
- package/src/lib/components/EntityPreviewCard.svelte +0 -104
- package/src/lib/components/FileDropzone.svelte +0 -493
- package/src/lib/components/FilePicker.svelte +0 -488
- package/src/lib/components/FileThumbnail.svelte +0 -80
- package/src/lib/components/FilterDropdown.svelte +0 -203
- package/src/lib/components/FloatingActionBar.svelte +0 -354
- package/src/lib/components/GPSTracker.svelte +0 -202
- package/src/lib/components/HunkDiffView.svelte +0 -348
- package/src/lib/components/ImageLightbox.svelte +0 -274
- package/src/lib/components/ImageUpload.svelte +0 -147
- package/src/lib/components/InlineEdit.svelte +0 -291
- package/src/lib/components/InputDialog.svelte +0 -327
- package/src/lib/components/KeyboardShortcutsOverlay.svelte +0 -296
- package/src/lib/components/LazyImage.svelte +0 -123
- package/src/lib/components/LinkShortener.svelte +0 -274
- package/src/lib/components/LoadingSpinner.svelte +0 -106
- package/src/lib/components/LocationMap.svelte +0 -186
- package/src/lib/components/MapView.svelte +0 -341
- package/src/lib/components/MarkupEditor.svelte +0 -485
- package/src/lib/components/MarkupOverlay.svelte +0 -55
- package/src/lib/components/MediaWorkbench.svelte +0 -871
- package/src/lib/components/MilestoneCard.svelte +0 -98
- package/src/lib/components/MilestoneTimeline.svelte +0 -91
- package/src/lib/components/Modal.svelte +0 -244
- package/src/lib/components/PDFViewer.svelte +0 -105
- package/src/lib/components/PdfThumbnail.svelte +0 -63
- package/src/lib/components/PhoneInput.svelte +0 -212
- package/src/lib/components/ResizableDivider.svelte +0 -307
- package/src/lib/components/ResizablePanel.svelte +0 -305
- package/src/lib/components/SearchDropdown.svelte +0 -421
- package/src/lib/components/SelectInput.svelte +0 -237
- package/src/lib/components/SidebarUserFooter.svelte +0 -34
- package/src/lib/components/SignaturePad.svelte +0 -175
- package/src/lib/components/SmartImageEditor.svelte +0 -720
- package/src/lib/components/SortDropdown.svelte +0 -156
- package/src/lib/components/Sparkline.svelte +0 -116
- package/src/lib/components/SpeechForm.svelte +0 -114
- package/src/lib/components/StatusBadge.svelte +0 -157
- package/src/lib/components/TaskTypeIcon.svelte +0 -54
- package/src/lib/components/TextArea.svelte +0 -162
- package/src/lib/components/TextInput.svelte +0 -131
- package/src/lib/components/ThemeSelector.svelte +0 -206
- package/src/lib/components/TimeSlotPicker.svelte +0 -162
- package/src/lib/components/UserAvatar.svelte +0 -72
- package/src/lib/components/VariablePicker.svelte +0 -170
- package/src/lib/components/VoicePlayer.svelte +0 -422
- package/src/lib/components/linked-columns/LinkedColumns.svelte +0 -526
- package/src/lib/components/markup.ts +0 -287
- package/src/lib/components/messaging/Avatar.svelte +0 -81
- package/src/lib/components/messaging/ChannelInfoModal.svelte +0 -163
- package/src/lib/components/messaging/ChannelList.svelte +0 -107
- package/src/lib/components/messaging/ChannelMemberAvatarStack.svelte +0 -69
- package/src/lib/components/messaging/ChannelMembersModal.svelte +0 -182
- package/src/lib/components/messaging/CreateChannelModal.svelte +0 -190
- package/src/lib/components/messaging/DirectMessageList.svelte +0 -145
- package/src/lib/components/messaging/EmojiSelector.svelte +0 -261
- package/src/lib/components/messaging/MentionAutocomplete.svelte +0 -193
- package/src/lib/components/messaging/MessageAttachment.svelte +0 -273
- package/src/lib/components/messaging/MessageAttachmentUpload.svelte +0 -243
- package/src/lib/components/messaging/MessageInput.svelte +0 -452
- package/src/lib/components/messaging/MessageItem.svelte +0 -341
- package/src/lib/components/messaging/MessageThread.svelte +0 -306
- package/src/lib/components/messaging/NotificationSettingsModal.svelte +0 -234
- package/src/lib/components/messaging/QuotedMessageDisplay.svelte +0 -123
- package/src/lib/components/messaging/StartDMModal.svelte +0 -100
- package/src/lib/components/messaging/ThreadPanel.svelte +0 -153
- package/src/lib/components/pipeline/DESIGN.md +0 -361
- package/src/lib/components/pipeline/Pipeline.svelte +0 -391
- package/src/lib/components/pipeline/PipelineCard.svelte +0 -94
- package/src/lib/components/pipeline/PipelineColumn.svelte +0 -163
- package/src/lib/components/replay/ChapterTimeline.svelte +0 -326
- package/src/lib/components/session-nav/transcriptModel.ts +0 -352
- package/src/lib/data/emojis.ts +0 -1208
- package/src/lib/index.ts +0 -405
- package/src/lib/stores/confirmDialog.svelte.ts +0 -48
- package/src/lib/stores/inputDialog.svelte.ts +0 -51
- package/src/lib/styles/rail.css +0 -63
- package/src/lib/types/annotation.ts +0 -38
- package/src/lib/types/booking.ts +0 -143
- package/src/lib/types/comments.ts +0 -97
- package/src/lib/types/entityPreview.ts +0 -45
- package/src/lib/types/filePicker.ts +0 -16
- package/src/lib/types/googleMaps.d.ts +0 -51
- package/src/lib/types/maps.ts +0 -43
- package/src/lib/types/messaging.ts +0 -459
- package/src/lib/types/milestone.ts +0 -22
- package/src/lib/types/pipeline.ts +0 -150
- package/src/lib/types/smartImageEditor.ts +0 -39
- package/src/lib/types/templateVars.ts +0 -36
- package/src/lib/utils/currency.ts +0 -20
- package/src/lib/utils/daisyuiColors.ts +0 -243
- package/src/lib/utils/dateFormatters.ts +0 -196
- package/src/lib/utils/googleMapsLoader.ts +0 -84
- package/src/lib/utils/mentionParser.ts +0 -188
- package/src/lib/utils/phone.ts +0 -80
- package/src/lib/utils/phoneFormat.ts +0 -74
- 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>
|