@joewinke/jatui 0.1.23 → 0.1.25
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/CLAUDE.md +244 -0
- package/package.json +23 -10
- package/src/lib/actions/collapse.ts +0 -136
- package/src/lib/actions/railNav.ts +0 -473
- package/src/lib/components/AnnotationLayer.svelte +0 -108
- package/src/lib/components/AnnotationPanel.svelte +0 -319
- package/src/lib/components/AudioWaveform.svelte +0 -698
- package/src/lib/components/AvailabilityModal.svelte +0 -177
- package/src/lib/components/AvatarUpload.svelte +0 -234
- package/src/lib/components/Badge.svelte +0 -38
- package/src/lib/components/BookingForm.svelte +0 -278
- package/src/lib/components/BurndownChart.svelte +0 -778
- package/src/lib/components/Button.svelte +0 -81
- package/src/lib/components/CalendarPicker.svelte +0 -284
- package/src/lib/components/Card.svelte +0 -67
- package/src/lib/components/CharacterCounter.svelte +0 -82
- package/src/lib/components/ChipInput.svelte +0 -609
- package/src/lib/components/Collapse.svelte +0 -82
- package/src/lib/components/ColorSelector.svelte +0 -167
- package/src/lib/components/CommentThread.svelte +0 -773
- package/src/lib/components/ConfirmDialog.svelte +0 -348
- package/src/lib/components/ConfirmModal.svelte +0 -142
- package/src/lib/components/ContextMenu.svelte +0 -188
- package/src/lib/components/CountdownTimer.svelte +0 -94
- package/src/lib/components/DateRangePicker.svelte +0 -194
- package/src/lib/components/Drawer.svelte +0 -143
- package/src/lib/components/EmojiPicker.svelte +0 -159
- package/src/lib/components/EntityPreviewCard.svelte +0 -104
- package/src/lib/components/FileDropzone.svelte +0 -493
- package/src/lib/components/FilePicker.svelte +0 -488
- package/src/lib/components/FileThumbnail.svelte +0 -80
- package/src/lib/components/FilterDropdown.svelte +0 -203
- package/src/lib/components/FloatingActionBar.svelte +0 -354
- package/src/lib/components/GPSTracker.svelte +0 -202
- package/src/lib/components/HunkDiffView.svelte +0 -348
- package/src/lib/components/ImageLightbox.svelte +0 -274
- package/src/lib/components/ImageUpload.svelte +0 -147
- package/src/lib/components/InlineEdit.svelte +0 -291
- package/src/lib/components/InputDialog.svelte +0 -327
- package/src/lib/components/KeyboardShortcutsOverlay.svelte +0 -296
- package/src/lib/components/LazyImage.svelte +0 -123
- package/src/lib/components/LinkShortener.svelte +0 -274
- package/src/lib/components/LoadingSpinner.svelte +0 -106
- package/src/lib/components/LocationMap.svelte +0 -186
- package/src/lib/components/MapView.svelte +0 -341
- package/src/lib/components/MarkupEditor.svelte +0 -485
- package/src/lib/components/MarkupOverlay.svelte +0 -55
- package/src/lib/components/MediaWorkbench.svelte +0 -871
- package/src/lib/components/MilestoneCard.svelte +0 -98
- package/src/lib/components/MilestoneTimeline.svelte +0 -91
- package/src/lib/components/Modal.svelte +0 -244
- package/src/lib/components/PDFViewer.svelte +0 -105
- package/src/lib/components/PdfThumbnail.svelte +0 -63
- package/src/lib/components/PhoneInput.svelte +0 -212
- package/src/lib/components/ResizableDivider.svelte +0 -307
- package/src/lib/components/ResizablePanel.svelte +0 -305
- package/src/lib/components/SearchDropdown.svelte +0 -421
- package/src/lib/components/SelectInput.svelte +0 -237
- package/src/lib/components/SidebarUserFooter.svelte +0 -34
- package/src/lib/components/SignaturePad.svelte +0 -175
- package/src/lib/components/SmartImageEditor.svelte +0 -720
- package/src/lib/components/SortDropdown.svelte +0 -156
- package/src/lib/components/Sparkline.svelte +0 -116
- package/src/lib/components/SpeechForm.svelte +0 -114
- package/src/lib/components/StatusBadge.svelte +0 -157
- package/src/lib/components/TaskTypeIcon.svelte +0 -54
- package/src/lib/components/TextArea.svelte +0 -162
- package/src/lib/components/TextInput.svelte +0 -131
- package/src/lib/components/ThemeSelector.svelte +0 -206
- package/src/lib/components/TimeSlotPicker.svelte +0 -162
- package/src/lib/components/UserAvatar.svelte +0 -72
- package/src/lib/components/VariablePicker.svelte +0 -170
- package/src/lib/components/VoicePlayer.svelte +0 -422
- package/src/lib/components/linked-columns/LinkedColumns.svelte +0 -526
- package/src/lib/components/markup.ts +0 -287
- package/src/lib/components/messaging/Avatar.svelte +0 -81
- package/src/lib/components/messaging/ChannelInfoModal.svelte +0 -163
- package/src/lib/components/messaging/ChannelList.svelte +0 -107
- package/src/lib/components/messaging/ChannelMemberAvatarStack.svelte +0 -69
- package/src/lib/components/messaging/ChannelMembersModal.svelte +0 -182
- package/src/lib/components/messaging/CreateChannelModal.svelte +0 -190
- package/src/lib/components/messaging/DirectMessageList.svelte +0 -145
- package/src/lib/components/messaging/EmojiSelector.svelte +0 -261
- package/src/lib/components/messaging/MentionAutocomplete.svelte +0 -193
- package/src/lib/components/messaging/MessageAttachment.svelte +0 -273
- package/src/lib/components/messaging/MessageAttachmentUpload.svelte +0 -243
- package/src/lib/components/messaging/MessageInput.svelte +0 -452
- package/src/lib/components/messaging/MessageItem.svelte +0 -341
- package/src/lib/components/messaging/MessageThread.svelte +0 -306
- package/src/lib/components/messaging/NotificationSettingsModal.svelte +0 -234
- package/src/lib/components/messaging/QuotedMessageDisplay.svelte +0 -123
- package/src/lib/components/messaging/StartDMModal.svelte +0 -100
- package/src/lib/components/messaging/ThreadPanel.svelte +0 -153
- package/src/lib/components/pipeline/DESIGN.md +0 -361
- package/src/lib/components/pipeline/Pipeline.svelte +0 -391
- package/src/lib/components/pipeline/PipelineCard.svelte +0 -94
- package/src/lib/components/pipeline/PipelineColumn.svelte +0 -163
- package/src/lib/components/replay/ChapterTimeline.svelte +0 -326
- package/src/lib/components/session-nav/transcriptModel.ts +0 -352
- package/src/lib/data/emojis.ts +0 -1208
- package/src/lib/index.ts +0 -405
- package/src/lib/stores/confirmDialog.svelte.ts +0 -48
- package/src/lib/stores/inputDialog.svelte.ts +0 -51
- package/src/lib/styles/rail.css +0 -63
- package/src/lib/types/annotation.ts +0 -38
- package/src/lib/types/booking.ts +0 -143
- package/src/lib/types/comments.ts +0 -97
- package/src/lib/types/entityPreview.ts +0 -45
- package/src/lib/types/filePicker.ts +0 -16
- package/src/lib/types/googleMaps.d.ts +0 -51
- package/src/lib/types/maps.ts +0 -43
- package/src/lib/types/messaging.ts +0 -459
- package/src/lib/types/milestone.ts +0 -22
- package/src/lib/types/pipeline.ts +0 -150
- package/src/lib/types/smartImageEditor.ts +0 -39
- package/src/lib/types/templateVars.ts +0 -36
- package/src/lib/utils/currency.ts +0 -20
- package/src/lib/utils/daisyuiColors.ts +0 -243
- package/src/lib/utils/dateFormatters.ts +0 -196
- package/src/lib/utils/googleMapsLoader.ts +0 -84
- package/src/lib/utils/mentionParser.ts +0 -188
- package/src/lib/utils/phone.ts +0 -80
- package/src/lib/utils/phoneFormat.ts +0 -74
- package/src/lib/utils/taskUtils.ts +0 -75
|
@@ -1,274 +0,0 @@
|
|
|
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}
|
|
@@ -1,147 +0,0 @@
|
|
|
1
|
-
<script lang="ts">
|
|
2
|
-
let {
|
|
3
|
-
imageUrl = "",
|
|
4
|
-
disabled = false,
|
|
5
|
-
label = "Upload Image",
|
|
6
|
-
maxSize = 10 * 1024 * 1024,
|
|
7
|
-
onUpload = undefined,
|
|
8
|
-
} = $props<{
|
|
9
|
-
imageUrl?: string
|
|
10
|
-
disabled?: boolean
|
|
11
|
-
label?: string
|
|
12
|
-
maxSize?: number
|
|
13
|
-
onUpload?: (url: string) => void
|
|
14
|
-
}>()
|
|
15
|
-
|
|
16
|
-
let error = $state("")
|
|
17
|
-
let isDragging = $state(false)
|
|
18
|
-
|
|
19
|
-
function handleDragOver(event: DragEvent) {
|
|
20
|
-
event.preventDefault()
|
|
21
|
-
isDragging = true
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
function handleDragLeave(event: DragEvent) {
|
|
25
|
-
// Only clear if leaving the label entirely (not entering a child)
|
|
26
|
-
if (!(event.currentTarget as HTMLElement).contains(event.relatedTarget as Node)) {
|
|
27
|
-
isDragging = false
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function handleDrop(event: DragEvent) {
|
|
32
|
-
event.preventDefault()
|
|
33
|
-
isDragging = false
|
|
34
|
-
error = ""
|
|
35
|
-
const file = event.dataTransfer?.files[0]
|
|
36
|
-
if (!file) return
|
|
37
|
-
if (!file.type.startsWith("image/")) {
|
|
38
|
-
error = "Please drop an image file."
|
|
39
|
-
return
|
|
40
|
-
}
|
|
41
|
-
if (file.size > maxSize) {
|
|
42
|
-
error = `File too large! Maximum size is ${formatFileSize(maxSize)}.`
|
|
43
|
-
return
|
|
44
|
-
}
|
|
45
|
-
const url = URL.createObjectURL(file)
|
|
46
|
-
imageUrl = url
|
|
47
|
-
onUpload?.(url)
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
async function handleFileInput(event: Event) {
|
|
51
|
-
error = ""
|
|
52
|
-
const input = event.target as HTMLInputElement
|
|
53
|
-
if (!input.files?.length) return
|
|
54
|
-
|
|
55
|
-
const file = input.files[0]
|
|
56
|
-
|
|
57
|
-
if (file.size > maxSize) {
|
|
58
|
-
error = `File too large! Maximum size is ${formatFileSize(maxSize)}.`
|
|
59
|
-
return
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
const url = URL.createObjectURL(file)
|
|
63
|
-
imageUrl = url
|
|
64
|
-
|
|
65
|
-
if (onUpload) {
|
|
66
|
-
onUpload(url)
|
|
67
|
-
}
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
function clearImage() {
|
|
71
|
-
if (imageUrl.startsWith("blob:")) {
|
|
72
|
-
URL.revokeObjectURL(imageUrl)
|
|
73
|
-
}
|
|
74
|
-
imageUrl = ""
|
|
75
|
-
error = ""
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
function formatFileSize(bytes: number): string {
|
|
79
|
-
if (bytes < 1024) return `${bytes} bytes`
|
|
80
|
-
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`
|
|
81
|
-
return `${(bytes / (1024 * 1024)).toFixed(1)} MB`
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
$effect(() => {
|
|
85
|
-
return () => {
|
|
86
|
-
if (imageUrl.startsWith("blob:")) {
|
|
87
|
-
URL.revokeObjectURL(imageUrl)
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
})
|
|
91
|
-
</script>
|
|
92
|
-
|
|
93
|
-
{#if imageUrl}
|
|
94
|
-
<div class="relative w-full h-40 bg-base-200 rounded-md overflow-hidden">
|
|
95
|
-
<img src={imageUrl} alt="Uploaded" class="w-full h-full object-contain" />
|
|
96
|
-
<button
|
|
97
|
-
class="btn btn-sm btn-circle absolute top-2 right-2 bg-base-100/80"
|
|
98
|
-
onclick={clearImage}
|
|
99
|
-
{disabled}
|
|
100
|
-
aria-label="Remove image"
|
|
101
|
-
>
|
|
102
|
-
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M18 6 6 18"/><path d="m6 6 12 12"/></svg>
|
|
103
|
-
</button>
|
|
104
|
-
</div>
|
|
105
|
-
{:else}
|
|
106
|
-
<!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
|
|
107
|
-
<label
|
|
108
|
-
class="flex flex-col items-center justify-center w-full h-32 border-2 border-dashed rounded-md cursor-pointer transition-all duration-200
|
|
109
|
-
{isDragging
|
|
110
|
-
? 'border-primary bg-primary/8 scale-[1.01]'
|
|
111
|
-
: 'border-base-content/20 bg-base-200 hover:bg-base-300 hover:border-base-content/30'}"
|
|
112
|
-
ondragover={handleDragOver}
|
|
113
|
-
ondragleave={handleDragLeave}
|
|
114
|
-
ondrop={handleDrop}
|
|
115
|
-
>
|
|
116
|
-
<div class="flex flex-col items-center justify-center pt-5 pb-6 pointer-events-none">
|
|
117
|
-
{#if isDragging}
|
|
118
|
-
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="mb-2 text-primary">
|
|
119
|
-
<path d="M12 15V3"/><path d="M7 10l5 5 5-5"/>
|
|
120
|
-
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/>
|
|
121
|
-
</svg>
|
|
122
|
-
<p class="mb-2 text-sm font-semibold text-primary">Drop to upload</p>
|
|
123
|
-
{:else}
|
|
124
|
-
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="mb-2 text-base-content/40">
|
|
125
|
-
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="17 8 12 3 7 8"/><line x1="12" x2="12" y1="3" y2="15"/>
|
|
126
|
-
</svg>
|
|
127
|
-
<p class="mb-2 text-sm">
|
|
128
|
-
<span class="font-semibold">Click to upload</span> or drag and drop
|
|
129
|
-
</p>
|
|
130
|
-
<p class="text-xs text-base-content/45">
|
|
131
|
-
PNG, JPG or JPEG (max {formatFileSize(maxSize)})
|
|
132
|
-
</p>
|
|
133
|
-
{/if}
|
|
134
|
-
</div>
|
|
135
|
-
<input
|
|
136
|
-
type="file"
|
|
137
|
-
class="hidden"
|
|
138
|
-
accept="image/*"
|
|
139
|
-
{disabled}
|
|
140
|
-
onchange={handleFileInput}
|
|
141
|
-
/>
|
|
142
|
-
</label>
|
|
143
|
-
{/if}
|
|
144
|
-
|
|
145
|
-
{#if error}
|
|
146
|
-
<p class="text-error text-sm mt-1">{error}</p>
|
|
147
|
-
{/if}
|
|
@@ -1,291 +0,0 @@
|
|
|
1
|
-
<script module lang="ts">
|
|
2
|
-
// Declared in the module script so `import type { DisplaySegment } from
|
|
3
|
-
// './InlineEdit.svelte'` resolves (instance-script exports are not module members).
|
|
4
|
-
/** A display segment for formula-aware rendering */
|
|
5
|
-
export interface DisplaySegment {
|
|
6
|
-
type: 'text' | 'formula';
|
|
7
|
-
display: string;
|
|
8
|
-
tooltip?: string;
|
|
9
|
-
}
|
|
10
|
-
</script>
|
|
11
|
-
|
|
12
|
-
<script lang="ts">
|
|
13
|
-
/**
|
|
14
|
-
* InlineEdit Component
|
|
15
|
-
*
|
|
16
|
-
* A reusable inline editing component for text and textarea inputs.
|
|
17
|
-
* Shows display value when not editing, converts to input on click.
|
|
18
|
-
*
|
|
19
|
-
* Features:
|
|
20
|
-
* - Auto-saves on blur or Enter (Enter only for text type)
|
|
21
|
-
* - Cancels on Escape, reverting to original value
|
|
22
|
-
* - Hover state to indicate editability
|
|
23
|
-
* - Auto-focus when editing starts
|
|
24
|
-
* - Disabled state during saves
|
|
25
|
-
* - Optional formula-aware display: segments with type/display/tooltip
|
|
26
|
-
*/
|
|
27
|
-
|
|
28
|
-
interface Props {
|
|
29
|
-
/** Current value */
|
|
30
|
-
value: string;
|
|
31
|
-
/** Callback when value is saved */
|
|
32
|
-
onSave: (newValue: string) => Promise<void> | void;
|
|
33
|
-
/** Input type: 'text' or 'textarea' */
|
|
34
|
-
type?: 'text' | 'textarea';
|
|
35
|
-
/** Placeholder text when empty */
|
|
36
|
-
placeholder?: string;
|
|
37
|
-
/** Disable editing */
|
|
38
|
-
disabled?: boolean;
|
|
39
|
-
/** Additional CSS classes for the container */
|
|
40
|
-
class?: string;
|
|
41
|
-
/** Number of rows for textarea (default: 3) */
|
|
42
|
-
rows?: number;
|
|
43
|
-
/** Show save/cancel buttons instead of auto-save on blur */
|
|
44
|
-
showButtons?: boolean;
|
|
45
|
-
/** Truncate long text with ellipsis (shows full text on hover) */
|
|
46
|
-
truncate?: boolean;
|
|
47
|
-
/** Optional display segments for formula-aware rendering */
|
|
48
|
-
displaySegments?: DisplaySegment[];
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
let {
|
|
52
|
-
value,
|
|
53
|
-
onSave,
|
|
54
|
-
type = 'text',
|
|
55
|
-
placeholder = 'Click to edit...',
|
|
56
|
-
disabled = false,
|
|
57
|
-
class: className = '',
|
|
58
|
-
rows = 3,
|
|
59
|
-
showButtons = false,
|
|
60
|
-
truncate = false,
|
|
61
|
-
displaySegments = []
|
|
62
|
-
}: Props = $props();
|
|
63
|
-
|
|
64
|
-
// --- Formula rendering ---
|
|
65
|
-
let hasFormulas = $derived(displaySegments.length > 0);
|
|
66
|
-
|
|
67
|
-
// Internal state
|
|
68
|
-
let isEditing = $state(false);
|
|
69
|
-
// svelte-ignore state_referenced_locally
|
|
70
|
-
let editValue = $state(value);
|
|
71
|
-
let isSaving = $state(false);
|
|
72
|
-
let inputElement = $state<HTMLInputElement | HTMLTextAreaElement | null>(null);
|
|
73
|
-
|
|
74
|
-
// Sync editValue when value prop changes (only when not editing)
|
|
75
|
-
$effect(() => {
|
|
76
|
-
if (!isEditing) {
|
|
77
|
-
editValue = value;
|
|
78
|
-
}
|
|
79
|
-
});
|
|
80
|
-
|
|
81
|
-
// Start editing
|
|
82
|
-
function startEditing() {
|
|
83
|
-
if (disabled || isSaving) return;
|
|
84
|
-
editValue = value;
|
|
85
|
-
isEditing = true;
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
// Cancel editing
|
|
89
|
-
function cancelEditing() {
|
|
90
|
-
editValue = value;
|
|
91
|
-
isEditing = false;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
// Save the edited value
|
|
95
|
-
async function saveValue() {
|
|
96
|
-
// Don't save if value hasn't changed
|
|
97
|
-
if (editValue === value) {
|
|
98
|
-
isEditing = false;
|
|
99
|
-
return;
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
isSaving = true;
|
|
103
|
-
try {
|
|
104
|
-
await onSave(editValue);
|
|
105
|
-
isEditing = false;
|
|
106
|
-
} catch (err) {
|
|
107
|
-
console.error('Failed to save:', err);
|
|
108
|
-
// Keep editing mode open on error
|
|
109
|
-
} finally {
|
|
110
|
-
isSaving = false;
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
// Handle keydown events
|
|
115
|
-
function handleKeyDown(event: KeyboardEvent) {
|
|
116
|
-
if (event.key === 'Escape') {
|
|
117
|
-
event.preventDefault();
|
|
118
|
-
cancelEditing();
|
|
119
|
-
} else if (event.key === 'Enter') {
|
|
120
|
-
if (type === 'text' || event.metaKey || event.ctrlKey) {
|
|
121
|
-
event.preventDefault();
|
|
122
|
-
saveValue();
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
// Handle blur (auto-save when not using buttons)
|
|
128
|
-
function handleBlur() {
|
|
129
|
-
if (!showButtons) {
|
|
130
|
-
saveValue();
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
// Auto-focus action
|
|
135
|
-
function autofocus(node: HTMLElement) {
|
|
136
|
-
requestAnimationFrame(() => {
|
|
137
|
-
node.focus();
|
|
138
|
-
if (node instanceof HTMLInputElement) {
|
|
139
|
-
node.select();
|
|
140
|
-
}
|
|
141
|
-
});
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
import { fade } from 'svelte/transition';
|
|
145
|
-
|
|
146
|
-
// Auto-resize textarea to fit content
|
|
147
|
-
function autoresize(node: HTMLTextAreaElement) {
|
|
148
|
-
function resize() {
|
|
149
|
-
node.style.height = 'auto';
|
|
150
|
-
node.style.height = node.scrollHeight + 'px';
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
requestAnimationFrame(resize);
|
|
154
|
-
|
|
155
|
-
node.addEventListener('input', resize);
|
|
156
|
-
|
|
157
|
-
return {
|
|
158
|
-
destroy() {
|
|
159
|
-
node.removeEventListener('input', resize);
|
|
160
|
-
}
|
|
161
|
-
};
|
|
162
|
-
}
|
|
163
|
-
</script>
|
|
164
|
-
|
|
165
|
-
{#if isEditing}
|
|
166
|
-
<!-- Edit mode -->
|
|
167
|
-
<div class="inline-edit-container w-full {className}" in:fade={{ duration: 120 }}>
|
|
168
|
-
{#if type === 'textarea'}
|
|
169
|
-
<textarea
|
|
170
|
-
bind:this={inputElement}
|
|
171
|
-
bind:value={editValue}
|
|
172
|
-
{placeholder}
|
|
173
|
-
{rows}
|
|
174
|
-
class="textarea textarea-bordered w-full text-[0.9375rem] resize-none overflow-hidden"
|
|
175
|
-
style="min-height: {rows * 1.5}rem;"
|
|
176
|
-
disabled={isSaving}
|
|
177
|
-
onkeydown={handleKeyDown}
|
|
178
|
-
onblur={handleBlur}
|
|
179
|
-
use:autofocus
|
|
180
|
-
use:autoresize
|
|
181
|
-
></textarea>
|
|
182
|
-
{:else}
|
|
183
|
-
<input
|
|
184
|
-
bind:this={inputElement}
|
|
185
|
-
bind:value={editValue}
|
|
186
|
-
type="text"
|
|
187
|
-
{placeholder}
|
|
188
|
-
class="input input-bordered input-sm w-full"
|
|
189
|
-
disabled={isSaving}
|
|
190
|
-
onkeydown={handleKeyDown}
|
|
191
|
-
onblur={handleBlur}
|
|
192
|
-
use:autofocus
|
|
193
|
-
/>
|
|
194
|
-
{/if}
|
|
195
|
-
|
|
196
|
-
{#if showButtons}
|
|
197
|
-
<div class="flex gap-1 mt-1">
|
|
198
|
-
<button
|
|
199
|
-
class="btn btn-xs btn-success"
|
|
200
|
-
onclick={saveValue}
|
|
201
|
-
disabled={isSaving}
|
|
202
|
-
>
|
|
203
|
-
{#if isSaving}
|
|
204
|
-
<span class="loading loading-spinner loading-xs"></span>
|
|
205
|
-
{:else}
|
|
206
|
-
Save
|
|
207
|
-
{/if}
|
|
208
|
-
</button>
|
|
209
|
-
<button
|
|
210
|
-
class="btn btn-xs btn-ghost"
|
|
211
|
-
onclick={cancelEditing}
|
|
212
|
-
disabled={isSaving}
|
|
213
|
-
>
|
|
214
|
-
Cancel
|
|
215
|
-
</button>
|
|
216
|
-
</div>
|
|
217
|
-
{:else if isSaving}
|
|
218
|
-
<div class="absolute right-2 top-1/2 -translate-y-1/2">
|
|
219
|
-
<span class="loading loading-spinner loading-xs"></span>
|
|
220
|
-
</div>
|
|
221
|
-
{/if}
|
|
222
|
-
</div>
|
|
223
|
-
{:else}
|
|
224
|
-
<!-- Display mode -->
|
|
225
|
-
<button
|
|
226
|
-
class="inline-edit-display text-left w-full rounded px-2 py-1 transition-colors {disabled ? 'cursor-not-allowed opacity-60' : 'cursor-pointer hover:bg-base-200'} {truncate ? 'inline-edit-truncate' : ''} {className}"
|
|
227
|
-
in:fade={{ duration: 120 }}
|
|
228
|
-
onclick={startEditing}
|
|
229
|
-
disabled={disabled}
|
|
230
|
-
type="button"
|
|
231
|
-
title={truncate && value ? value : undefined}
|
|
232
|
-
>
|
|
233
|
-
{#if value}
|
|
234
|
-
{#if hasFormulas}
|
|
235
|
-
<span class={type === 'textarea' ? 'whitespace-pre-wrap' : (truncate ? 'truncate-text' : '')}>
|
|
236
|
-
{#each displaySegments as seg}
|
|
237
|
-
{#if seg.type === 'formula'}
|
|
238
|
-
<span class="inline-fx-result" title={seg.tooltip}>{seg.display}</span>
|
|
239
|
-
{:else}
|
|
240
|
-
{seg.display}
|
|
241
|
-
{/if}
|
|
242
|
-
{/each}
|
|
243
|
-
</span>
|
|
244
|
-
{:else if type === 'textarea'}
|
|
245
|
-
<span class="whitespace-pre-wrap">{value}</span>
|
|
246
|
-
{:else}
|
|
247
|
-
<span class={truncate ? 'truncate-text' : ''}>{value}</span>
|
|
248
|
-
{/if}
|
|
249
|
-
{:else}
|
|
250
|
-
<span class="text-base-content/50 italic">{placeholder}</span>
|
|
251
|
-
{/if}
|
|
252
|
-
</button>
|
|
253
|
-
{/if}
|
|
254
|
-
|
|
255
|
-
<style>
|
|
256
|
-
.inline-edit-container {
|
|
257
|
-
position: relative;
|
|
258
|
-
}
|
|
259
|
-
|
|
260
|
-
.inline-edit-display {
|
|
261
|
-
min-height: 2rem;
|
|
262
|
-
display: flex;
|
|
263
|
-
align-items: center;
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
/* Truncation styles */
|
|
267
|
-
.inline-edit-truncate {
|
|
268
|
-
max-width: 100%;
|
|
269
|
-
overflow: hidden;
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
.truncate-text {
|
|
273
|
-
display: block;
|
|
274
|
-
white-space: nowrap;
|
|
275
|
-
overflow: hidden;
|
|
276
|
-
text-overflow: ellipsis;
|
|
277
|
-
max-width: 100%;
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
.inline-fx-result {
|
|
281
|
-
display: inline;
|
|
282
|
-
padding: 0 4px;
|
|
283
|
-
border-radius: 3px;
|
|
284
|
-
background: oklch(0.25 0.06 80 / 0.4);
|
|
285
|
-
border: 1px solid oklch(0.35 0.08 80 / 0.3);
|
|
286
|
-
color: oklch(0.82 0.10 80);
|
|
287
|
-
font-family: 'JetBrains Mono', ui-monospace, monospace;
|
|
288
|
-
font-size: 0.85em;
|
|
289
|
-
cursor: help;
|
|
290
|
-
}
|
|
291
|
-
</style>
|