@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,348 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
/**
|
|
3
|
+
* HunkDiffView — side-by-side line-level diff with inline word highlights.
|
|
4
|
+
*
|
|
5
|
+
* Renders the before/after of a single DiffHunk with the visual treatment of
|
|
6
|
+
* a real code-review tool:
|
|
7
|
+
* - Line numbers in gutter
|
|
8
|
+
* - "−" / "+" markers per line
|
|
9
|
+
* - Red background + strikethrough for removed lines
|
|
10
|
+
* - Green background for added lines
|
|
11
|
+
* - Inline word-level highlights when a line was modified (Myers diff)
|
|
12
|
+
* - Monospace, syntax-naive (YAML is plain enough)
|
|
13
|
+
*
|
|
14
|
+
* Pure presentation — no state, no side effects. Pairs with DiffReview's
|
|
15
|
+
* per-hunk Accept/Reject buttons.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { diffLines, diffWordsWithSpace, type Change } from 'diff'
|
|
19
|
+
|
|
20
|
+
interface Props {
|
|
21
|
+
oldText: string | null
|
|
22
|
+
newText: string | null
|
|
23
|
+
/** When false (default), shows two columns. When true, unified single column. */
|
|
24
|
+
unified?: boolean
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
let { oldText, newText, unified = false }: Props = $props()
|
|
28
|
+
|
|
29
|
+
type Row = {
|
|
30
|
+
kind: 'context' | 'removed' | 'added' | 'modified'
|
|
31
|
+
oldLineNo: number | null
|
|
32
|
+
newLineNo: number | null
|
|
33
|
+
oldContent: string | null
|
|
34
|
+
newContent: string | null
|
|
35
|
+
/** When kind=='modified', word-level segments per side */
|
|
36
|
+
oldSegments?: Array<{ text: string; changed: boolean }>
|
|
37
|
+
newSegments?: Array<{ text: string; changed: boolean }>
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Build paired rows for side-by-side rendering.
|
|
42
|
+
* Adjacent removed+added blocks of equal-ish line count are zipped into
|
|
43
|
+
* 'modified' rows so the eye can compare line-for-line. Otherwise they
|
|
44
|
+
* render as separate red/green rows (with blank counterpart on the other
|
|
45
|
+
* side).
|
|
46
|
+
*/
|
|
47
|
+
function buildRows(oldT: string, newT: string): Row[] {
|
|
48
|
+
const changes = diffLines(oldT, newT)
|
|
49
|
+
const rows: Row[] = []
|
|
50
|
+
let oldLine = 1
|
|
51
|
+
let newLine = 1
|
|
52
|
+
|
|
53
|
+
for (let i = 0; i < changes.length; i++) {
|
|
54
|
+
const c = changes[i]
|
|
55
|
+
const lines = c.value.replace(/\n$/, '').split('\n')
|
|
56
|
+
|
|
57
|
+
if (!c.added && !c.removed) {
|
|
58
|
+
// context
|
|
59
|
+
for (const line of lines) {
|
|
60
|
+
rows.push({
|
|
61
|
+
kind: 'context',
|
|
62
|
+
oldLineNo: oldLine++,
|
|
63
|
+
newLineNo: newLine++,
|
|
64
|
+
oldContent: line,
|
|
65
|
+
newContent: line,
|
|
66
|
+
})
|
|
67
|
+
}
|
|
68
|
+
} else if (c.removed) {
|
|
69
|
+
// Lookahead: is the very next change an 'added' block? If so, pair them
|
|
70
|
+
const next = changes[i + 1]
|
|
71
|
+
if (next?.added) {
|
|
72
|
+
const remLines = lines
|
|
73
|
+
const addLines = next.value.replace(/\n$/, '').split('\n')
|
|
74
|
+
const pairs = Math.max(remLines.length, addLines.length)
|
|
75
|
+
for (let j = 0; j < pairs; j++) {
|
|
76
|
+
const r = remLines[j]
|
|
77
|
+
const a = addLines[j]
|
|
78
|
+
if (r !== undefined && a !== undefined) {
|
|
79
|
+
// both present — modified row with inline word diff
|
|
80
|
+
const wordChanges = diffWordsWithSpace(r, a)
|
|
81
|
+
const oldSegments: Array<{ text: string; changed: boolean }> = []
|
|
82
|
+
const newSegments: Array<{ text: string; changed: boolean }> = []
|
|
83
|
+
for (const w of wordChanges) {
|
|
84
|
+
if (w.added) newSegments.push({ text: w.value, changed: true })
|
|
85
|
+
else if (w.removed) oldSegments.push({ text: w.value, changed: true })
|
|
86
|
+
else {
|
|
87
|
+
oldSegments.push({ text: w.value, changed: false })
|
|
88
|
+
newSegments.push({ text: w.value, changed: false })
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
rows.push({
|
|
92
|
+
kind: 'modified',
|
|
93
|
+
oldLineNo: oldLine++,
|
|
94
|
+
newLineNo: newLine++,
|
|
95
|
+
oldContent: r,
|
|
96
|
+
newContent: a,
|
|
97
|
+
oldSegments,
|
|
98
|
+
newSegments,
|
|
99
|
+
})
|
|
100
|
+
} else if (r !== undefined) {
|
|
101
|
+
rows.push({
|
|
102
|
+
kind: 'removed',
|
|
103
|
+
oldLineNo: oldLine++,
|
|
104
|
+
newLineNo: null,
|
|
105
|
+
oldContent: r,
|
|
106
|
+
newContent: null,
|
|
107
|
+
})
|
|
108
|
+
} else if (a !== undefined) {
|
|
109
|
+
rows.push({
|
|
110
|
+
kind: 'added',
|
|
111
|
+
oldLineNo: null,
|
|
112
|
+
newLineNo: newLine++,
|
|
113
|
+
oldContent: null,
|
|
114
|
+
newContent: a,
|
|
115
|
+
})
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
i++ // consumed the next 'added' change
|
|
119
|
+
} else {
|
|
120
|
+
for (const line of lines) {
|
|
121
|
+
rows.push({
|
|
122
|
+
kind: 'removed',
|
|
123
|
+
oldLineNo: oldLine++,
|
|
124
|
+
newLineNo: null,
|
|
125
|
+
oldContent: line,
|
|
126
|
+
newContent: null,
|
|
127
|
+
})
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
} else if (c.added) {
|
|
131
|
+
// standalone added (not preceded by a removed block — already handled above)
|
|
132
|
+
for (const line of lines) {
|
|
133
|
+
rows.push({
|
|
134
|
+
kind: 'added',
|
|
135
|
+
oldLineNo: null,
|
|
136
|
+
newLineNo: newLine++,
|
|
137
|
+
oldContent: null,
|
|
138
|
+
newContent: line,
|
|
139
|
+
})
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return rows
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const rows = $derived.by<Row[]>(() => {
|
|
148
|
+
const oldT = oldText ?? ''
|
|
149
|
+
const newT = newText ?? ''
|
|
150
|
+
if (!oldT && !newT) return []
|
|
151
|
+
if (!oldT) {
|
|
152
|
+
// pure-add — every line is 'added'
|
|
153
|
+
return newT.replace(/\n$/, '').split('\n').map((line, i) => ({
|
|
154
|
+
kind: 'added' as const,
|
|
155
|
+
oldLineNo: null,
|
|
156
|
+
newLineNo: i + 1,
|
|
157
|
+
oldContent: null,
|
|
158
|
+
newContent: line,
|
|
159
|
+
}))
|
|
160
|
+
}
|
|
161
|
+
if (!newT) {
|
|
162
|
+
// pure-remove — every line is 'removed'
|
|
163
|
+
return oldT.replace(/\n$/, '').split('\n').map((line, i) => ({
|
|
164
|
+
kind: 'removed' as const,
|
|
165
|
+
oldLineNo: i + 1,
|
|
166
|
+
newLineNo: null,
|
|
167
|
+
oldContent: line,
|
|
168
|
+
newContent: null,
|
|
169
|
+
}))
|
|
170
|
+
}
|
|
171
|
+
return buildRows(oldT, newT)
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
function lineNoStr(n: number | null): string {
|
|
175
|
+
return n === null ? ' ' : String(n).padStart(3, ' ')
|
|
176
|
+
}
|
|
177
|
+
</script>
|
|
178
|
+
|
|
179
|
+
<div class="hunk-diff text-xs font-mono leading-relaxed rounded border border-base-300/60 overflow-hidden bg-base-200/30">
|
|
180
|
+
{#if rows.length === 0}
|
|
181
|
+
<div class="px-3 py-4 text-center text-base-content/40 italic">no content</div>
|
|
182
|
+
{:else if unified}
|
|
183
|
+
<!-- Unified column -->
|
|
184
|
+
<div class="flex flex-col">
|
|
185
|
+
{#each rows as row, i (i)}
|
|
186
|
+
{#if row.kind === 'context'}
|
|
187
|
+
<div class="diff-row diff-row-context">
|
|
188
|
+
<span class="diff-gutter">{lineNoStr(row.oldLineNo)}</span>
|
|
189
|
+
<span class="diff-gutter">{lineNoStr(row.newLineNo)}</span>
|
|
190
|
+
<span class="diff-marker"> </span>
|
|
191
|
+
<span class="diff-content">{row.oldContent}</span>
|
|
192
|
+
</div>
|
|
193
|
+
{:else if row.kind === 'removed'}
|
|
194
|
+
<div class="diff-row diff-row-removed">
|
|
195
|
+
<span class="diff-gutter">{lineNoStr(row.oldLineNo)}</span>
|
|
196
|
+
<span class="diff-gutter"> </span>
|
|
197
|
+
<span class="diff-marker">−</span>
|
|
198
|
+
<span class="diff-content line-through opacity-80">{row.oldContent}</span>
|
|
199
|
+
</div>
|
|
200
|
+
{:else if row.kind === 'added'}
|
|
201
|
+
<div class="diff-row diff-row-added">
|
|
202
|
+
<span class="diff-gutter"> </span>
|
|
203
|
+
<span class="diff-gutter">{lineNoStr(row.newLineNo)}</span>
|
|
204
|
+
<span class="diff-marker">+</span>
|
|
205
|
+
<span class="diff-content">{row.newContent}</span>
|
|
206
|
+
</div>
|
|
207
|
+
{:else if row.kind === 'modified'}
|
|
208
|
+
<div class="diff-row diff-row-removed">
|
|
209
|
+
<span class="diff-gutter">{lineNoStr(row.oldLineNo)}</span>
|
|
210
|
+
<span class="diff-gutter"> </span>
|
|
211
|
+
<span class="diff-marker">−</span>
|
|
212
|
+
<span class="diff-content">
|
|
213
|
+
{#each row.oldSegments ?? [] as seg, j (j)}{#if seg.changed}<span class="word-removed">{seg.text}</span>{:else}<span>{seg.text}</span>{/if}{/each}
|
|
214
|
+
</span>
|
|
215
|
+
</div>
|
|
216
|
+
<div class="diff-row diff-row-added">
|
|
217
|
+
<span class="diff-gutter"> </span>
|
|
218
|
+
<span class="diff-gutter">{lineNoStr(row.newLineNo)}</span>
|
|
219
|
+
<span class="diff-marker">+</span>
|
|
220
|
+
<span class="diff-content">
|
|
221
|
+
{#each row.newSegments ?? [] as seg, j (j)}{#if seg.changed}<span class="word-added">{seg.text}</span>{:else}<span>{seg.text}</span>{/if}{/each}
|
|
222
|
+
</span>
|
|
223
|
+
</div>
|
|
224
|
+
{/if}
|
|
225
|
+
{/each}
|
|
226
|
+
</div>
|
|
227
|
+
{:else}
|
|
228
|
+
<!-- Side-by-side: two synced columns -->
|
|
229
|
+
<div class="grid grid-cols-2 divide-x divide-base-300/60">
|
|
230
|
+
<!-- LEFT (current/old) -->
|
|
231
|
+
<div class="flex flex-col">
|
|
232
|
+
{#each rows as row, i (i)}
|
|
233
|
+
{#if row.kind === 'context'}
|
|
234
|
+
<div class="diff-row diff-row-context">
|
|
235
|
+
<span class="diff-gutter">{lineNoStr(row.oldLineNo)}</span>
|
|
236
|
+
<span class="diff-marker"> </span>
|
|
237
|
+
<span class="diff-content">{row.oldContent}</span>
|
|
238
|
+
</div>
|
|
239
|
+
{:else if row.kind === 'removed'}
|
|
240
|
+
<div class="diff-row diff-row-removed">
|
|
241
|
+
<span class="diff-gutter">{lineNoStr(row.oldLineNo)}</span>
|
|
242
|
+
<span class="diff-marker">−</span>
|
|
243
|
+
<span class="diff-content line-through opacity-80">{row.oldContent}</span>
|
|
244
|
+
</div>
|
|
245
|
+
{:else if row.kind === 'added'}
|
|
246
|
+
<div class="diff-row diff-row-empty">
|
|
247
|
+
<span class="diff-gutter"> </span>
|
|
248
|
+
<span class="diff-marker"> </span>
|
|
249
|
+
<span class="diff-content"> </span>
|
|
250
|
+
</div>
|
|
251
|
+
{:else if row.kind === 'modified'}
|
|
252
|
+
<div class="diff-row diff-row-removed">
|
|
253
|
+
<span class="diff-gutter">{lineNoStr(row.oldLineNo)}</span>
|
|
254
|
+
<span class="diff-marker">−</span>
|
|
255
|
+
<span class="diff-content">
|
|
256
|
+
{#each row.oldSegments ?? [] as seg, j (j)}{#if seg.changed}<span class="word-removed">{seg.text}</span>{:else}<span>{seg.text}</span>{/if}{/each}
|
|
257
|
+
</span>
|
|
258
|
+
</div>
|
|
259
|
+
{/if}
|
|
260
|
+
{/each}
|
|
261
|
+
</div>
|
|
262
|
+
|
|
263
|
+
<!-- RIGHT (proposed/new) -->
|
|
264
|
+
<div class="flex flex-col">
|
|
265
|
+
{#each rows as row, i (i)}
|
|
266
|
+
{#if row.kind === 'context'}
|
|
267
|
+
<div class="diff-row diff-row-context">
|
|
268
|
+
<span class="diff-gutter">{lineNoStr(row.newLineNo)}</span>
|
|
269
|
+
<span class="diff-marker"> </span>
|
|
270
|
+
<span class="diff-content">{row.newContent}</span>
|
|
271
|
+
</div>
|
|
272
|
+
{:else if row.kind === 'added'}
|
|
273
|
+
<div class="diff-row diff-row-added">
|
|
274
|
+
<span class="diff-gutter">{lineNoStr(row.newLineNo)}</span>
|
|
275
|
+
<span class="diff-marker">+</span>
|
|
276
|
+
<span class="diff-content">{row.newContent}</span>
|
|
277
|
+
</div>
|
|
278
|
+
{:else if row.kind === 'removed'}
|
|
279
|
+
<div class="diff-row diff-row-empty">
|
|
280
|
+
<span class="diff-gutter"> </span>
|
|
281
|
+
<span class="diff-marker"> </span>
|
|
282
|
+
<span class="diff-content"> </span>
|
|
283
|
+
</div>
|
|
284
|
+
{:else if row.kind === 'modified'}
|
|
285
|
+
<div class="diff-row diff-row-added">
|
|
286
|
+
<span class="diff-gutter">{lineNoStr(row.newLineNo)}</span>
|
|
287
|
+
<span class="diff-marker">+</span>
|
|
288
|
+
<span class="diff-content">
|
|
289
|
+
{#each row.newSegments ?? [] as seg, j (j)}{#if seg.changed}<span class="word-added">{seg.text}</span>{:else}<span>{seg.text}</span>{/if}{/each}
|
|
290
|
+
</span>
|
|
291
|
+
</div>
|
|
292
|
+
{/if}
|
|
293
|
+
{/each}
|
|
294
|
+
</div>
|
|
295
|
+
</div>
|
|
296
|
+
{/if}
|
|
297
|
+
</div>
|
|
298
|
+
|
|
299
|
+
<style>
|
|
300
|
+
.diff-row {
|
|
301
|
+
display: grid;
|
|
302
|
+
grid-template-columns: auto auto 1fr;
|
|
303
|
+
column-gap: 0.5rem;
|
|
304
|
+
padding: 0 0.5rem;
|
|
305
|
+
align-items: baseline;
|
|
306
|
+
min-height: 1.4em;
|
|
307
|
+
white-space: pre-wrap;
|
|
308
|
+
word-break: break-word;
|
|
309
|
+
}
|
|
310
|
+
.diff-gutter {
|
|
311
|
+
color: color-mix(in oklab, currentColor 35%, transparent);
|
|
312
|
+
font-variant-numeric: tabular-nums;
|
|
313
|
+
user-select: none;
|
|
314
|
+
text-align: right;
|
|
315
|
+
}
|
|
316
|
+
.diff-marker {
|
|
317
|
+
color: color-mix(in oklab, currentColor 55%, transparent);
|
|
318
|
+
width: 0.75em;
|
|
319
|
+
user-select: none;
|
|
320
|
+
}
|
|
321
|
+
.diff-row-removed {
|
|
322
|
+
background-color: color-mix(in oklab, var(--color-error, oklch(0.68 0.18 28)) 12%, transparent);
|
|
323
|
+
}
|
|
324
|
+
.diff-row-removed .diff-marker {
|
|
325
|
+
color: var(--color-error, oklch(0.68 0.18 28));
|
|
326
|
+
}
|
|
327
|
+
.diff-row-added {
|
|
328
|
+
background-color: color-mix(in oklab, var(--color-success, oklch(0.7 0.17 145)) 12%, transparent);
|
|
329
|
+
}
|
|
330
|
+
.diff-row-added .diff-marker {
|
|
331
|
+
color: var(--color-success, oklch(0.7 0.17 145));
|
|
332
|
+
}
|
|
333
|
+
.diff-row-empty {
|
|
334
|
+
background-color: color-mix(in oklab, currentColor 3%, transparent);
|
|
335
|
+
}
|
|
336
|
+
.diff-row-context {
|
|
337
|
+
background-color: transparent;
|
|
338
|
+
}
|
|
339
|
+
.word-removed {
|
|
340
|
+
background-color: color-mix(in oklab, var(--color-error, oklch(0.68 0.18 28)) 28%, transparent);
|
|
341
|
+
text-decoration: line-through;
|
|
342
|
+
text-decoration-color: color-mix(in oklab, var(--color-error, oklch(0.68 0.18 28)) 65%, transparent);
|
|
343
|
+
}
|
|
344
|
+
.word-added {
|
|
345
|
+
background-color: color-mix(in oklab, var(--color-success, oklch(0.7 0.17 145)) 28%, transparent);
|
|
346
|
+
font-weight: 600;
|
|
347
|
+
}
|
|
348
|
+
</style>
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
import type { Annotation } from '../types/annotation'
|
|
3
|
+
import type { MarkupShape } from '../components/markup'
|
|
4
|
+
import AnnotationLayer from './AnnotationLayer.svelte'
|
|
5
|
+
import MarkupOverlay from './MarkupOverlay.svelte'
|
|
6
|
+
|
|
7
|
+
interface Props {
|
|
8
|
+
/** URL of the image to display */
|
|
9
|
+
imageUrl: string
|
|
10
|
+
/** Alt text / filename for accessibility */
|
|
11
|
+
filename?: string
|
|
12
|
+
/** Markup shapes to overlay on the image */
|
|
13
|
+
markupShapes?: MarkupShape[]
|
|
14
|
+
/** Annotation pins to display */
|
|
15
|
+
annotations?: Annotation[]
|
|
16
|
+
/** Active annotation ID (highlighted pin) */
|
|
17
|
+
activeAnnotationId?: string | null
|
|
18
|
+
/** Whether annotation placement mode is active */
|
|
19
|
+
annotationMode?: boolean
|
|
20
|
+
/** Called when a pin is clicked */
|
|
21
|
+
onAnnotationClick?: (annotation: Annotation) => void
|
|
22
|
+
/** Called when the image is clicked in annotation mode */
|
|
23
|
+
onImageClick?: (x: number, y: number) => void
|
|
24
|
+
/** Called when the lightbox requests close */
|
|
25
|
+
onClose?: () => void
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
let {
|
|
29
|
+
imageUrl,
|
|
30
|
+
filename = "Image",
|
|
31
|
+
markupShapes = [],
|
|
32
|
+
annotations = [],
|
|
33
|
+
activeAnnotationId = null,
|
|
34
|
+
annotationMode = false,
|
|
35
|
+
onAnnotationClick,
|
|
36
|
+
onImageClick,
|
|
37
|
+
onClose,
|
|
38
|
+
}: Props = $props()
|
|
39
|
+
|
|
40
|
+
let isFullscreen = $state(false)
|
|
41
|
+
let zoom = $state(1)
|
|
42
|
+
let isLoading = $state(true)
|
|
43
|
+
let error = $state(false)
|
|
44
|
+
|
|
45
|
+
function handleImageLoad() {
|
|
46
|
+
isLoading = false
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function handleImageError() {
|
|
50
|
+
error = true
|
|
51
|
+
isLoading = false
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function toggleFullscreen() {
|
|
55
|
+
isFullscreen = !isFullscreen
|
|
56
|
+
if (!isFullscreen) {
|
|
57
|
+
zoom = 1
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function zoomIn() {
|
|
62
|
+
zoom = Math.min(3, zoom + 0.25)
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function zoomOut() {
|
|
66
|
+
zoom = Math.max(0.5, zoom - 0.25)
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function resetZoom() {
|
|
70
|
+
zoom = 1
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function handleKeydown(e: KeyboardEvent) {
|
|
74
|
+
if (e.key === "Escape") {
|
|
75
|
+
if (isFullscreen) {
|
|
76
|
+
isFullscreen = false
|
|
77
|
+
zoom = 1
|
|
78
|
+
} else {
|
|
79
|
+
onClose?.()
|
|
80
|
+
}
|
|
81
|
+
} else if (e.key === "+" || e.key === "=") {
|
|
82
|
+
zoomIn()
|
|
83
|
+
} else if (e.key === "-") {
|
|
84
|
+
zoomOut()
|
|
85
|
+
} else if (e.key === "0") {
|
|
86
|
+
resetZoom()
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function handleBackdropClick(e: MouseEvent) {
|
|
91
|
+
if (e.target === e.currentTarget) {
|
|
92
|
+
if (isFullscreen) {
|
|
93
|
+
isFullscreen = false
|
|
94
|
+
zoom = 1
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
</script>
|
|
99
|
+
|
|
100
|
+
<svelte:window onkeydown={handleKeydown} />
|
|
101
|
+
|
|
102
|
+
<div class="flex flex-col h-full min-h-[400px]">
|
|
103
|
+
<!-- Inline Preview -->
|
|
104
|
+
<div class="flex-1 bg-base-200 flex items-center justify-center p-2 overflow-auto">
|
|
105
|
+
{#if isLoading}
|
|
106
|
+
<div class="flex items-center justify-center">
|
|
107
|
+
<span class="loading loading-spinner loading-lg text-primary"></span>
|
|
108
|
+
</div>
|
|
109
|
+
{/if}
|
|
110
|
+
|
|
111
|
+
{#if error}
|
|
112
|
+
<div class="text-center">
|
|
113
|
+
<svg
|
|
114
|
+
class="mx-auto h-12 w-12 text-error"
|
|
115
|
+
fill="none"
|
|
116
|
+
stroke="currentColor"
|
|
117
|
+
viewBox="0 0 24 24"
|
|
118
|
+
>
|
|
119
|
+
<path
|
|
120
|
+
stroke-linecap="round"
|
|
121
|
+
stroke-linejoin="round"
|
|
122
|
+
stroke-width="2"
|
|
123
|
+
d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"
|
|
124
|
+
/>
|
|
125
|
+
</svg>
|
|
126
|
+
<p class="mt-2 text-error">Failed to load image</p>
|
|
127
|
+
</div>
|
|
128
|
+
{:else}
|
|
129
|
+
<div
|
|
130
|
+
class="relative inline-block"
|
|
131
|
+
class:hidden={isLoading}
|
|
132
|
+
>
|
|
133
|
+
{#if !annotationMode}
|
|
134
|
+
<button
|
|
135
|
+
type="button"
|
|
136
|
+
onclick={toggleFullscreen}
|
|
137
|
+
class="focus:outline-none focus:ring-2 focus:ring-primary rounded"
|
|
138
|
+
aria-label="Click to view fullscreen"
|
|
139
|
+
>
|
|
140
|
+
<img
|
|
141
|
+
src={imageUrl}
|
|
142
|
+
alt={filename}
|
|
143
|
+
class="max-w-full max-h-full object-contain rounded border border-base-300 shadow-lg cursor-zoom-in"
|
|
144
|
+
style="max-height: 60vh;"
|
|
145
|
+
onload={handleImageLoad}
|
|
146
|
+
onerror={handleImageError}
|
|
147
|
+
/>
|
|
148
|
+
</button>
|
|
149
|
+
{:else}
|
|
150
|
+
<img
|
|
151
|
+
src={imageUrl}
|
|
152
|
+
alt={filename}
|
|
153
|
+
class="max-w-full max-h-full object-contain rounded border border-base-300 shadow-lg"
|
|
154
|
+
style="max-height: 60vh;"
|
|
155
|
+
onload={handleImageLoad}
|
|
156
|
+
onerror={handleImageError}
|
|
157
|
+
/>
|
|
158
|
+
{/if}
|
|
159
|
+
|
|
160
|
+
<!-- Markup shapes overlay -->
|
|
161
|
+
{#if markupShapes.length > 0}
|
|
162
|
+
<MarkupOverlay shapes={markupShapes} {imageUrl} />
|
|
163
|
+
{/if}
|
|
164
|
+
|
|
165
|
+
<!-- Annotation overlay -->
|
|
166
|
+
{#if annotations.length > 0 || annotationMode}
|
|
167
|
+
<AnnotationLayer
|
|
168
|
+
{annotations}
|
|
169
|
+
{activeAnnotationId}
|
|
170
|
+
{annotationMode}
|
|
171
|
+
{onAnnotationClick}
|
|
172
|
+
{onImageClick}
|
|
173
|
+
/>
|
|
174
|
+
{/if}
|
|
175
|
+
</div>
|
|
176
|
+
{/if}
|
|
177
|
+
</div>
|
|
178
|
+
</div>
|
|
179
|
+
|
|
180
|
+
<!-- Fullscreen Modal -->
|
|
181
|
+
{#if isFullscreen}
|
|
182
|
+
<div
|
|
183
|
+
class="fixed inset-0 z-50 bg-black/90 flex items-center justify-center"
|
|
184
|
+
onclick={handleBackdropClick}
|
|
185
|
+
onkeydown={(e) => e.key === "Escape" && handleBackdropClick(e as unknown as MouseEvent)}
|
|
186
|
+
role="dialog"
|
|
187
|
+
aria-modal="true"
|
|
188
|
+
aria-label="Image fullscreen view"
|
|
189
|
+
tabindex="-1"
|
|
190
|
+
>
|
|
191
|
+
<!-- Close Button -->
|
|
192
|
+
<button
|
|
193
|
+
class="absolute top-4 right-4 btn btn-circle btn-ghost text-white hover:bg-white/20"
|
|
194
|
+
onclick={() => { isFullscreen = false; zoom = 1; }}
|
|
195
|
+
title="Close (Esc)"
|
|
196
|
+
>
|
|
197
|
+
<svg class="h-6 w-6" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
198
|
+
<path
|
|
199
|
+
stroke-linecap="round"
|
|
200
|
+
stroke-linejoin="round"
|
|
201
|
+
stroke-width="2"
|
|
202
|
+
d="M6 18L18 6M6 6l12 12"
|
|
203
|
+
/>
|
|
204
|
+
</svg>
|
|
205
|
+
</button>
|
|
206
|
+
|
|
207
|
+
<!-- Zoom Controls -->
|
|
208
|
+
<div class="absolute bottom-4 left-1/2 transform -translate-x-1/2 flex items-center gap-2 bg-black/50 rounded-lg p-2">
|
|
209
|
+
<button
|
|
210
|
+
class="btn btn-sm btn-ghost text-white"
|
|
211
|
+
onclick={zoomOut}
|
|
212
|
+
disabled={zoom <= 0.5}
|
|
213
|
+
title="Zoom out (-)"
|
|
214
|
+
>
|
|
215
|
+
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
216
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M20 12H4" />
|
|
217
|
+
</svg>
|
|
218
|
+
</button>
|
|
219
|
+
|
|
220
|
+
<span class="text-white text-sm min-w-[60px] text-center">
|
|
221
|
+
{Math.round(zoom * 100)}%
|
|
222
|
+
</span>
|
|
223
|
+
|
|
224
|
+
<button
|
|
225
|
+
class="btn btn-sm btn-ghost text-white"
|
|
226
|
+
onclick={zoomIn}
|
|
227
|
+
disabled={zoom >= 3}
|
|
228
|
+
title="Zoom in (+)"
|
|
229
|
+
>
|
|
230
|
+
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
231
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4" />
|
|
232
|
+
</svg>
|
|
233
|
+
</button>
|
|
234
|
+
|
|
235
|
+
<button
|
|
236
|
+
class="btn btn-sm btn-ghost text-white"
|
|
237
|
+
onclick={resetZoom}
|
|
238
|
+
title="Reset zoom (0)"
|
|
239
|
+
>
|
|
240
|
+
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
241
|
+
<path
|
|
242
|
+
stroke-linecap="round"
|
|
243
|
+
stroke-linejoin="round"
|
|
244
|
+
stroke-width="2"
|
|
245
|
+
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"
|
|
246
|
+
/>
|
|
247
|
+
</svg>
|
|
248
|
+
</button>
|
|
249
|
+
</div>
|
|
250
|
+
|
|
251
|
+
<!-- Image with overlays -->
|
|
252
|
+
<div class="relative overflow-auto max-w-full max-h-full p-4">
|
|
253
|
+
<div class="relative inline-block">
|
|
254
|
+
<img
|
|
255
|
+
src={imageUrl}
|
|
256
|
+
alt={filename}
|
|
257
|
+
class="transition-transform duration-200"
|
|
258
|
+
style="transform: scale({zoom}); transform-origin: center center;"
|
|
259
|
+
/>
|
|
260
|
+
{#if markupShapes.length > 0}
|
|
261
|
+
<MarkupOverlay shapes={markupShapes} {imageUrl} />
|
|
262
|
+
{/if}
|
|
263
|
+
{#if annotations.length > 0}
|
|
264
|
+
<AnnotationLayer
|
|
265
|
+
{annotations}
|
|
266
|
+
{activeAnnotationId}
|
|
267
|
+
annotationMode={false}
|
|
268
|
+
{onAnnotationClick}
|
|
269
|
+
/>
|
|
270
|
+
{/if}
|
|
271
|
+
</div>
|
|
272
|
+
</div>
|
|
273
|
+
</div>
|
|
274
|
+
{/if}
|