@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.
- package/dist/actions/collapse.d.ts +80 -0
- package/dist/actions/collapse.js +134 -0
- package/dist/actions/railNav.d.ts +167 -0
- package/dist/actions/railNav.js +351 -0
- package/dist/components/AnnotationLayer.svelte +108 -0
- package/dist/components/AnnotationLayer.svelte.d.ts +12 -0
- package/dist/components/AnnotationPanel.svelte +319 -0
- package/dist/components/AnnotationPanel.svelte.d.ts +13 -0
- package/dist/components/AudioWaveform.svelte +701 -0
- package/dist/components/AudioWaveform.svelte.d.ts +19 -0
- package/dist/components/AvailabilityModal.svelte +177 -0
- package/dist/components/AvailabilityModal.svelte.d.ts +14 -0
- package/dist/components/AvatarUpload.svelte +234 -0
- package/dist/components/AvatarUpload.svelte.d.ts +23 -0
- package/dist/components/Badge.svelte +38 -0
- package/dist/components/Badge.svelte.d.ts +14 -0
- package/dist/components/BookingForm.svelte +278 -0
- package/dist/components/BookingForm.svelte.d.ts +37 -0
- package/dist/components/BurndownChart.svelte +778 -0
- package/dist/components/BurndownChart.svelte.d.ts +17 -0
- package/dist/components/Button.svelte +81 -0
- package/dist/components/Button.svelte.d.ts +21 -0
- package/dist/components/CalendarPicker.svelte +284 -0
- package/dist/components/CalendarPicker.svelte.d.ts +22 -0
- package/dist/components/Card.svelte +67 -0
- package/dist/components/Card.svelte.d.ts +17 -0
- package/dist/components/CharacterCounter.svelte +82 -0
- package/dist/components/CharacterCounter.svelte.d.ts +10 -0
- package/dist/components/ChipInput.svelte +609 -0
- package/dist/components/ChipInput.svelte.d.ts +55 -0
- package/dist/components/Collapse.svelte +91 -0
- package/dist/components/Collapse.svelte.d.ts +18 -0
- package/dist/components/ColorSelector.svelte +167 -0
- package/dist/components/ColorSelector.svelte.d.ts +9 -0
- package/dist/components/CommentThread.svelte +773 -0
- package/dist/components/CommentThread.svelte.d.ts +32 -0
- package/dist/components/ConfirmDialog.svelte +348 -0
- package/dist/components/ConfirmDialog.svelte.d.ts +3 -0
- package/dist/components/ConfirmModal.svelte +142 -0
- package/dist/components/ConfirmModal.svelte.d.ts +15 -0
- package/dist/components/ContextMenu.svelte +188 -0
- package/dist/components/ContextMenu.svelte.d.ts +31 -0
- package/dist/components/CountdownTimer.svelte +94 -0
- package/dist/components/CountdownTimer.svelte.d.ts +10 -0
- package/dist/components/DateRangePicker.svelte +194 -0
- package/dist/components/DateRangePicker.svelte.d.ts +10 -0
- package/dist/components/Drawer.svelte +143 -0
- package/dist/components/Drawer.svelte.d.ts +33 -0
- package/dist/components/EmojiPicker.svelte +159 -0
- package/dist/components/EmojiPicker.svelte.d.ts +8 -0
- package/dist/components/EntityPreviewCard.svelte +104 -0
- package/dist/components/EntityPreviewCard.svelte.d.ts +9 -0
- package/dist/components/FileDropzone.svelte +493 -0
- package/dist/components/FileDropzone.svelte.d.ts +40 -0
- package/dist/components/FilePicker.svelte +488 -0
- package/dist/components/FilePicker.svelte.d.ts +20 -0
- package/dist/components/FileThumbnail.svelte +80 -0
- package/dist/components/FileThumbnail.svelte.d.ts +12 -0
- package/dist/components/FilterDropdown.svelte +203 -0
- package/dist/components/FilterDropdown.svelte.d.ts +48 -0
- package/dist/components/FloatingActionBar.svelte +354 -0
- package/dist/components/FloatingActionBar.svelte.d.ts +51 -0
- package/dist/components/GPSTracker.svelte +202 -0
- package/dist/components/GPSTracker.svelte.d.ts +34 -0
- package/dist/components/HunkDiffView.svelte +348 -0
- package/dist/components/HunkDiffView.svelte.d.ts +9 -0
- package/dist/components/ImageLightbox.svelte +274 -0
- package/dist/components/ImageLightbox.svelte.d.ts +25 -0
- package/dist/components/ImageUpload.svelte +147 -0
- package/dist/components/ImageUpload.svelte.d.ts +10 -0
- package/dist/components/InlineEdit.svelte +291 -0
- package/dist/components/InlineEdit.svelte.d.ts +45 -0
- package/dist/components/InputDialog.svelte +327 -0
- package/dist/components/InputDialog.svelte.d.ts +3 -0
- package/dist/components/KeyboardShortcutsOverlay.svelte +296 -0
- package/dist/components/KeyboardShortcutsOverlay.svelte.d.ts +42 -0
- package/dist/components/LazyImage.svelte +123 -0
- package/dist/components/LazyImage.svelte.d.ts +14 -0
- package/dist/components/LinkShortener.svelte +274 -0
- package/dist/components/LinkShortener.svelte.d.ts +27 -0
- package/dist/components/LoadingSpinner.svelte +106 -0
- package/dist/components/LoadingSpinner.svelte.d.ts +21 -0
- package/dist/components/LocationMap.svelte +186 -0
- package/dist/components/LocationMap.svelte.d.ts +11 -0
- package/dist/components/MapView.svelte +341 -0
- package/dist/components/MapView.svelte.d.ts +20 -0
- package/dist/components/MarkupEditor.svelte +485 -0
- package/dist/components/MarkupEditor.svelte.d.ts +10 -0
- package/dist/components/MarkupOverlay.svelte +55 -0
- package/dist/components/MarkupOverlay.svelte.d.ts +8 -0
- package/dist/components/MediaWorkbench.svelte +871 -0
- package/dist/components/MediaWorkbench.svelte.d.ts +9 -0
- package/dist/components/MilestoneCard.svelte +98 -0
- package/dist/components/MilestoneCard.svelte.d.ts +16 -0
- package/dist/components/MilestoneTimeline.svelte +91 -0
- package/dist/components/MilestoneTimeline.svelte.d.ts +15 -0
- package/dist/components/Modal.svelte +244 -0
- package/dist/components/Modal.svelte.d.ts +35 -0
- package/dist/components/PDFViewer.svelte +105 -0
- package/dist/components/PDFViewer.svelte.d.ts +9 -0
- package/dist/components/PdfThumbnail.svelte +63 -0
- package/dist/components/PdfThumbnail.svelte.d.ts +9 -0
- package/dist/components/PhoneInput.svelte +212 -0
- package/dist/components/PhoneInput.svelte.d.ts +25 -0
- package/dist/components/ResizableDivider.svelte +307 -0
- package/dist/components/ResizableDivider.svelte.d.ts +26 -0
- package/dist/components/ResizablePanel.svelte +305 -0
- package/dist/components/ResizablePanel.svelte.d.ts +18 -0
- package/dist/components/SearchDropdown.svelte +421 -0
- package/dist/components/SearchDropdown.svelte.d.ts +24 -0
- package/dist/components/SelectInput.svelte +237 -0
- package/dist/components/SelectInput.svelte.d.ts +26 -0
- package/dist/components/SidebarUserFooter.svelte +34 -0
- package/dist/components/SidebarUserFooter.svelte.d.ts +16 -0
- package/dist/components/SignaturePad.svelte +175 -0
- package/dist/components/SignaturePad.svelte.d.ts +17 -0
- package/dist/components/SmartImageEditor.svelte +720 -0
- package/dist/components/SmartImageEditor.svelte.d.ts +24 -0
- package/dist/components/SortDropdown.svelte +156 -0
- package/dist/components/SortDropdown.svelte.d.ts +40 -0
- package/dist/components/Sparkline.svelte +116 -0
- package/dist/components/Sparkline.svelte.d.ts +12 -0
- package/dist/components/SpeechForm.svelte +114 -0
- package/dist/components/SpeechForm.svelte.d.ts +17 -0
- package/dist/components/StatusBadge.svelte +157 -0
- package/dist/components/StatusBadge.svelte.d.ts +24 -0
- package/dist/components/TaskTypeIcon.svelte +54 -0
- package/dist/components/TaskTypeIcon.svelte.d.ts +17 -0
- package/dist/components/TextArea.svelte +162 -0
- package/dist/components/TextArea.svelte.d.ts +30 -0
- package/dist/components/TextInput.svelte +131 -0
- package/dist/components/TextInput.svelte.d.ts +29 -0
- package/dist/components/ThemeSelector.svelte +206 -0
- package/dist/components/ThemeSelector.svelte.d.ts +14 -0
- package/dist/components/TimeSlotPicker.svelte +162 -0
- package/dist/components/TimeSlotPicker.svelte.d.ts +18 -0
- package/dist/components/UserAvatar.svelte +72 -0
- package/dist/components/UserAvatar.svelte.d.ts +11 -0
- package/dist/components/VariablePicker.svelte +170 -0
- package/dist/components/VariablePicker.svelte.d.ts +40 -0
- package/dist/components/VoicePlayer.svelte +422 -0
- package/dist/components/VoicePlayer.svelte.d.ts +18 -0
- package/dist/components/linked-columns/LinkedColumns.svelte +526 -0
- package/dist/components/linked-columns/LinkedColumns.svelte.d.ts +130 -0
- package/dist/components/markup.d.ts +47 -0
- package/dist/components/markup.js +201 -0
- package/dist/components/messaging/Avatar.svelte +81 -0
- package/dist/components/messaging/Avatar.svelte.d.ts +11 -0
- package/dist/components/messaging/ChannelInfoModal.svelte +163 -0
- package/dist/components/messaging/ChannelInfoModal.svelte.d.ts +18 -0
- package/dist/components/messaging/ChannelList.svelte +107 -0
- package/dist/components/messaging/ChannelList.svelte.d.ts +11 -0
- package/dist/components/messaging/ChannelMemberAvatarStack.svelte +69 -0
- package/dist/components/messaging/ChannelMemberAvatarStack.svelte.d.ts +11 -0
- package/dist/components/messaging/ChannelMembersModal.svelte +182 -0
- package/dist/components/messaging/ChannelMembersModal.svelte.d.ts +16 -0
- package/dist/components/messaging/CreateChannelModal.svelte +190 -0
- package/dist/components/messaging/CreateChannelModal.svelte.d.ts +14 -0
- package/dist/components/messaging/DirectMessageList.svelte +145 -0
- package/dist/components/messaging/DirectMessageList.svelte.d.ts +12 -0
- package/dist/components/messaging/EmojiSelector.svelte +261 -0
- package/dist/components/messaging/EmojiSelector.svelte.d.ts +19 -0
- package/dist/components/messaging/MentionAutocomplete.svelte +193 -0
- package/dist/components/messaging/MentionAutocomplete.svelte.d.ts +17 -0
- package/dist/components/messaging/MessageAttachment.svelte +273 -0
- package/dist/components/messaging/MessageAttachment.svelte.d.ts +13 -0
- package/dist/components/messaging/MessageAttachmentUpload.svelte +243 -0
- package/dist/components/messaging/MessageAttachmentUpload.svelte.d.ts +19 -0
- package/dist/components/messaging/MessageInput.svelte +452 -0
- package/dist/components/messaging/MessageInput.svelte.d.ts +28 -0
- package/dist/components/messaging/MessageItem.svelte +341 -0
- package/dist/components/messaging/MessageItem.svelte.d.ts +41 -0
- package/dist/components/messaging/MessageThread.svelte +306 -0
- package/dist/components/messaging/MessageThread.svelte.d.ts +30 -0
- package/dist/components/messaging/NotificationSettingsModal.svelte +234 -0
- package/dist/components/messaging/NotificationSettingsModal.svelte.d.ts +13 -0
- package/dist/components/messaging/QuotedMessageDisplay.svelte +123 -0
- package/dist/components/messaging/QuotedMessageDisplay.svelte.d.ts +10 -0
- package/dist/components/messaging/StartDMModal.svelte +100 -0
- package/dist/components/messaging/StartDMModal.svelte.d.ts +14 -0
- package/dist/components/messaging/ThreadPanel.svelte +153 -0
- package/dist/components/messaging/ThreadPanel.svelte.d.ts +16 -0
- package/dist/components/pipeline/DESIGN.md +361 -0
- package/dist/components/pipeline/Pipeline.svelte +391 -0
- package/dist/components/pipeline/Pipeline.svelte.d.ts +28 -0
- package/dist/components/pipeline/PipelineCard.svelte +94 -0
- package/dist/components/pipeline/PipelineCard.svelte.d.ts +13 -0
- package/dist/components/pipeline/PipelineColumn.svelte +163 -0
- package/dist/components/pipeline/PipelineColumn.svelte.d.ts +30 -0
- package/dist/components/replay/ChapterTimeline.svelte +326 -0
- package/dist/components/replay/ChapterTimeline.svelte.d.ts +39 -0
- package/dist/components/session-nav/transcriptModel.d.ts +132 -0
- package/dist/components/session-nav/transcriptModel.js +285 -0
- package/dist/data/emojis.d.ts +18 -0
- package/dist/data/emojis.js +1196 -0
- package/dist/index.d.ts +142 -0
- package/dist/index.js +142 -0
- package/dist/stores/confirmDialog.svelte.d.ts +21 -0
- package/dist/stores/confirmDialog.svelte.js +31 -0
- package/dist/stores/inputDialog.svelte.d.ts +24 -0
- package/dist/stores/inputDialog.svelte.js +32 -0
- package/dist/styles/rail.css +63 -0
- package/dist/types/annotation.d.ts +32 -0
- package/dist/types/annotation.js +3 -0
- package/dist/types/booking.d.ts +115 -0
- package/dist/types/booking.js +24 -0
- package/dist/types/comments.d.ts +65 -0
- package/dist/types/comments.js +22 -0
- package/dist/types/entityPreview.d.ts +42 -0
- package/dist/types/entityPreview.js +10 -0
- package/dist/types/filePicker.d.ts +15 -0
- package/dist/types/filePicker.js +1 -0
- package/dist/types/googleMaps.d.ts +51 -0
- package/dist/types/maps.d.ts +43 -0
- package/dist/types/maps.js +4 -0
- package/dist/types/messaging.d.ts +282 -0
- package/dist/types/messaging.js +100 -0
- package/dist/types/milestone.d.ts +20 -0
- package/dist/types/milestone.js +7 -0
- package/dist/types/pipeline.d.ts +115 -0
- package/dist/types/pipeline.js +15 -0
- package/dist/types/smartImageEditor.d.ts +42 -0
- package/dist/types/smartImageEditor.js +1 -0
- package/dist/types/templateVars.d.ts +34 -0
- package/dist/types/templateVars.js +12 -0
- package/dist/utils/currency.d.ts +8 -0
- package/dist/utils/currency.js +17 -0
- package/dist/utils/daisyuiColors.d.ts +37 -0
- package/dist/utils/daisyuiColors.js +152 -0
- package/dist/utils/dateFormatters.d.ts +69 -0
- package/dist/utils/dateFormatters.js +199 -0
- package/dist/utils/googleMapsLoader.d.ts +19 -0
- package/dist/utils/googleMapsLoader.js +121 -0
- package/dist/utils/mentionParser.d.ts +67 -0
- package/dist/utils/mentionParser.js +132 -0
- package/dist/utils/phone.d.ts +13 -0
- package/dist/utils/phone.js +67 -0
- package/dist/utils/phoneFormat.d.ts +26 -0
- package/dist/utils/phoneFormat.js +69 -0
- package/dist/utils/taskUtils.d.ts +37 -0
- package/dist/utils/taskUtils.js +71 -0
- package/package.json +3 -2
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
// Markup drawing primitives for construction drawing redlining
|
|
2
|
+
// Adapted from JAT feedback annotation engine with cloud bubble tool added
|
|
3
|
+
export var MARKUP_COLORS = [
|
|
4
|
+
"#ef4444",
|
|
5
|
+
"#eab308",
|
|
6
|
+
"#22c55e",
|
|
7
|
+
"#3b82f6",
|
|
8
|
+
"#ffffff",
|
|
9
|
+
"#111827",
|
|
10
|
+
];
|
|
11
|
+
export var DEFAULT_STROKE_WIDTH = 3;
|
|
12
|
+
var _nextId = 1;
|
|
13
|
+
export function nextShapeId() {
|
|
14
|
+
return _nextId++;
|
|
15
|
+
}
|
|
16
|
+
function drawArrow(ctx, shape) {
|
|
17
|
+
var start = shape.start, end = shape.end, color = shape.color, strokeWidth = shape.strokeWidth;
|
|
18
|
+
ctx.strokeStyle = color;
|
|
19
|
+
ctx.lineWidth = strokeWidth;
|
|
20
|
+
ctx.lineCap = "round";
|
|
21
|
+
ctx.lineJoin = "round";
|
|
22
|
+
ctx.beginPath();
|
|
23
|
+
ctx.moveTo(start.x, start.y);
|
|
24
|
+
ctx.lineTo(end.x, end.y);
|
|
25
|
+
ctx.stroke();
|
|
26
|
+
// Arrowhead
|
|
27
|
+
var angle = Math.atan2(end.y - start.y, end.x - start.x);
|
|
28
|
+
var headLen = 14;
|
|
29
|
+
var headAngle = Math.PI / 7;
|
|
30
|
+
ctx.beginPath();
|
|
31
|
+
ctx.moveTo(end.x, end.y);
|
|
32
|
+
ctx.lineTo(end.x - headLen * Math.cos(angle - headAngle), end.y - headLen * Math.sin(angle - headAngle));
|
|
33
|
+
ctx.moveTo(end.x, end.y);
|
|
34
|
+
ctx.lineTo(end.x - headLen * Math.cos(angle + headAngle), end.y - headLen * Math.sin(angle + headAngle));
|
|
35
|
+
ctx.stroke();
|
|
36
|
+
}
|
|
37
|
+
function drawRectangle(ctx, shape) {
|
|
38
|
+
var start = shape.start, end = shape.end, color = shape.color, strokeWidth = shape.strokeWidth;
|
|
39
|
+
ctx.strokeStyle = color;
|
|
40
|
+
ctx.lineWidth = strokeWidth;
|
|
41
|
+
ctx.lineJoin = "round";
|
|
42
|
+
var x = Math.min(start.x, end.x);
|
|
43
|
+
var y = Math.min(start.y, end.y);
|
|
44
|
+
var w = Math.abs(end.x - start.x);
|
|
45
|
+
var h = Math.abs(end.y - start.y);
|
|
46
|
+
ctx.strokeRect(x, y, w, h);
|
|
47
|
+
}
|
|
48
|
+
function drawEllipse(ctx, shape) {
|
|
49
|
+
var start = shape.start, end = shape.end, color = shape.color, strokeWidth = shape.strokeWidth;
|
|
50
|
+
ctx.strokeStyle = color;
|
|
51
|
+
ctx.lineWidth = strokeWidth;
|
|
52
|
+
var cx = (start.x + end.x) / 2;
|
|
53
|
+
var cy = (start.y + end.y) / 2;
|
|
54
|
+
var rx = Math.abs(end.x - start.x) / 2;
|
|
55
|
+
var ry = Math.abs(end.y - start.y) / 2;
|
|
56
|
+
if (rx < 1 || ry < 1)
|
|
57
|
+
return;
|
|
58
|
+
ctx.beginPath();
|
|
59
|
+
ctx.ellipse(cx, cy, rx, ry, 0, 0, Math.PI * 2);
|
|
60
|
+
ctx.stroke();
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Revision cloud — a series of small arcs along the perimeter of a rectangle.
|
|
64
|
+
* Standard construction markup tool for highlighting areas of change.
|
|
65
|
+
*/
|
|
66
|
+
function drawCloud(ctx, shape) {
|
|
67
|
+
var start = shape.start, end = shape.end, color = shape.color, strokeWidth = shape.strokeWidth;
|
|
68
|
+
ctx.strokeStyle = color;
|
|
69
|
+
ctx.lineWidth = strokeWidth;
|
|
70
|
+
ctx.lineCap = "round";
|
|
71
|
+
ctx.lineJoin = "round";
|
|
72
|
+
var x = Math.min(start.x, end.x);
|
|
73
|
+
var y = Math.min(start.y, end.y);
|
|
74
|
+
var w = Math.abs(end.x - start.x);
|
|
75
|
+
var h = Math.abs(end.y - start.y);
|
|
76
|
+
if (w < 4 || h < 4)
|
|
77
|
+
return;
|
|
78
|
+
// Generate perimeter points, then draw arcs between them
|
|
79
|
+
var arcSize = Math.min(w, h, 30); // arc diameter adapts to shape size
|
|
80
|
+
var perimeter = 2 * (w + h);
|
|
81
|
+
var numArcs = Math.max(8, Math.round(perimeter / arcSize));
|
|
82
|
+
// Collect points along the rectangle perimeter
|
|
83
|
+
var points = [];
|
|
84
|
+
var segLengths = [w, h, w, h];
|
|
85
|
+
var segDirs = [
|
|
86
|
+
{ dx: 1, dy: 0 },
|
|
87
|
+
{ dx: 0, dy: 1 },
|
|
88
|
+
{ dx: -1, dy: 0 },
|
|
89
|
+
{ dx: 0, dy: -1 },
|
|
90
|
+
];
|
|
91
|
+
var segStarts = [
|
|
92
|
+
{ x: x, y: y },
|
|
93
|
+
{ x: x + w, y: y },
|
|
94
|
+
{ x: x + w, y: y + h },
|
|
95
|
+
{ x: x, y: y + h },
|
|
96
|
+
];
|
|
97
|
+
var totalLen = 0;
|
|
98
|
+
for (var _i = 0, segLengths_1 = segLengths; _i < segLengths_1.length; _i++) {
|
|
99
|
+
var l = segLengths_1[_i];
|
|
100
|
+
totalLen += l;
|
|
101
|
+
}
|
|
102
|
+
var step = totalLen / numArcs;
|
|
103
|
+
var dist = 0;
|
|
104
|
+
var segIdx = 0;
|
|
105
|
+
var segDist = 0;
|
|
106
|
+
for (var i = 0; i < numArcs; i++) {
|
|
107
|
+
var target = i * step;
|
|
108
|
+
while (dist + segLengths[segIdx] - segDist < target && segIdx < 3) {
|
|
109
|
+
dist += segLengths[segIdx] - segDist;
|
|
110
|
+
segIdx++;
|
|
111
|
+
segDist = 0;
|
|
112
|
+
}
|
|
113
|
+
var remaining = target - dist;
|
|
114
|
+
points.push({
|
|
115
|
+
x: segStarts[segIdx].x + segDirs[segIdx].dx * (segDist + remaining),
|
|
116
|
+
y: segStarts[segIdx].y + segDirs[segIdx].dy * (segDist + remaining),
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
// Draw arcs between consecutive points
|
|
120
|
+
ctx.beginPath();
|
|
121
|
+
for (var i = 0; i < points.length; i++) {
|
|
122
|
+
var p1 = points[i];
|
|
123
|
+
var p2 = points[(i + 1) % points.length];
|
|
124
|
+
var mx = (p1.x + p2.x) / 2;
|
|
125
|
+
var my = (p1.y + p2.y) / 2;
|
|
126
|
+
// Bulge outward from center of rectangle
|
|
127
|
+
var cx = x + w / 2;
|
|
128
|
+
var cy = y + h / 2;
|
|
129
|
+
var dx = mx - cx;
|
|
130
|
+
var dy = my - cy;
|
|
131
|
+
var len = Math.sqrt(dx * dx + dy * dy) || 1;
|
|
132
|
+
var bulge = arcSize * 0.35;
|
|
133
|
+
var ctrlX = mx + (dx / len) * bulge;
|
|
134
|
+
var ctrlY = my + (dy / len) * bulge;
|
|
135
|
+
if (i === 0) {
|
|
136
|
+
ctx.moveTo(p1.x, p1.y);
|
|
137
|
+
}
|
|
138
|
+
ctx.quadraticCurveTo(ctrlX, ctrlY, p2.x, p2.y);
|
|
139
|
+
}
|
|
140
|
+
ctx.closePath();
|
|
141
|
+
ctx.stroke();
|
|
142
|
+
}
|
|
143
|
+
function drawFreehand(ctx, shape) {
|
|
144
|
+
var points = shape.points, color = shape.color, strokeWidth = shape.strokeWidth;
|
|
145
|
+
if (points.length < 2)
|
|
146
|
+
return;
|
|
147
|
+
ctx.strokeStyle = color;
|
|
148
|
+
ctx.lineWidth = strokeWidth;
|
|
149
|
+
ctx.lineCap = "round";
|
|
150
|
+
ctx.lineJoin = "round";
|
|
151
|
+
ctx.beginPath();
|
|
152
|
+
ctx.moveTo(points[0].x, points[0].y);
|
|
153
|
+
for (var i = 1; i < points.length; i++) {
|
|
154
|
+
ctx.lineTo(points[i].x, points[i].y);
|
|
155
|
+
}
|
|
156
|
+
ctx.stroke();
|
|
157
|
+
}
|
|
158
|
+
function drawText(ctx, shape) {
|
|
159
|
+
var position = shape.position, content = shape.content, color = shape.color, fontSize = shape.fontSize;
|
|
160
|
+
if (!content)
|
|
161
|
+
return;
|
|
162
|
+
ctx.font = "bold ".concat(fontSize, "px sans-serif");
|
|
163
|
+
ctx.textBaseline = "top";
|
|
164
|
+
// Black outline for contrast on construction drawings
|
|
165
|
+
ctx.strokeStyle = "#000000";
|
|
166
|
+
ctx.lineWidth = 2;
|
|
167
|
+
ctx.lineJoin = "round";
|
|
168
|
+
ctx.strokeText(content, position.x, position.y);
|
|
169
|
+
ctx.fillStyle = color;
|
|
170
|
+
ctx.fillText(content, position.x, position.y);
|
|
171
|
+
}
|
|
172
|
+
export function renderShape(ctx, shape) {
|
|
173
|
+
ctx.save();
|
|
174
|
+
switch (shape.type) {
|
|
175
|
+
case "arrow":
|
|
176
|
+
drawArrow(ctx, shape);
|
|
177
|
+
break;
|
|
178
|
+
case "rectangle":
|
|
179
|
+
drawRectangle(ctx, shape);
|
|
180
|
+
break;
|
|
181
|
+
case "ellipse":
|
|
182
|
+
drawEllipse(ctx, shape);
|
|
183
|
+
break;
|
|
184
|
+
case "cloud":
|
|
185
|
+
drawCloud(ctx, shape);
|
|
186
|
+
break;
|
|
187
|
+
case "freehand":
|
|
188
|
+
drawFreehand(ctx, shape);
|
|
189
|
+
break;
|
|
190
|
+
case "text":
|
|
191
|
+
drawText(ctx, shape);
|
|
192
|
+
break;
|
|
193
|
+
}
|
|
194
|
+
ctx.restore();
|
|
195
|
+
}
|
|
196
|
+
export function renderAllShapes(ctx, shapes) {
|
|
197
|
+
for (var _i = 0, shapes_1 = shapes; _i < shapes_1.length; _i++) {
|
|
198
|
+
var shape = shapes_1[_i];
|
|
199
|
+
renderShape(ctx, shape);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
@@ -0,0 +1,81 @@
|
|
|
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>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
interface Props {
|
|
2
|
+
name: string;
|
|
3
|
+
avatarUrl?: string | null;
|
|
4
|
+
size?: 'xs' | 'small' | 'medium' | 'large';
|
|
5
|
+
showStatus?: boolean;
|
|
6
|
+
status?: 'online' | 'offline' | 'away' | 'busy';
|
|
7
|
+
class?: string;
|
|
8
|
+
}
|
|
9
|
+
declare const Avatar: import("svelte").Component<Props, {}, "">;
|
|
10
|
+
type Avatar = ReturnType<typeof Avatar>;
|
|
11
|
+
export default Avatar;
|
|
@@ -0,0 +1,163 @@
|
|
|
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}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Channel, ChannelCallbacks } from '../../types/messaging';
|
|
2
|
+
interface Props {
|
|
3
|
+
channel: Channel & {
|
|
4
|
+
memberCount?: number;
|
|
5
|
+
createdBy?: string;
|
|
6
|
+
creatorName?: string;
|
|
7
|
+
};
|
|
8
|
+
currentUserRole: 'admin' | 'moderator' | 'member';
|
|
9
|
+
open: boolean;
|
|
10
|
+
channelCallbacks?: ChannelCallbacks;
|
|
11
|
+
onclose?: () => void;
|
|
12
|
+
onchannelupdate?: (updatedChannel: Channel) => void;
|
|
13
|
+
onchanneldelete?: (channelId: string) => void;
|
|
14
|
+
onmanagemembers?: () => void;
|
|
15
|
+
}
|
|
16
|
+
declare const ChannelInfoModal: import("svelte").Component<Props, {}, "">;
|
|
17
|
+
type ChannelInfoModal = ReturnType<typeof ChannelInfoModal>;
|
|
18
|
+
export default ChannelInfoModal;
|
|
@@ -0,0 +1,107 @@
|
|
|
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>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { Channel } from '../../types/messaging';
|
|
2
|
+
interface Props {
|
|
3
|
+
channels: Channel[];
|
|
4
|
+
activeChannelId?: string | null;
|
|
5
|
+
onchannelselect?: (event: {
|
|
6
|
+
channelId: string;
|
|
7
|
+
}) => void;
|
|
8
|
+
}
|
|
9
|
+
declare const ChannelList: import("svelte").Component<Props, {}, "">;
|
|
10
|
+
type ChannelList = ReturnType<typeof ChannelList>;
|
|
11
|
+
export default ChannelList;
|
|
@@ -0,0 +1,69 @@
|
|
|
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>
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { ChannelMember, PresenceState } from '../../types/messaging';
|
|
2
|
+
interface Props {
|
|
3
|
+
members: ChannelMember[];
|
|
4
|
+
presenceState?: PresenceState;
|
|
5
|
+
maxVisible?: number;
|
|
6
|
+
size?: 'xs' | 'small' | 'medium' | 'large';
|
|
7
|
+
class?: string;
|
|
8
|
+
}
|
|
9
|
+
declare const ChannelMemberAvatarStack: import("svelte").Component<Props, {}, "">;
|
|
10
|
+
type ChannelMemberAvatarStack = ReturnType<typeof ChannelMemberAvatarStack>;
|
|
11
|
+
export default ChannelMemberAvatarStack;
|