@joewinke/jatui 0.1.10 → 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.
- package/README.md +123 -0
- package/package.json +3 -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 +21 -15
- 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 +188 -0
- 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/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 +15 -9
- package/src/lib/components/InputDialog.svelte +327 -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/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 +183 -63
- 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/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/index.ts +105 -1
- 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/smartImageEditor.ts +39 -0
- package/src/lib/types/templateVars.ts +36 -0
- package/src/lib/utils/dateFormatters.ts +12 -10
- package/src/lib/utils/phone.ts +80 -0
- package/src/lib/utils/taskUtils.ts +21 -7
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
<script lang="ts">
|
|
2
|
+
interface Props {
|
|
3
|
+
/** Display name of the file */
|
|
4
|
+
name: string
|
|
5
|
+
/** MIME type — used to pick the right icon / thumbnail */
|
|
6
|
+
mimeType: string
|
|
7
|
+
/** URL to render as image thumbnail (for image files) */
|
|
8
|
+
url?: string
|
|
9
|
+
size?: "sm" | "md" | "lg" | "xl"
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
let { name, mimeType, url, size = "md" }: Props = $props()
|
|
13
|
+
|
|
14
|
+
let imageError = $state(false)
|
|
15
|
+
let isLoading = $state(true)
|
|
16
|
+
|
|
17
|
+
const sizeClasses: Record<string, string> = {
|
|
18
|
+
sm: "h-8 w-8",
|
|
19
|
+
md: "h-16 w-16",
|
|
20
|
+
lg: "h-24 w-24",
|
|
21
|
+
xl: "h-32 w-32",
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const isImageFile = $derived(mimeType.startsWith("image/"))
|
|
25
|
+
const isPdfFile = $derived(mimeType === "application/pdf")
|
|
26
|
+
const isCadFile = $derived(mimeType.includes("dwg") || mimeType.includes("autocad"))
|
|
27
|
+
const isZipFile = $derived(
|
|
28
|
+
mimeType === "application/zip" ||
|
|
29
|
+
mimeType === "application/x-zip-compressed" ||
|
|
30
|
+
mimeType === "application/x-zip"
|
|
31
|
+
)
|
|
32
|
+
</script>
|
|
33
|
+
|
|
34
|
+
<div class="relative {sizeClasses[size]} flex-shrink-0">
|
|
35
|
+
{#if isImageFile && url && !imageError}
|
|
36
|
+
<img
|
|
37
|
+
src={url}
|
|
38
|
+
alt={name}
|
|
39
|
+
class="w-full h-full object-cover rounded border border-base-300 shadow-sm"
|
|
40
|
+
onload={() => { isLoading = false }}
|
|
41
|
+
onerror={() => { imageError = true; isLoading = false }}
|
|
42
|
+
/>
|
|
43
|
+
{:else}
|
|
44
|
+
<div class="w-full h-full bg-base-200 rounded border border-base-300 flex items-center justify-center">
|
|
45
|
+
{#if isPdfFile}
|
|
46
|
+
<svg class="h-6 w-6 text-error" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
|
47
|
+
<path d="M14,2H6A2,2 0 0,0 4,4V20A2,2 0 0,0 6,22H18A2,2 0 0,0 20,20V8L14,2M18,20H6V4H13V9H18V20M10.92,12.31C10.68,11.54 10.15,9.08 11.55,9.04C12.95,9 12.03,12.16 12.03,12.16C12.42,13.65 14.05,14.72 14.05,14.72C14.55,14.57 17.4,14.24 17,15.72C16.57,17.2 13.5,15.81 13.5,15.81C11.55,15.95 10.09,16.47 10.09,16.47C8.96,18.58 7.64,19.5 7.1,18.61C6.43,17.5 9.23,16.07 9.23,16.07C10.68,13.67 10.92,12.31 10.92,12.31Z" />
|
|
48
|
+
</svg>
|
|
49
|
+
{:else if isCadFile}
|
|
50
|
+
<svg class="h-6 w-6 text-info" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
|
51
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
|
52
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 10l5-5 5 5" />
|
|
53
|
+
</svg>
|
|
54
|
+
{:else if isZipFile}
|
|
55
|
+
<svg class="h-6 w-6 text-warning" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
|
56
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 8h14M5 8a2 2 0 110-4h14a2 2 0 110 4M5 8v10a2 2 0 002 2h10a2 2 0 002-2V8m-9 4h4" />
|
|
57
|
+
</svg>
|
|
58
|
+
{:else}
|
|
59
|
+
<svg class="h-6 w-6 text-base-content/40" fill="none" stroke="currentColor" viewBox="0 0 24 24" aria-hidden="true">
|
|
60
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z" />
|
|
61
|
+
</svg>
|
|
62
|
+
{/if}
|
|
63
|
+
</div>
|
|
64
|
+
{/if}
|
|
65
|
+
|
|
66
|
+
{#if isLoading && isImageFile && url}
|
|
67
|
+
<div class="absolute inset-0 bg-base-200 rounded animate-pulse"></div>
|
|
68
|
+
{/if}
|
|
69
|
+
|
|
70
|
+
<!-- File type badge for small size -->
|
|
71
|
+
{#if size === "sm" && (isPdfFile || isCadFile)}
|
|
72
|
+
<div
|
|
73
|
+
class="absolute -top-1 -right-1 w-3 h-3 rounded-full text-xs font-bold text-white flex items-center justify-center
|
|
74
|
+
{isPdfFile ? 'bg-error' : 'bg-info'}"
|
|
75
|
+
aria-hidden="true"
|
|
76
|
+
>
|
|
77
|
+
{isPdfFile ? "P" : "D"}
|
|
78
|
+
</div>
|
|
79
|
+
{/if}
|
|
80
|
+
</div>
|
|
@@ -109,18 +109,18 @@
|
|
|
109
109
|
onchange={() => handleToggle(opt.value)}
|
|
110
110
|
/>
|
|
111
111
|
<span>{opt.label}</span>
|
|
112
|
-
<span class="text-
|
|
112
|
+
<span class="text-[0.75rem] opacity-60">({opt.count})</span>
|
|
113
113
|
</label>
|
|
114
114
|
{:else}
|
|
115
115
|
<button
|
|
116
116
|
class="badge badge-sm transition-all duration-200 cursor-pointer {selected.has(opt.value)
|
|
117
|
-
? colorFn(opt.value, true)
|
|
118
|
-
: 'badge-ghost hover:badge-primary/20
|
|
117
|
+
? colorFn(opt.value, true)
|
|
118
|
+
: 'badge-ghost hover:badge-primary/20'}"
|
|
119
119
|
onclick={() => handleToggle(opt.value)}
|
|
120
120
|
onkeydown={(e) => handleKeydown(e, opt.value)}
|
|
121
121
|
>
|
|
122
122
|
{opt.label}
|
|
123
|
-
<span class="ml-1 opacity-
|
|
123
|
+
<span class="ml-1 text-[0.75rem] opacity-60">({opt.count})</span>
|
|
124
124
|
</button>
|
|
125
125
|
{/if}
|
|
126
126
|
{/each}
|
|
@@ -132,11 +132,11 @@
|
|
|
132
132
|
<div
|
|
133
133
|
tabindex="0"
|
|
134
134
|
role="button"
|
|
135
|
-
class="px-2.5 py-1 rounded cursor-pointer transition-all flex items-center gap-1.5
|
|
135
|
+
class="px-2.5 py-1 rounded cursor-pointer transition-all flex items-center gap-1.5 text-[0.8125rem] bg-base-200 border border-base-300 text-base-content/60"
|
|
136
136
|
>
|
|
137
|
-
<span
|
|
137
|
+
<span>{label}</span>
|
|
138
138
|
<span
|
|
139
|
-
class="px-1.5 py-0.5 rounded text-
|
|
139
|
+
class="px-1.5 py-0.5 rounded text-[0.75rem] {selected.size > 0 && !(emptyMeansAll && selected.size === 0) ? 'bg-primary/20 text-primary' : 'bg-base-300 text-base-content opacity-60'}"
|
|
140
140
|
>
|
|
141
141
|
{displayText}
|
|
142
142
|
</span>
|
|
@@ -171,8 +171,8 @@
|
|
|
171
171
|
checked={selected.has(opt.value)}
|
|
172
172
|
onchange={() => handleToggle(opt.value)}
|
|
173
173
|
/>
|
|
174
|
-
<span class="truncate
|
|
175
|
-
<span class="text-
|
|
174
|
+
<span class="truncate text-[0.8125rem]">{opt.label}</span>
|
|
175
|
+
<span class="text-[0.75rem] text-base-content/45">({opt.count})</span>
|
|
176
176
|
</label>
|
|
177
177
|
</li>
|
|
178
178
|
{/each}
|
|
@@ -183,12 +183,12 @@
|
|
|
183
183
|
<div
|
|
184
184
|
tabindex="0"
|
|
185
185
|
role="menu"
|
|
186
|
-
class="dropdown-content rounded-box
|
|
186
|
+
class="dropdown-content rounded-box p-2 z-40 {menuWidth} mt-1 {maxHeight ? maxHeight + ' overflow-y-auto' : ''} bg-base-200 border border-base-300"
|
|
187
187
|
>
|
|
188
188
|
<div class="flex flex-wrap gap-1.5">
|
|
189
189
|
{#each options as opt, index}
|
|
190
190
|
<button
|
|
191
|
-
class="px-2 py-0.5 rounded
|
|
191
|
+
class="px-2 py-0.5 rounded text-[0.75rem] transition-all cursor-pointer border {selected.has(opt.value) ? 'bg-primary/20 border-primary/40 text-primary' : 'bg-base-300 border-base-content/20 text-base-content opacity-70 hover:opacity-100'}"
|
|
192
192
|
onclick={() => handleToggle(opt.value)}
|
|
193
193
|
onkeydown={(e) => handleKeydown(e, opt.value)}
|
|
194
194
|
>
|
|
@@ -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>
|