@joewinke/jatui 0.1.11 → 0.1.20
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/README.md +123 -0
- package/package.json +2 -1
- package/src/lib/actions/railNav.ts +473 -0
- package/src/lib/components/AnnotationLayer.svelte +108 -0
- package/src/lib/components/AnnotationPanel.svelte +319 -0
- package/src/lib/components/AudioWaveform.svelte +9 -5
- package/src/lib/components/AvailabilityModal.svelte +7 -3
- package/src/lib/components/AvatarUpload.svelte +27 -4
- package/src/lib/components/BookingForm.svelte +11 -9
- package/src/lib/components/BurndownChart.svelte +778 -0
- package/src/lib/components/Button.svelte +10 -1
- package/src/lib/components/CalendarPicker.svelte +3 -3
- package/src/lib/components/Card.svelte +2 -2
- package/src/lib/components/ChipInput.svelte +8 -3
- package/src/lib/components/ColorSelector.svelte +17 -13
- package/src/lib/components/CommentThread.svelte +773 -0
- package/src/lib/components/ConfirmDialog.svelte +348 -0
- package/src/lib/components/ConfirmModal.svelte +78 -11
- package/src/lib/components/ContextMenu.svelte +59 -19
- package/src/lib/components/CountdownTimer.svelte +1 -1
- package/src/lib/components/DateRangePicker.svelte +6 -4
- package/src/lib/components/Drawer.svelte +36 -3
- package/src/lib/components/EntityPreviewCard.svelte +104 -0
- package/src/lib/components/FileDropzone.svelte +493 -0
- package/src/lib/components/FilePicker.svelte +83 -14
- package/src/lib/components/FileThumbnail.svelte +80 -0
- package/src/lib/components/FilterDropdown.svelte +11 -11
- package/src/lib/components/GPSTracker.svelte +202 -0
- package/src/lib/components/HunkDiffView.svelte +348 -0
- package/src/lib/components/ImageLightbox.svelte +274 -0
- package/src/lib/components/ImageUpload.svelte +58 -9
- package/src/lib/components/InlineEdit.svelte +6 -2
- package/src/lib/components/InputDialog.svelte +327 -0
- package/src/lib/components/KeyboardShortcutsOverlay.svelte +296 -0
- package/src/lib/components/LazyImage.svelte +1 -0
- package/src/lib/components/LinkShortener.svelte +1 -1
- package/src/lib/components/LoadingSpinner.svelte +6 -2
- package/src/lib/components/LocationMap.svelte +186 -0
- package/src/lib/components/MapView.svelte +341 -0
- package/src/lib/components/MarkupEditor.svelte +485 -0
- package/src/lib/components/MarkupOverlay.svelte +55 -0
- package/src/lib/components/MediaWorkbench.svelte +871 -0
- package/src/lib/components/MilestoneCard.svelte +1 -1
- package/src/lib/components/MilestoneTimeline.svelte +1 -1
- package/src/lib/components/Modal.svelte +39 -4
- package/src/lib/components/PDFViewer.svelte +105 -0
- package/src/lib/components/PdfThumbnail.svelte +3 -1
- package/src/lib/components/PhoneInput.svelte +1 -1
- package/src/lib/components/ResizablePanel.svelte +4 -4
- package/src/lib/components/SearchDropdown.svelte +26 -13
- package/src/lib/components/SelectInput.svelte +26 -4
- package/src/lib/components/SidebarUserFooter.svelte +1 -1
- package/src/lib/components/SignaturePad.svelte +8 -4
- package/src/lib/components/SmartImageEditor.svelte +720 -0
- package/src/lib/components/SortDropdown.svelte +9 -3
- package/src/lib/components/Sparkline.svelte +9 -0
- package/src/lib/components/StatusBadge.svelte +20 -18
- package/src/lib/components/TextArea.svelte +24 -5
- package/src/lib/components/TextInput.svelte +29 -6
- package/src/lib/components/ThemeSelector.svelte +15 -4
- package/src/lib/components/TimeSlotPicker.svelte +7 -7
- package/src/lib/components/UserAvatar.svelte +14 -1
- package/src/lib/components/VariablePicker.svelte +170 -0
- package/src/lib/components/VoicePlayer.svelte +4 -3
- package/src/lib/components/linked-columns/LinkedColumns.svelte +520 -0
- package/src/lib/components/markup.ts +287 -0
- package/src/lib/components/messaging/ChannelInfoModal.svelte +9 -9
- package/src/lib/components/messaging/ChannelList.svelte +1 -1
- package/src/lib/components/messaging/ChannelMembersModal.svelte +1 -1
- package/src/lib/components/messaging/CreateChannelModal.svelte +1 -1
- package/src/lib/components/messaging/DirectMessageList.svelte +1 -1
- package/src/lib/components/messaging/EmojiSelector.svelte +2 -1
- package/src/lib/components/messaging/MentionAutocomplete.svelte +1 -1
- package/src/lib/components/messaging/MessageAttachment.svelte +3 -3
- package/src/lib/components/messaging/MessageAttachmentUpload.svelte +3 -3
- package/src/lib/components/messaging/MessageInput.svelte +1 -1
- package/src/lib/components/messaging/MessageItem.svelte +6 -3
- package/src/lib/components/messaging/NotificationSettingsModal.svelte +1 -1
- package/src/lib/components/messaging/QuotedMessageDisplay.svelte +6 -1
- package/src/lib/components/messaging/StartDMModal.svelte +1 -1
- package/src/lib/components/pipeline/Pipeline.svelte +4 -4
- package/src/lib/components/pipeline/PipelineCard.svelte +1 -1
- package/src/lib/components/pipeline/PipelineColumn.svelte +8 -3
- package/src/lib/components/replay/ChapterTimeline.svelte +326 -0
- package/src/lib/components/session-nav/transcriptModel.ts +352 -0
- package/src/lib/index.ts +138 -0
- package/src/lib/stores/confirmDialog.svelte.ts +48 -0
- package/src/lib/stores/inputDialog.svelte.ts +51 -0
- package/src/lib/styles/rail.css +63 -0
- package/src/lib/types/annotation.ts +38 -0
- package/src/lib/types/comments.ts +97 -0
- package/src/lib/types/entityPreview.ts +45 -0
- package/src/lib/types/filePicker.ts +2 -0
- package/src/lib/types/googleMaps.d.ts +51 -0
- package/src/lib/types/maps.ts +43 -0
- package/src/lib/types/smartImageEditor.ts +39 -0
- package/src/lib/types/templateVars.ts +36 -0
- package/src/lib/utils/dateFormatters.ts +12 -10
- package/src/lib/utils/googleMapsLoader.ts +84 -0
- package/src/lib/utils/taskUtils.ts +21 -7
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Annotation, AnnotationThread } from '../types/annotation'
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
annotations: Annotation[]
|
|
6
|
+
activeAnnotationId?: string | null
|
|
7
|
+
annotationMode?: boolean
|
|
8
|
+
showResolved?: boolean
|
|
9
|
+
onAnnotationClick?: (annotation: Annotation) => void
|
|
10
|
+
onImageClick?: (x: number, y: number) => void
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
let {
|
|
14
|
+
annotations = [],
|
|
15
|
+
activeAnnotationId = null,
|
|
16
|
+
annotationMode = false,
|
|
17
|
+
showResolved = false,
|
|
18
|
+
onAnnotationClick,
|
|
19
|
+
onImageClick,
|
|
20
|
+
}: Props = $props()
|
|
21
|
+
|
|
22
|
+
// Group into threads (only show root pins)
|
|
23
|
+
const threads = $derived.by(() => {
|
|
24
|
+
const roots = annotations.filter((a) => !a.parent_annotation_id)
|
|
25
|
+
return roots
|
|
26
|
+
.filter((r) => showResolved || !r.resolved)
|
|
27
|
+
.map((root) => ({
|
|
28
|
+
root,
|
|
29
|
+
replies: annotations.filter((a) => a.parent_annotation_id === root.id),
|
|
30
|
+
}))
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
function handleOverlayClick(e: MouseEvent) {
|
|
34
|
+
if (!annotationMode || !onImageClick) return
|
|
35
|
+
const target = e.currentTarget as HTMLElement
|
|
36
|
+
const rect = target.getBoundingClientRect()
|
|
37
|
+
const x = ((e.clientX - rect.left) / rect.width) * 100
|
|
38
|
+
const y = ((e.clientY - rect.top) / rect.height) * 100
|
|
39
|
+
onImageClick(Math.round(x * 1000) / 1000, Math.round(y * 1000) / 1000)
|
|
40
|
+
}
|
|
41
|
+
</script>
|
|
42
|
+
|
|
43
|
+
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
44
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
45
|
+
<div
|
|
46
|
+
class="absolute inset-0 z-10"
|
|
47
|
+
class:cursor-crosshair={annotationMode}
|
|
48
|
+
class:pointer-events-none={!annotationMode && threads.length === 0}
|
|
49
|
+
onclick={handleOverlayClick}
|
|
50
|
+
>
|
|
51
|
+
{#each threads as thread (thread.root.id)}
|
|
52
|
+
{@const isActive = activeAnnotationId === thread.root.id}
|
|
53
|
+
{@const replyCount = thread.replies.length}
|
|
54
|
+
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
55
|
+
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
56
|
+
<div
|
|
57
|
+
class="absolute pointer-events-auto"
|
|
58
|
+
style="left: {thread.root.x_percent}%; top: {thread.root.y_percent}%; transform: translate(-50%, -100%);"
|
|
59
|
+
>
|
|
60
|
+
<button
|
|
61
|
+
class="relative group flex flex-col items-center"
|
|
62
|
+
onclick={(e) => { e.stopPropagation(); onAnnotationClick?.(thread.root) }}
|
|
63
|
+
title={thread.root.content.slice(0, 80)}
|
|
64
|
+
>
|
|
65
|
+
<!-- Pin icon -->
|
|
66
|
+
<div
|
|
67
|
+
class="w-7 h-7 rounded-full flex items-center justify-center shadow-lg border-2 transition-all
|
|
68
|
+
{isActive
|
|
69
|
+
? 'bg-primary border-primary text-primary-content scale-125'
|
|
70
|
+
: thread.root.resolved
|
|
71
|
+
? 'bg-success/80 border-success/60 text-success-content'
|
|
72
|
+
: 'bg-warning border-warning/80 text-warning-content hover:scale-110'}"
|
|
73
|
+
>
|
|
74
|
+
{#if thread.root.resolved}
|
|
75
|
+
<svg class="w-3.5 h-3.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
76
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M5 13l4 4L19 7" />
|
|
77
|
+
</svg>
|
|
78
|
+
{:else}
|
|
79
|
+
<svg class="w-3.5 h-3.5" fill="currentColor" viewBox="0 0 24 24">
|
|
80
|
+
<path d="M20 2H4c-1.1 0-2 .9-2 2v18l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2z" />
|
|
81
|
+
</svg>
|
|
82
|
+
{/if}
|
|
83
|
+
</div>
|
|
84
|
+
|
|
85
|
+
<!-- Reply count badge -->
|
|
86
|
+
{#if replyCount > 0}
|
|
87
|
+
<span class="absolute -top-1 -right-1 min-w-[16px] h-4 px-1 rounded-full bg-base-content text-base-100 text-[10px] font-bold flex items-center justify-center">
|
|
88
|
+
{replyCount}
|
|
89
|
+
</span>
|
|
90
|
+
{/if}
|
|
91
|
+
|
|
92
|
+
<!-- Pin tail -->
|
|
93
|
+
<div
|
|
94
|
+
class="w-0.5 h-2 -mt-0.5 {isActive ? 'bg-primary' : thread.root.resolved ? 'bg-success/60' : 'bg-warning/80'}"
|
|
95
|
+
></div>
|
|
96
|
+
</button>
|
|
97
|
+
</div>
|
|
98
|
+
{/each}
|
|
99
|
+
|
|
100
|
+
<!-- Placement indicator when in annotation mode -->
|
|
101
|
+
{#if annotationMode}
|
|
102
|
+
<div class="absolute inset-0 border-2 border-dashed border-primary/30 rounded pointer-events-none">
|
|
103
|
+
<div class="absolute top-2 left-1/2 -translate-x-1/2 bg-primary/90 text-primary-content text-xs px-3 py-1 rounded-full shadow whitespace-nowrap">
|
|
104
|
+
Click to place annotation
|
|
105
|
+
</div>
|
|
106
|
+
</div>
|
|
107
|
+
{/if}
|
|
108
|
+
</div>
|
|
@@ -0,0 +1,319 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Annotation, AnnotationProfile, AnnotationCallbacks } from '../types/annotation'
|
|
3
|
+
|
|
4
|
+
interface Props {
|
|
5
|
+
annotations: Annotation[]
|
|
6
|
+
profiles: Record<string, AnnotationProfile>
|
|
7
|
+
activeAnnotationId?: string | null
|
|
8
|
+
currentUserId: string
|
|
9
|
+
callbacks: AnnotationCallbacks
|
|
10
|
+
onAnnotationSelect?: (annotation: Annotation | null) => void
|
|
11
|
+
onAnnotationsChanged?: () => void
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
let {
|
|
15
|
+
annotations = [],
|
|
16
|
+
profiles = {},
|
|
17
|
+
activeAnnotationId = null,
|
|
18
|
+
currentUserId,
|
|
19
|
+
callbacks,
|
|
20
|
+
onAnnotationSelect,
|
|
21
|
+
onAnnotationsChanged,
|
|
22
|
+
}: Props = $props()
|
|
23
|
+
|
|
24
|
+
let replyContent = $state("")
|
|
25
|
+
let isSubmitting = $state(false)
|
|
26
|
+
let showResolved = $state(false)
|
|
27
|
+
let editingId = $state<string | null>(null)
|
|
28
|
+
let editContent = $state("")
|
|
29
|
+
|
|
30
|
+
// Group annotations into threads
|
|
31
|
+
const threads = $derived.by(() => {
|
|
32
|
+
const roots = annotations.filter((a) => !a.parent_annotation_id)
|
|
33
|
+
return roots
|
|
34
|
+
.filter((r) => showResolved || !r.resolved)
|
|
35
|
+
.map((root) => ({
|
|
36
|
+
root,
|
|
37
|
+
replies: annotations
|
|
38
|
+
.filter((a) => a.parent_annotation_id === root.id)
|
|
39
|
+
.sort((a, b) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime()),
|
|
40
|
+
}))
|
|
41
|
+
.sort((a, b) => new Date(b.root.created_at).getTime() - new Date(a.root.created_at).getTime())
|
|
42
|
+
})
|
|
43
|
+
|
|
44
|
+
const resolvedCount = $derived(
|
|
45
|
+
annotations.filter((a) => !a.parent_annotation_id && a.resolved).length,
|
|
46
|
+
)
|
|
47
|
+
const totalRoots = $derived(annotations.filter((a) => !a.parent_annotation_id).length)
|
|
48
|
+
|
|
49
|
+
function getAuthorName(userId: string): string {
|
|
50
|
+
return profiles[userId]?.full_name || "Unknown"
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function getInitials(userId: string): string {
|
|
54
|
+
const name = getAuthorName(userId)
|
|
55
|
+
return name
|
|
56
|
+
.split(" ")
|
|
57
|
+
.map((n) => n[0])
|
|
58
|
+
.join("")
|
|
59
|
+
.toUpperCase()
|
|
60
|
+
.slice(0, 2)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function formatTime(dateStr: string): string {
|
|
64
|
+
const date = new Date(dateStr)
|
|
65
|
+
const now = new Date()
|
|
66
|
+
const diffMs = now.getTime() - date.getTime()
|
|
67
|
+
const diffMins = Math.floor(diffMs / 60000)
|
|
68
|
+
if (diffMins < 1) return "just now"
|
|
69
|
+
if (diffMins < 60) return `${diffMins}m ago`
|
|
70
|
+
const diffHours = Math.floor(diffMins / 60)
|
|
71
|
+
if (diffHours < 24) return `${diffHours}h ago`
|
|
72
|
+
const diffDays = Math.floor(diffHours / 24)
|
|
73
|
+
if (diffDays < 7) return `${diffDays}d ago`
|
|
74
|
+
return date.toLocaleDateString("en-US", { month: "short", day: "numeric" })
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function submitReply(parentId: string) {
|
|
78
|
+
if (!replyContent.trim() || isSubmitting) return
|
|
79
|
+
isSubmitting = true
|
|
80
|
+
try {
|
|
81
|
+
await callbacks.onAddReply(parentId, replyContent.trim())
|
|
82
|
+
replyContent = ""
|
|
83
|
+
onAnnotationsChanged?.()
|
|
84
|
+
} catch (err) {
|
|
85
|
+
console.error("Error posting reply:", err)
|
|
86
|
+
} finally {
|
|
87
|
+
isSubmitting = false
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async function toggleResolve(annotation: Annotation) {
|
|
92
|
+
try {
|
|
93
|
+
await callbacks.onResolve(annotation.id, !annotation.resolved)
|
|
94
|
+
onAnnotationsChanged?.()
|
|
95
|
+
} catch (err) {
|
|
96
|
+
console.error("Error updating annotation:", err)
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
async function deleteAnnotation(id: string) {
|
|
101
|
+
try {
|
|
102
|
+
await callbacks.onDelete(id)
|
|
103
|
+
if (activeAnnotationId === id) onAnnotationSelect?.(null)
|
|
104
|
+
onAnnotationsChanged?.()
|
|
105
|
+
} catch (err) {
|
|
106
|
+
console.error("Error deleting annotation:", err)
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async function saveEdit(id: string) {
|
|
111
|
+
if (!editContent.trim() || isSubmitting) return
|
|
112
|
+
isSubmitting = true
|
|
113
|
+
try {
|
|
114
|
+
await callbacks.onEdit(id, editContent.trim())
|
|
115
|
+
editingId = null
|
|
116
|
+
editContent = ""
|
|
117
|
+
onAnnotationsChanged?.()
|
|
118
|
+
} catch (err) {
|
|
119
|
+
console.error("Error editing annotation:", err)
|
|
120
|
+
} finally {
|
|
121
|
+
isSubmitting = false
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function startEdit(annotation: Annotation) {
|
|
126
|
+
editingId = annotation.id
|
|
127
|
+
editContent = annotation.content
|
|
128
|
+
}
|
|
129
|
+
</script>
|
|
130
|
+
|
|
131
|
+
<div class="flex flex-col h-full">
|
|
132
|
+
<!-- Header -->
|
|
133
|
+
<div class="flex items-center justify-between px-3 py-2 border-b border-base-300 flex-shrink-0">
|
|
134
|
+
<div class="flex items-center gap-2">
|
|
135
|
+
<h4 class="text-sm font-semibold">Annotations</h4>
|
|
136
|
+
{#if totalRoots > 0}
|
|
137
|
+
<span class="badge badge-xs badge-ghost">{totalRoots - resolvedCount} open</span>
|
|
138
|
+
{/if}
|
|
139
|
+
</div>
|
|
140
|
+
{#if resolvedCount > 0}
|
|
141
|
+
<label class="flex items-center gap-1.5 cursor-pointer">
|
|
142
|
+
<span class="text-xs text-base-content/50">Show resolved</span>
|
|
143
|
+
<input
|
|
144
|
+
type="checkbox"
|
|
145
|
+
class="toggle toggle-xs"
|
|
146
|
+
bind:checked={showResolved}
|
|
147
|
+
/>
|
|
148
|
+
</label>
|
|
149
|
+
{/if}
|
|
150
|
+
</div>
|
|
151
|
+
|
|
152
|
+
<!-- Thread list -->
|
|
153
|
+
<div class="flex-1 overflow-y-auto">
|
|
154
|
+
{#if threads.length === 0}
|
|
155
|
+
<div class="text-center py-8 px-4">
|
|
156
|
+
<svg class="mx-auto h-10 w-10 text-base-content/20" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
157
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M7 8h10M7 12h4m1 8l-4-4H5a2 2 0 01-2-2V6a2 2 0 012-2h14a2 2 0 012 2v8a2 2 0 01-2 2h-3l-4 4z" />
|
|
158
|
+
</svg>
|
|
159
|
+
<p class="text-sm text-base-content/40 mt-2">No annotations yet</p>
|
|
160
|
+
<p class="text-xs text-base-content/30 mt-1">Click the pin icon then click on the image to add one</p>
|
|
161
|
+
</div>
|
|
162
|
+
{:else}
|
|
163
|
+
{#each threads as thread (thread.root.id)}
|
|
164
|
+
{@const isActive = activeAnnotationId === thread.root.id}
|
|
165
|
+
<div
|
|
166
|
+
class="border-b border-base-200 transition-colors {isActive ? 'bg-primary/5' : 'hover:bg-base-200/50'}"
|
|
167
|
+
>
|
|
168
|
+
<!-- Root annotation -->
|
|
169
|
+
<button
|
|
170
|
+
class="w-full text-left px-3 py-2.5"
|
|
171
|
+
onclick={() => onAnnotationSelect?.(isActive ? null : thread.root)}
|
|
172
|
+
>
|
|
173
|
+
<div class="flex items-start gap-2">
|
|
174
|
+
<!-- Avatar -->
|
|
175
|
+
<div class="flex-shrink-0 w-6 h-6 rounded-full bg-primary/10 text-primary flex items-center justify-center text-[10px] font-bold mt-0.5">
|
|
176
|
+
{getInitials(thread.root.user_id)}
|
|
177
|
+
</div>
|
|
178
|
+
<div class="flex-1 min-w-0">
|
|
179
|
+
<div class="flex items-center gap-1.5">
|
|
180
|
+
<span class="text-xs font-medium text-base-content">{getAuthorName(thread.root.user_id)}</span>
|
|
181
|
+
<span class="text-[10px] text-base-content/40">{formatTime(thread.root.created_at)}</span>
|
|
182
|
+
{#if thread.root.resolved}
|
|
183
|
+
<span class="badge badge-xs badge-success gap-0.5">
|
|
184
|
+
<svg class="w-2.5 h-2.5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
185
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M5 13l4 4L19 7" />
|
|
186
|
+
</svg>
|
|
187
|
+
Resolved
|
|
188
|
+
</span>
|
|
189
|
+
{/if}
|
|
190
|
+
</div>
|
|
191
|
+
<p class="text-sm text-base-content/80 mt-0.5 {isActive ? '' : 'line-clamp-2'}">{thread.root.content}</p>
|
|
192
|
+
{#if thread.replies.length > 0 && !isActive}
|
|
193
|
+
<span class="text-xs text-base-content/40 mt-1 inline-block">
|
|
194
|
+
{thread.replies.length} {thread.replies.length === 1 ? "reply" : "replies"}
|
|
195
|
+
</span>
|
|
196
|
+
{/if}
|
|
197
|
+
</div>
|
|
198
|
+
</div>
|
|
199
|
+
</button>
|
|
200
|
+
|
|
201
|
+
<!-- Expanded thread -->
|
|
202
|
+
{#if isActive}
|
|
203
|
+
<div class="px-3 pb-3 space-y-2">
|
|
204
|
+
<!-- Actions for root -->
|
|
205
|
+
<div class="flex items-center gap-1 ml-8">
|
|
206
|
+
<button
|
|
207
|
+
class="btn btn-ghost btn-xs gap-1 {thread.root.resolved ? 'text-warning' : 'text-success'}"
|
|
208
|
+
onclick={() => toggleResolve(thread.root)}
|
|
209
|
+
>
|
|
210
|
+
{#if thread.root.resolved}
|
|
211
|
+
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
212
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 4v5h.582m15.356 2A8.001 8.001 0 004.582 9m0 0H9m11 11v-5h-.581m0 0a8.003 8.003 0 01-15.357-2m15.357 2H15" />
|
|
213
|
+
</svg>
|
|
214
|
+
Reopen
|
|
215
|
+
{:else}
|
|
216
|
+
<svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
217
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7" />
|
|
218
|
+
</svg>
|
|
219
|
+
Resolve
|
|
220
|
+
{/if}
|
|
221
|
+
</button>
|
|
222
|
+
{#if thread.root.user_id === currentUserId}
|
|
223
|
+
<button
|
|
224
|
+
class="btn btn-ghost btn-xs text-base-content/50"
|
|
225
|
+
onclick={() => startEdit(thread.root)}
|
|
226
|
+
>Edit</button>
|
|
227
|
+
<button
|
|
228
|
+
class="btn btn-ghost btn-xs text-error/70"
|
|
229
|
+
onclick={() => deleteAnnotation(thread.root.id)}
|
|
230
|
+
>Delete</button>
|
|
231
|
+
{/if}
|
|
232
|
+
</div>
|
|
233
|
+
|
|
234
|
+
<!-- Edit form for root -->
|
|
235
|
+
{#if editingId === thread.root.id}
|
|
236
|
+
<div class="ml-8 space-y-1.5">
|
|
237
|
+
<textarea
|
|
238
|
+
class="textarea textarea-bordered textarea-sm w-full"
|
|
239
|
+
rows="2"
|
|
240
|
+
bind:value={editContent}
|
|
241
|
+
></textarea>
|
|
242
|
+
<div class="flex gap-1">
|
|
243
|
+
<button class="btn btn-xs btn-primary" onclick={() => saveEdit(thread.root.id)} disabled={isSubmitting}>Save</button>
|
|
244
|
+
<button class="btn btn-xs btn-ghost" onclick={() => { editingId = null; editContent = ""; }}>Cancel</button>
|
|
245
|
+
</div>
|
|
246
|
+
</div>
|
|
247
|
+
{/if}
|
|
248
|
+
|
|
249
|
+
<!-- Replies -->
|
|
250
|
+
{#each thread.replies as reply (reply.id)}
|
|
251
|
+
<div class="ml-8 flex items-start gap-2 bg-base-200/50 rounded-lg p-2">
|
|
252
|
+
<div class="flex-shrink-0 w-5 h-5 rounded-full bg-secondary/10 text-secondary flex items-center justify-center text-[9px] font-bold mt-0.5">
|
|
253
|
+
{getInitials(reply.user_id)}
|
|
254
|
+
</div>
|
|
255
|
+
<div class="flex-1 min-w-0">
|
|
256
|
+
<div class="flex items-center gap-1.5">
|
|
257
|
+
<span class="text-xs font-medium">{getAuthorName(reply.user_id)}</span>
|
|
258
|
+
<span class="text-[10px] text-base-content/40">{formatTime(reply.created_at)}</span>
|
|
259
|
+
</div>
|
|
260
|
+
{#if editingId === reply.id}
|
|
261
|
+
<div class="mt-1 space-y-1.5">
|
|
262
|
+
<textarea
|
|
263
|
+
class="textarea textarea-bordered textarea-xs w-full"
|
|
264
|
+
rows="2"
|
|
265
|
+
bind:value={editContent}
|
|
266
|
+
></textarea>
|
|
267
|
+
<div class="flex gap-1">
|
|
268
|
+
<button class="btn btn-xs btn-primary" onclick={() => saveEdit(reply.id)} disabled={isSubmitting}>Save</button>
|
|
269
|
+
<button class="btn btn-xs btn-ghost" onclick={() => { editingId = null; editContent = ""; }}>Cancel</button>
|
|
270
|
+
</div>
|
|
271
|
+
</div>
|
|
272
|
+
{:else}
|
|
273
|
+
<p class="text-sm text-base-content/80 mt-0.5">{reply.content}</p>
|
|
274
|
+
{#if reply.user_id === currentUserId}
|
|
275
|
+
<div class="flex gap-1 mt-1">
|
|
276
|
+
<button class="btn btn-ghost btn-xs text-base-content/40 h-5 min-h-0" onclick={() => startEdit(reply)}>Edit</button>
|
|
277
|
+
<button class="btn btn-ghost btn-xs text-error/50 h-5 min-h-0" onclick={() => deleteAnnotation(reply.id)}>Delete</button>
|
|
278
|
+
</div>
|
|
279
|
+
{/if}
|
|
280
|
+
{/if}
|
|
281
|
+
</div>
|
|
282
|
+
</div>
|
|
283
|
+
{/each}
|
|
284
|
+
|
|
285
|
+
<!-- Reply input -->
|
|
286
|
+
<div class="ml-8 flex gap-2">
|
|
287
|
+
<textarea
|
|
288
|
+
class="textarea textarea-bordered textarea-sm flex-1 min-h-[36px]"
|
|
289
|
+
rows="1"
|
|
290
|
+
placeholder="Reply..."
|
|
291
|
+
bind:value={replyContent}
|
|
292
|
+
onkeydown={(e) => {
|
|
293
|
+
if (e.key === "Enter" && !e.shiftKey) {
|
|
294
|
+
e.preventDefault()
|
|
295
|
+
submitReply(thread.root.id)
|
|
296
|
+
}
|
|
297
|
+
}}
|
|
298
|
+
></textarea>
|
|
299
|
+
<button
|
|
300
|
+
class="btn btn-sm btn-primary self-end"
|
|
301
|
+
disabled={!replyContent.trim() || isSubmitting}
|
|
302
|
+
onclick={() => submitReply(thread.root.id)}
|
|
303
|
+
>
|
|
304
|
+
{#if isSubmitting}
|
|
305
|
+
<span class="loading loading-spinner loading-xs"></span>
|
|
306
|
+
{:else}
|
|
307
|
+
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
308
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 19l9 2-9-18-9 18 9-2zm0 0v-8" />
|
|
309
|
+
</svg>
|
|
310
|
+
{/if}
|
|
311
|
+
</button>
|
|
312
|
+
</div>
|
|
313
|
+
</div>
|
|
314
|
+
{/if}
|
|
315
|
+
</div>
|
|
316
|
+
{/each}
|
|
317
|
+
{/if}
|
|
318
|
+
</div>
|
|
319
|
+
</div>
|
|
@@ -15,9 +15,9 @@
|
|
|
15
15
|
waveColor = "neutral",
|
|
16
16
|
progressColor = "accent",
|
|
17
17
|
backgroundColor = "base-100",
|
|
18
|
-
waveColorHex = "
|
|
19
|
-
progressColorHex = "
|
|
20
|
-
backgroundColorHex = "
|
|
18
|
+
waveColorHex = "",
|
|
19
|
+
progressColorHex = "",
|
|
20
|
+
backgroundColorHex = "",
|
|
21
21
|
interactive = true,
|
|
22
22
|
showTrimControls = false,
|
|
23
23
|
onTrim,
|
|
@@ -69,8 +69,11 @@
|
|
|
69
69
|
}
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
+
// svelte-ignore state_referenced_locally
|
|
72
73
|
let resolvedWaveColor = $state(resolveColor(waveColor, waveColorHex))
|
|
74
|
+
// svelte-ignore state_referenced_locally
|
|
73
75
|
let resolvedProgressColor = $state(resolveColor(progressColor, progressColorHex))
|
|
76
|
+
// svelte-ignore state_referenced_locally
|
|
74
77
|
let resolvedBackgroundColor = $state(resolveColor(backgroundColor, backgroundColorHex))
|
|
75
78
|
|
|
76
79
|
function updateResolvedColors() {
|
|
@@ -79,7 +82,7 @@
|
|
|
79
82
|
resolvedBackgroundColor = resolveColor(backgroundColor, backgroundColorHex)
|
|
80
83
|
}
|
|
81
84
|
|
|
82
|
-
onMount(
|
|
85
|
+
onMount(() => {
|
|
83
86
|
const cleanupThemeWatcher = watchThemeChanges(() => {
|
|
84
87
|
updateResolvedColors()
|
|
85
88
|
if (canvasContext && canvas && waveformData.length > 0) {
|
|
@@ -488,6 +491,7 @@
|
|
|
488
491
|
audio.play()
|
|
489
492
|
|
|
490
493
|
const checkTime = () => {
|
|
494
|
+
if (!audio) return
|
|
491
495
|
if (audio.currentTime >= trimEnd) {
|
|
492
496
|
audio.pause()
|
|
493
497
|
} else if (isPlaying) {
|
|
@@ -612,7 +616,7 @@
|
|
|
612
616
|
|
|
613
617
|
{#if isHovering && interactive && hoverX > 0}
|
|
614
618
|
<div
|
|
615
|
-
class="absolute pointer-events-none bg-
|
|
619
|
+
class="absolute pointer-events-none bg-neutral text-neutral-content text-xs px-2 py-1 rounded z-10"
|
|
616
620
|
style="left: {hoverX - 20}px; top: -30px;"
|
|
617
621
|
>
|
|
618
622
|
{formatTime(hoverTime)}
|
|
@@ -22,9 +22,13 @@
|
|
|
22
22
|
onClose: () => void;
|
|
23
23
|
} = $props();
|
|
24
24
|
|
|
25
|
+
// svelte-ignore state_referenced_locally
|
|
25
26
|
const initialType = existingOverride?.override_type || 'unavailable';
|
|
27
|
+
// svelte-ignore state_referenced_locally
|
|
26
28
|
const initialStart = existingOverride?.start_time?.slice(0, 5) || '09:00';
|
|
29
|
+
// svelte-ignore state_referenced_locally
|
|
27
30
|
const initialEnd = existingOverride?.end_time?.slice(0, 5) || '17:00';
|
|
31
|
+
// svelte-ignore state_referenced_locally
|
|
28
32
|
const initialReason = existingOverride?.reason || '';
|
|
29
33
|
|
|
30
34
|
let overrideType = $state<OverrideType>(initialType);
|
|
@@ -66,15 +70,15 @@
|
|
|
66
70
|
<!-- svelte-ignore a11y_click_events_have_key_events -->
|
|
67
71
|
<!-- svelte-ignore a11y_no_static_element_interactions -->
|
|
68
72
|
<div
|
|
69
|
-
class="fixed inset-0 z-50 flex items-center justify-center bg-
|
|
73
|
+
class="fixed inset-0 z-50 flex items-center justify-center bg-[color-mix(in_oklch,#180042_55%,transparent)] p-4"
|
|
70
74
|
onclick={handleBackdropClick}
|
|
71
75
|
transition:slide={{ duration: 200 }}
|
|
72
76
|
>
|
|
73
|
-
<div class="bg-base-100 rounded-
|
|
77
|
+
<div class="bg-base-100 rounded-md border border-base-300 w-full max-w-md" transition:slide={{ duration: 300 }}>
|
|
74
78
|
<!-- Header -->
|
|
75
79
|
<div class="p-6 border-b border-base-300">
|
|
76
80
|
<div class="flex items-center justify-between">
|
|
77
|
-
<h2 class="text-
|
|
81
|
+
<h2 class="text-lg font-semibold">Set Date Override</h2>
|
|
78
82
|
<button onclick={onClose} class="btn btn-ghost btn-sm btn-circle" aria-label="Close">
|
|
79
83
|
<!-- X icon -->
|
|
80
84
|
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
@@ -41,9 +41,31 @@
|
|
|
41
41
|
let error = $state("")
|
|
42
42
|
let previewUrl = $state<string | null>(null)
|
|
43
43
|
let fileInput: HTMLInputElement | undefined = $state()
|
|
44
|
+
let justUploaded = $state(false)
|
|
44
45
|
|
|
45
46
|
const displayUrl = $derived(previewUrl ?? avatarUrl)
|
|
46
47
|
|
|
48
|
+
// Flash green border briefly when uploading transitions to done
|
|
49
|
+
$effect(() => {
|
|
50
|
+
if (!uploading && previewUrl) {
|
|
51
|
+
justUploaded = true
|
|
52
|
+
const t = setTimeout(() => { justUploaded = false }, 1000)
|
|
53
|
+
return () => clearTimeout(t)
|
|
54
|
+
}
|
|
55
|
+
})
|
|
56
|
+
|
|
57
|
+
// Colorized initials background — consistent color per user
|
|
58
|
+
const avatarBg = $derived.by(() => {
|
|
59
|
+
const source = name || email || ""
|
|
60
|
+
if (!source) return "oklch(0.62 0.14 240)"
|
|
61
|
+
let hash = 0
|
|
62
|
+
for (let i = 0; i < source.length; i++) {
|
|
63
|
+
hash = source.charCodeAt(i) + ((hash << 5) - hash)
|
|
64
|
+
}
|
|
65
|
+
const hues = [30, 85, 145, 200, 240, 270, 310, 350]
|
|
66
|
+
return `oklch(0.62 0.14 ${hues[Math.abs(hash) % hues.length]})`
|
|
67
|
+
})
|
|
68
|
+
|
|
47
69
|
const initials = $derived.by(() => {
|
|
48
70
|
const source = name || email || ""
|
|
49
71
|
if (!source) return "?"
|
|
@@ -144,7 +166,8 @@
|
|
|
144
166
|
<!-- Avatar circle -->
|
|
145
167
|
<button
|
|
146
168
|
type="button"
|
|
147
|
-
class="relative group rounded-full overflow-hidden w-20 h-20 shrink-0 cursor-pointer border-2
|
|
169
|
+
class="relative group rounded-full overflow-hidden w-20 h-20 shrink-0 cursor-pointer border-2 transition-all duration-300
|
|
170
|
+
{justUploaded ? 'border-success scale-[1.04]' : 'border-base-300 hover:border-primary'}"
|
|
148
171
|
class:opacity-50={disabled || uploading}
|
|
149
172
|
onclick={openPicker}
|
|
150
173
|
{disabled}
|
|
@@ -152,14 +175,14 @@
|
|
|
152
175
|
{#if displayUrl}
|
|
153
176
|
<img src={displayUrl} alt="Avatar" class="w-full h-full object-cover" />
|
|
154
177
|
{:else}
|
|
155
|
-
<div class="w-full h-full
|
|
178
|
+
<div class="w-full h-full flex items-center justify-center text-2xl font-semibold text-white" style="background: {avatarBg}">
|
|
156
179
|
{initials}
|
|
157
180
|
</div>
|
|
158
181
|
{/if}
|
|
159
182
|
|
|
160
183
|
<!-- Hover overlay -->
|
|
161
184
|
{#if !disabled && !uploading}
|
|
162
|
-
<div class="absolute inset-0 bg-
|
|
185
|
+
<div class="absolute inset-0 bg-[color-mix(in_oklch,#180042_45%,transparent)] opacity-0 group-hover:opacity-100 transition-opacity flex items-center justify-center">
|
|
163
186
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
|
164
187
|
<path d="M23 19a2 2 0 0 1-2 2H3a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h4l2-3h6l2 3h4a2 2 0 0 1 2 2z"/>
|
|
165
188
|
<circle cx="12" cy="13" r="4"/>
|
|
@@ -169,7 +192,7 @@
|
|
|
169
192
|
|
|
170
193
|
<!-- Loading spinner -->
|
|
171
194
|
{#if uploading}
|
|
172
|
-
<div class="absolute inset-0 bg-
|
|
195
|
+
<div class="absolute inset-0 bg-[color-mix(in_oklch,#180042_45%,transparent)] flex items-center justify-center">
|
|
173
196
|
<span class="loading loading-spinner loading-sm text-white"></span>
|
|
174
197
|
</div>
|
|
175
198
|
{/if}
|
|
@@ -48,7 +48,9 @@
|
|
|
48
48
|
accentColor?: string;
|
|
49
49
|
} = $props();
|
|
50
50
|
|
|
51
|
+
// svelte-ignore state_referenced_locally
|
|
51
52
|
let guestName = $state(initialName);
|
|
53
|
+
// svelte-ignore state_referenced_locally
|
|
52
54
|
let guestEmail = $state(initialEmail);
|
|
53
55
|
let guestPhone = $state('');
|
|
54
56
|
let guestNotes = $state('');
|
|
@@ -101,7 +103,7 @@
|
|
|
101
103
|
<form onsubmit={handleSubmit} class="space-y-6">
|
|
102
104
|
<!-- Name -->
|
|
103
105
|
<div>
|
|
104
|
-
<label for="booking-name" class="block text-
|
|
106
|
+
<label for="booking-name" class="block text-[0.8125rem] font-medium text-base-content/85 mb-2">
|
|
105
107
|
Your Name <span class="text-error">*</span>
|
|
106
108
|
</label>
|
|
107
109
|
<input
|
|
@@ -117,7 +119,7 @@
|
|
|
117
119
|
|
|
118
120
|
<!-- Email -->
|
|
119
121
|
<div>
|
|
120
|
-
<label for="booking-email" class="block text-
|
|
122
|
+
<label for="booking-email" class="block text-[0.8125rem] font-medium text-base-content/85 mb-2">
|
|
121
123
|
Email Address <span class="text-error">*</span>
|
|
122
124
|
</label>
|
|
123
125
|
<input
|
|
@@ -129,14 +131,14 @@
|
|
|
129
131
|
disabled={submitting}
|
|
130
132
|
class="input input-bordered w-full bg-base-200/50 focus:border-{accentColor}"
|
|
131
133
|
/>
|
|
132
|
-
<p class="text-
|
|
134
|
+
<p class="text-[0.75rem] text-base-content/45 mt-1">
|
|
133
135
|
Confirmation and meeting details will be sent here
|
|
134
136
|
</p>
|
|
135
137
|
</div>
|
|
136
138
|
|
|
137
139
|
<!-- Phone -->
|
|
138
140
|
<div>
|
|
139
|
-
<label for="booking-phone" class="block text-
|
|
141
|
+
<label for="booking-phone" class="block text-[0.8125rem] font-medium text-base-content/85 mb-2">
|
|
140
142
|
Phone Number {#if requirePhone}<span class="text-error">*</span>{:else}<span class="text-base-content/50">(optional)</span>{/if}
|
|
141
143
|
</label>
|
|
142
144
|
<input
|
|
@@ -149,7 +151,7 @@
|
|
|
149
151
|
class="input input-bordered w-full bg-base-200/50 focus:border-{accentColor}"
|
|
150
152
|
/>
|
|
151
153
|
{#if locationHint}
|
|
152
|
-
<p class="text-
|
|
154
|
+
<p class="text-[0.75rem] text-base-content/45 mt-1">{locationHint}</p>
|
|
153
155
|
{/if}
|
|
154
156
|
</div>
|
|
155
157
|
|
|
@@ -158,8 +160,8 @@
|
|
|
158
160
|
<div class="border-t border-base-300 pt-6">
|
|
159
161
|
<div class="flex items-center justify-between mb-4">
|
|
160
162
|
<div>
|
|
161
|
-
<h3 class="text-
|
|
162
|
-
<p class="text-
|
|
163
|
+
<h3 class="text-[0.9375rem] font-medium">Additional Attendees</h3>
|
|
164
|
+
<p class="text-[0.75rem] text-base-content/45">Invite team members to join the meeting</p>
|
|
163
165
|
</div>
|
|
164
166
|
{#if !showAttendees}
|
|
165
167
|
<button
|
|
@@ -231,7 +233,7 @@
|
|
|
231
233
|
<!-- Notes -->
|
|
232
234
|
{#if showNotes}
|
|
233
235
|
<div>
|
|
234
|
-
<label for="booking-notes" class="block text-
|
|
236
|
+
<label for="booking-notes" class="block text-[0.8125rem] font-medium text-base-content/85 mb-2">
|
|
235
237
|
Additional Notes <span class="text-base-content/50">(optional)</span>
|
|
236
238
|
</label>
|
|
237
239
|
<textarea
|
|
@@ -247,7 +249,7 @@
|
|
|
247
249
|
|
|
248
250
|
<!-- Terms Notice -->
|
|
249
251
|
{#if showTerms}
|
|
250
|
-
<div class="bg-base-200/30 rounded-lg p-4 text-
|
|
252
|
+
<div class="bg-base-200/30 rounded-lg p-4 text-[0.8125rem] text-base-content/45">
|
|
251
253
|
<p>
|
|
252
254
|
By scheduling this meeting, you agree to our
|
|
253
255
|
<a href={termsUrl} class="text-{accentColor} hover:underline">Terms of Service</a>
|