@joewinke/jatui 0.1.11 → 0.1.19

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 (90) hide show
  1. package/README.md +123 -0
  2. package/package.json +2 -1
  3. package/src/lib/actions/railNav.ts +473 -0
  4. package/src/lib/components/AnnotationLayer.svelte +108 -0
  5. package/src/lib/components/AnnotationPanel.svelte +319 -0
  6. package/src/lib/components/AudioWaveform.svelte +9 -5
  7. package/src/lib/components/AvailabilityModal.svelte +7 -3
  8. package/src/lib/components/AvatarUpload.svelte +27 -4
  9. package/src/lib/components/BookingForm.svelte +11 -9
  10. package/src/lib/components/BurndownChart.svelte +778 -0
  11. package/src/lib/components/Button.svelte +10 -1
  12. package/src/lib/components/CalendarPicker.svelte +3 -3
  13. package/src/lib/components/Card.svelte +2 -2
  14. package/src/lib/components/ChipInput.svelte +21 -15
  15. package/src/lib/components/ColorSelector.svelte +17 -13
  16. package/src/lib/components/CommentThread.svelte +773 -0
  17. package/src/lib/components/ConfirmDialog.svelte +348 -0
  18. package/src/lib/components/ConfirmModal.svelte +78 -11
  19. package/src/lib/components/ContextMenu.svelte +59 -19
  20. package/src/lib/components/CountdownTimer.svelte +1 -1
  21. package/src/lib/components/DateRangePicker.svelte +6 -4
  22. package/src/lib/components/Drawer.svelte +36 -3
  23. package/src/lib/components/EntityPreviewCard.svelte +104 -0
  24. package/src/lib/components/FileDropzone.svelte +493 -0
  25. package/src/lib/components/FilePicker.svelte +83 -14
  26. package/src/lib/components/FileThumbnail.svelte +80 -0
  27. package/src/lib/components/FilterDropdown.svelte +11 -11
  28. package/src/lib/components/HunkDiffView.svelte +348 -0
  29. package/src/lib/components/ImageLightbox.svelte +274 -0
  30. package/src/lib/components/ImageUpload.svelte +58 -9
  31. package/src/lib/components/InlineEdit.svelte +15 -9
  32. package/src/lib/components/InputDialog.svelte +327 -0
  33. package/src/lib/components/LazyImage.svelte +1 -0
  34. package/src/lib/components/LinkShortener.svelte +1 -1
  35. package/src/lib/components/LoadingSpinner.svelte +6 -2
  36. package/src/lib/components/MarkupEditor.svelte +485 -0
  37. package/src/lib/components/MarkupOverlay.svelte +55 -0
  38. package/src/lib/components/MediaWorkbench.svelte +871 -0
  39. package/src/lib/components/MilestoneCard.svelte +1 -1
  40. package/src/lib/components/MilestoneTimeline.svelte +1 -1
  41. package/src/lib/components/Modal.svelte +39 -4
  42. package/src/lib/components/PDFViewer.svelte +105 -0
  43. package/src/lib/components/PdfThumbnail.svelte +3 -1
  44. package/src/lib/components/PhoneInput.svelte +1 -1
  45. package/src/lib/components/ResizablePanel.svelte +4 -4
  46. package/src/lib/components/SearchDropdown.svelte +26 -13
  47. package/src/lib/components/SelectInput.svelte +26 -4
  48. package/src/lib/components/SidebarUserFooter.svelte +1 -1
  49. package/src/lib/components/SignaturePad.svelte +8 -4
  50. package/src/lib/components/SmartImageEditor.svelte +720 -0
  51. package/src/lib/components/SortDropdown.svelte +9 -3
  52. package/src/lib/components/Sparkline.svelte +9 -0
  53. package/src/lib/components/StatusBadge.svelte +20 -18
  54. package/src/lib/components/TextArea.svelte +24 -5
  55. package/src/lib/components/TextInput.svelte +29 -6
  56. package/src/lib/components/ThemeSelector.svelte +15 -4
  57. package/src/lib/components/TimeSlotPicker.svelte +7 -7
  58. package/src/lib/components/UserAvatar.svelte +14 -1
  59. package/src/lib/components/VariablePicker.svelte +170 -0
  60. package/src/lib/components/VoicePlayer.svelte +4 -3
  61. package/src/lib/components/markup.ts +287 -0
  62. package/src/lib/components/messaging/ChannelInfoModal.svelte +9 -9
  63. package/src/lib/components/messaging/ChannelList.svelte +1 -1
  64. package/src/lib/components/messaging/ChannelMembersModal.svelte +1 -1
  65. package/src/lib/components/messaging/CreateChannelModal.svelte +1 -1
  66. package/src/lib/components/messaging/DirectMessageList.svelte +1 -1
  67. package/src/lib/components/messaging/EmojiSelector.svelte +2 -1
  68. package/src/lib/components/messaging/MentionAutocomplete.svelte +1 -1
  69. package/src/lib/components/messaging/MessageAttachment.svelte +3 -3
  70. package/src/lib/components/messaging/MessageAttachmentUpload.svelte +3 -3
  71. package/src/lib/components/messaging/MessageInput.svelte +1 -1
  72. package/src/lib/components/messaging/MessageItem.svelte +6 -3
  73. package/src/lib/components/messaging/NotificationSettingsModal.svelte +1 -1
  74. package/src/lib/components/messaging/QuotedMessageDisplay.svelte +6 -1
  75. package/src/lib/components/messaging/StartDMModal.svelte +1 -1
  76. package/src/lib/components/pipeline/Pipeline.svelte +4 -4
  77. package/src/lib/components/pipeline/PipelineCard.svelte +1 -1
  78. package/src/lib/components/pipeline/PipelineColumn.svelte +8 -3
  79. package/src/lib/index.ts +91 -0
  80. package/src/lib/stores/confirmDialog.svelte.ts +48 -0
  81. package/src/lib/stores/inputDialog.svelte.ts +51 -0
  82. package/src/lib/styles/rail.css +63 -0
  83. package/src/lib/types/annotation.ts +38 -0
  84. package/src/lib/types/comments.ts +97 -0
  85. package/src/lib/types/entityPreview.ts +45 -0
  86. package/src/lib/types/filePicker.ts +2 -0
  87. package/src/lib/types/smartImageEditor.ts +39 -0
  88. package/src/lib/types/templateVars.ts +36 -0
  89. package/src/lib/utils/dateFormatters.ts +12 -10
  90. package/src/lib/utils/taskUtils.ts +21 -7
@@ -0,0 +1,287 @@
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
+ }
@@ -83,7 +83,7 @@
83
83
  <dialog class="modal modal-open">
84
84
  <div class="modal-box max-w-md">
85
85
  <div class="flex items-center justify-between mb-4">
86
- <h3 class="font-bold text-lg">
86
+ <h3 class="font-semibold">
87
87
  {channel.emoji || '#'} {channel.name}
88
88
  </h3>
89
89
  <button class="btn btn-ghost btn-sm btn-circle" onclick={onclose}>✕</button>
@@ -96,11 +96,11 @@
96
96
  {#if isEditing}
97
97
  <div class="space-y-4">
98
98
  <div>
99
- <label for="channel-info-name" class="label"><span class="label-text font-medium">Name</span></label>
99
+ <label for="channel-info-name" class="label"><span class="text-[0.8125rem] font-medium text-base-content/85">Name</span></label>
100
100
  <input id="channel-info-name" type="text" bind:value={editName} class="input input-bordered w-full" maxlength="80" />
101
101
  </div>
102
102
  <div>
103
- <label for="channel-info-description" class="label"><span class="label-text font-medium">Description</span></label>
103
+ <label for="channel-info-description" class="label"><span class="text-[0.8125rem] font-medium text-base-content/85">Description</span></label>
104
104
  <textarea id="channel-info-description" bind:value={editDescription} class="textarea textarea-bordered w-full" rows="3" maxlength="250"></textarea>
105
105
  </div>
106
106
  <div class="flex gap-2">
@@ -114,20 +114,20 @@
114
114
  {:else}
115
115
  <div class="space-y-4">
116
116
  <div>
117
- <span class="text-xs text-base-content/50 uppercase tracking-wider">Type</span>
118
- <p class="text-sm">{channel.type === 'private' ? '🔒 Private' : '# Public'}</p>
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
119
  </div>
120
120
 
121
121
  {#if channel.description}
122
122
  <div>
123
- <span class="text-xs text-base-content/50 uppercase tracking-wider">Description</span>
124
- <p class="text-sm">{channel.description}</p>
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
125
  </div>
126
126
  {/if}
127
127
 
128
128
  <div>
129
- <span class="text-xs text-base-content/50 uppercase tracking-wider">Created</span>
130
- <p class="text-sm">{formatDate(channel.createdAt)}{channel.creatorName ? ` by ${channel.creatorName}` : ''}</p>
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
131
  </div>
132
132
 
133
133
  <div class="flex flex-wrap gap-2">
@@ -74,7 +74,7 @@
74
74
  </span>
75
75
 
76
76
  {#if channel.unreadCount && channel.unreadCount > 0}
77
- <span class="badge badge-error badge-xs text-white">
77
+ <span class="badge badge-error badge-xs text-error-content">
78
78
  {channel.unreadCount > 99 ? '99+' : channel.unreadCount}
79
79
  </span>
80
80
  {/if}
@@ -100,7 +100,7 @@
100
100
  <dialog class="modal modal-open">
101
101
  <div class="modal-box max-w-md">
102
102
  <div class="flex items-center justify-between mb-4">
103
- <h3 class="font-bold text-lg">Members of #{channelName}</h3>
103
+ <h3 class="font-semibold">Members of #{channelName}</h3>
104
104
  <button class="btn btn-ghost btn-sm btn-circle" onclick={onclose}>✕</button>
105
105
  </div>
106
106
 
@@ -109,7 +109,7 @@
109
109
  <dialog class="modal modal-open">
110
110
  <div class="modal-box max-w-lg">
111
111
  <div class="flex items-center justify-between mb-4">
112
- <h3 class="font-bold text-lg">Create Channel</h3>
112
+ <h3 class="font-semibold">Create Channel</h3>
113
113
  <button class="btn btn-ghost btn-sm btn-circle" onclick={handleClose}>✕</button>
114
114
  </div>
115
115
 
@@ -115,7 +115,7 @@
115
115
 
116
116
  <div class="flex items-center gap-1">
117
117
  {#if user.unreadCount > 0}
118
- <span class="badge badge-error badge-xs text-white">
118
+ <span class="badge badge-error badge-xs text-error-content">
119
119
  {user.unreadCount > 99 ? '99+' : user.unreadCount}
120
120
  </span>
121
121
  {/if}
@@ -46,6 +46,7 @@
46
46
 
47
47
  let searchQuery = $state('')
48
48
  let activeCategory: EmojiCategory = $state('smileys_people')
49
+ // svelte-ignore state_referenced_locally
49
50
  let selectedSkinTone: SkinTone = $state(preferences?.skinTone || 'default')
50
51
  let hoveredEmoji = $state<{ emoji: string; name: string; shortcode: string } | null>(null)
51
52
  let loading = $state(false)
@@ -164,7 +165,7 @@
164
165
  {#if visible}
165
166
  <div
166
167
  bind:this={selectorElement}
167
- class="fixed z-50 bg-base-100 border border-base-300 rounded-box shadow-xl w-88 flex flex-col"
168
+ class="fixed z-50 bg-base-200 border border-base-300 rounded-box w-88 flex flex-col"
168
169
  style="left: {position.x}px; top: {position.y}px; max-height: 420px;"
169
170
  >
170
171
  <!-- Search -->
@@ -144,7 +144,7 @@
144
144
  {#if visible}
145
145
  <div
146
146
  bind:this={dropdownElement}
147
- class="bg-base-100 border border-base-300 rounded-box shadow-lg overflow-y-auto overflow-x-hidden flex flex-col"
147
+ class="bg-base-200 border border-base-300 rounded-box overflow-y-auto overflow-x-hidden flex flex-col"
148
148
  style={dropdownStyle}
149
149
  >
150
150
  {#if loading}
@@ -196,7 +196,7 @@
196
196
  {/if}
197
197
 
198
198
  {#if !compact}
199
- <button type="button" class="absolute top-1 right-1 btn btn-circle btn-xs bg-base-100/80 backdrop-blur-sm opacity-0 group-hover:opacity-100 transition-opacity" onclick={(e) => { e.stopPropagation(); handleContextMenu(e) }} title="More options">
199
+ <button type="button" class="absolute top-1 right-1 btn btn-circle btn-xs bg-base-100/80 opacity-0 group-hover:opacity-100 transition-opacity" onclick={(e) => { e.stopPropagation(); handleContextMenu(e) }} title="More options">
200
200
 
201
201
  </button>
202
202
  {/if}
@@ -206,7 +206,7 @@
206
206
  {#if showContextMenu}
207
207
  <!-- svelte-ignore a11y_click_events_have_key_events -->
208
208
  <!-- svelte-ignore a11y_no_static_element_interactions -->
209
- <div class="fixed z-50 bg-base-100 rounded-lg shadow-xl border border-base-300 py-1 min-w-[160px]" style="left: {contextMenuPosition.x}px; top: {contextMenuPosition.y}px;" onclick={(e) => e.stopPropagation()} role="menu" tabindex="-1">
209
+ <div class="fixed z-50 bg-base-200 rounded-lg border border-base-300 py-1 min-w-[160px]" style="left: {contextMenuPosition.x}px; top: {contextMenuPosition.y}px;" onclick={(e) => e.stopPropagation()} role="menu" tabindex="-1">
210
210
  <button class="w-full flex items-center gap-2 px-3 py-2 text-sm hover:bg-base-200 transition-colors" onclick={handleOpenInNewTab} role="menuitem">↗ Open in new tab</button>
211
211
  <button class="w-full flex items-center gap-2 px-3 py-2 text-sm hover:bg-base-200 transition-colors" onclick={handleDownload} role="menuitem">⬇ Download</button>
212
212
  <button class="w-full flex items-center gap-2 px-3 py-2 text-sm hover:bg-base-200 transition-colors" onclick={handleCopyLink} role="menuitem">📋 Copy link</button>
@@ -248,7 +248,7 @@
248
248
  {#if showDetailsModal}
249
249
  <dialog class="modal modal-open">
250
250
  <div class="modal-box max-w-md">
251
- <h3 class="font-bold text-lg mb-4">File Details</h3>
251
+ <h3 class="font-semibold mb-4">File Details</h3>
252
252
  <div class="space-y-3 text-sm">
253
253
  <div class="flex justify-between"><span class="text-base-content/60">Name</span><span class="font-medium truncate max-w-[200px]" title={attachment.file_name}>{attachment.file_name}</span></div>
254
254
  <div class="flex justify-between"><span class="text-base-content/60">Type</span><span class="font-medium">{attachment.file_type}</span></div>
@@ -188,11 +188,11 @@
188
188
  {#if isDragging}
189
189
  <div
190
190
  bind:this={dropZoneRef}
191
- class="fixed inset-0 z-50 bg-primary/10 backdrop-blur-sm flex items-center justify-center"
191
+ class="fixed inset-0 z-50 bg-primary/10 flex items-center justify-center"
192
192
  ondragenter={handleDragEnter} ondragleave={handleDragLeave} ondragover={handleDragOver} ondrop={handleDrop}
193
193
  role="region" aria-label="Drop zone for file uploads"
194
194
  >
195
- <div class="bg-base-100 rounded-2xl border-2 border-dashed border-primary p-8 text-center shadow-xl">
195
+ <div class="bg-base-100 rounded-md border-2 border-dashed border-primary p-8 text-center">
196
196
  <div class="text-4xl mb-4">📎</div>
197
197
  <p class="text-lg font-medium text-primary">Drop files to upload</p>
198
198
  <p class="text-sm text-base-content/60 mt-2">Images, videos, and documents up to {maxSize}MB</p>
@@ -213,7 +213,7 @@
213
213
  </div>
214
214
 
215
215
  {#if att.uploading}
216
- <div class="absolute inset-0 bg-black/50 flex items-center justify-center">
216
+ <div class="absolute inset-0 bg-[color-mix(in_oklch,#180042_55%,transparent)] flex items-center justify-center">
217
217
  <span class="loading loading-spinner loading-sm text-white"></span>
218
218
  </div>
219
219
  {/if}
@@ -362,7 +362,7 @@
362
362
  {/if}
363
363
  <button type="button" class="absolute top-0.5 right-0.5 btn btn-circle btn-xs bg-base-100/80 hover:bg-error hover:text-error-content opacity-0 group-hover:opacity-100 transition-opacity" onclick={() => removeAttachment(att.id)} title="Remove">✕</button>
364
364
  {#if att.uploading}
365
- <div class="absolute inset-0 bg-black/50 flex items-center justify-center"><span class="loading loading-spinner loading-xs text-white"></span></div>
365
+ <div class="absolute inset-0 bg-[color-mix(in_oklch,#180042_55%,transparent)] flex items-center justify-center"><span class="loading loading-spinner loading-xs text-white"></span></div>
366
366
  {/if}
367
367
  </div>
368
368
  {/each}
@@ -183,7 +183,7 @@
183
183
  </script>
184
184
 
185
185
  <div
186
- class="group flex gap-3 px-2 py-1 rounded relative transition-colors w-full text-left cursor-pointer {message.isPinned ? 'bg-warning/5 hover:bg-warning/10 border-l-4 border-warning pl-1' : 'hover:bg-base-200'}"
186
+ class="group flex gap-3 px-2 py-1 rounded relative transition-colors w-full text-left cursor-pointer {message.isPinned ? 'bg-warning/5 hover:bg-warning/10' : 'hover:bg-base-200'}"
187
187
  class:pt-2={!isConsecutive}
188
188
  data-message-id={message.id}
189
189
  onclick={handleMessageClick}
@@ -205,7 +205,7 @@
205
205
  <!-- Content -->
206
206
  <div class="flex-1 min-w-0 relative">
207
207
  <!-- Floating action buttons -->
208
- <div class="absolute right-0 {!isConsecutive ? 'top-0' : 'top-1'} opacity-0 group-hover:opacity-100 transition-opacity bg-base-100/90 backdrop-blur-sm rounded-lg shadow-lg border border-base-300 p-1 z-10">
208
+ <div class="absolute right-0 {!isConsecutive ? 'top-0' : 'top-1'} opacity-0 group-hover:opacity-100 transition-opacity bg-base-200 rounded-lg border border-base-300 p-1 z-10">
209
209
  <div class="flex items-center">
210
210
  {#if reactionCallbacks && currentUserId}
211
211
  <div class="flex items-center gap-1 mr-1">
@@ -289,8 +289,11 @@
289
289
 
290
290
  <!-- Thread indicator -->
291
291
  {#if message.thread_reply_count && message.thread_reply_count > 0}
292
- <button type="button" class="mt-2 p-2 bg-base-200/50 rounded-lg border-l-2 border-primary/30 hover:bg-base-300/50 transition-colors w-full text-left" onclick={(e) => { e.stopPropagation(); handleReply() }}>
292
+ <button type="button" class="mt-2 p-2 bg-base-200/50 rounded-lg hover:bg-base-300/50 transition-colors w-full text-left" onclick={(e) => { e.stopPropagation(); handleReply() }}>
293
293
  <div class="flex items-center gap-2 text-xs">
294
+ <svg class="w-3.5 h-3.5 text-primary/50 flex-shrink-0" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
295
+ <path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"></path>
296
+ </svg>
294
297
  {#if message.thread_last_sender}
295
298
  <Avatar name={message.thread_last_sender.full_name} avatarUrl={message.thread_last_sender.avatar_url} size="xs" />
296
299
  {/if}
@@ -121,7 +121,7 @@
121
121
  <dialog class="modal modal-open">
122
122
  <div class="modal-box max-w-md">
123
123
  <div class="flex items-center justify-between mb-4">
124
- <h3 class="font-bold text-lg">
124
+ <h3 class="font-semibold">
125
125
  {type === 'global' ? 'Notification Settings' : `Notifications: #${channelName}`}
126
126
  </h3>
127
127
  <button class="btn btn-ghost btn-sm btn-circle" onclick={onclose}>✕</button>
@@ -54,7 +54,7 @@
54
54
 
55
55
  <div class="quoted-message-display {mode} {className}">
56
56
  <div
57
- class="flex items-start gap-2 p-3 bg-base-200/50 rounded-lg border-l-4 border-primary/40 relative"
57
+ class="flex items-start gap-2 p-3 bg-base-200/50 rounded-lg relative"
58
58
  >
59
59
  {#if mode === 'preview' && onremove}
60
60
  <button
@@ -66,6 +66,10 @@
66
66
  </button>
67
67
  {/if}
68
68
 
69
+ <svg class="w-3.5 h-3.5 text-primary/50 flex-shrink-0 mt-0.5" viewBox="0 0 24 24" fill="currentColor" aria-hidden="true">
70
+ <path d="M14.017 21v-7.391c0-5.704 3.731-9.57 8.983-10.609l.995 2.151c-2.432.917-3.995 3.293-3.995 5.849h4v10h-9.983zm-14.017 0v-7.391c0-5.704 3.748-9.57 9-10.609l.996 2.151c-2.433.917-3.996 3.293-3.996 5.849h3.983v10h-9.983z"/>
71
+ </svg>
72
+
69
73
  <div class="flex-shrink-0">
70
74
  <Avatar
71
75
  name={quotedMessage.senderName}
@@ -95,6 +99,7 @@
95
99
  .line-clamp-3 {
96
100
  display: -webkit-box;
97
101
  -webkit-line-clamp: 3;
102
+ line-clamp: 3;
98
103
  -webkit-box-orient: vertical;
99
104
  overflow: hidden;
100
105
  }
@@ -50,7 +50,7 @@
50
50
  <dialog class="modal modal-open">
51
51
  <div class="modal-box max-w-md">
52
52
  <div class="flex items-center justify-between mb-4">
53
- <h3 class="font-bold text-lg">New Message</h3>
53
+ <h3 class="font-semibold">New Message</h3>
54
54
  <button class="btn btn-ghost btn-sm btn-circle" onclick={handleClose}>✕</button>
55
55
  </div>
56
56
 
@@ -196,8 +196,8 @@
196
196
  const sortedTableItems = $derived.by(() => {
197
197
  if (!sortKey) return allItems;
198
198
  return [...allItems].sort((a, b) => {
199
- const av = (a as Record<string, unknown>)[sortKey] ?? '';
200
- const bv = (b as Record<string, unknown>)[sortKey] ?? '';
199
+ const av = (a as unknown as Record<string, unknown>)[sortKey] ?? '';
200
+ const bv = (b as unknown as Record<string, unknown>)[sortKey] ?? '';
201
201
  const cmp =
202
202
  typeof av === 'number' && typeof bv === 'number'
203
203
  ? av - bv
@@ -236,7 +236,7 @@
236
236
  day: 'numeric'
237
237
  });
238
238
  }
239
- return String((item as Record<string, unknown>)[key] ?? '');
239
+ return String((item as unknown as Record<string, unknown>)[key] ?? '');
240
240
  }
241
241
  </script>
242
242
 
@@ -245,7 +245,7 @@
245
245
  {#if metrics && metrics.length > 0}
246
246
  <div class="grid grid-cols-2 md:grid-cols-4 gap-3">
247
247
  {#each metrics as m}
248
- <div class="stat bg-base-100 rounded-xl border border-base-300 p-3">
248
+ <div class="stat bg-base-100 rounded-md border border-base-300 p-3">
249
249
  <div class="stat-title text-xs">{m.label}</div>
250
250
  <div class="stat-value text-lg" style:color={m.color}>
251
251
  {formatMetricValue(m)}
@@ -37,7 +37,7 @@
37
37
 
38
38
  <button
39
39
  type="button"
40
- class="card card-compact bg-base-100 shadow-sm border cursor-pointer hover:shadow-md transition-shadow w-full text-left {isDragging ? 'opacity-50 rotate-2' : ''} {className}"
40
+ class="card card-compact bg-base-200 border cursor-pointer hover:bg-base-300/80 transition-colors w-full text-left {isDragging ? 'opacity-50 rotate-2' : ''} {className}"
41
41
  style:border-color={colors.border}
42
42
  onclick={() => onclick?.(item)}
43
43
  >
@@ -63,7 +63,7 @@
63
63
  </script>
64
64
 
65
65
  <div
66
- class="flex flex-col rounded-xl border {className}"
66
+ class="flex flex-col rounded-md border {className}"
67
67
  style:border-color={colors.border}
68
68
  style:background-color={colors.bg}
69
69
  >
@@ -148,8 +148,13 @@
148
148
  {#if emptyState}
149
149
  {@render emptyState({ stage })}
150
150
  {:else}
151
- <div class="flex-1 flex items-center justify-center text-xs opacity-30 p-4">
152
- No items
151
+ <div class="flex-1 flex items-center justify-center p-4 my-1">
152
+ <div class="w-full rounded-lg border border-dashed border-base-content/15 py-6 flex flex-col items-center gap-1.5">
153
+ <svg class="w-5 h-5 text-base-content/20" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
154
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
155
+ </svg>
156
+ <span class="text-xs text-base-content/25">Empty</span>
157
+ </div>
153
158
  </div>
154
159
  {/if}
155
160
  {/if}