@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.
Files changed (124) hide show
  1. package/CLAUDE.md +244 -0
  2. package/package.json +23 -10
  3. package/src/lib/actions/collapse.ts +0 -136
  4. package/src/lib/actions/railNav.ts +0 -473
  5. package/src/lib/components/AnnotationLayer.svelte +0 -108
  6. package/src/lib/components/AnnotationPanel.svelte +0 -319
  7. package/src/lib/components/AudioWaveform.svelte +0 -698
  8. package/src/lib/components/AvailabilityModal.svelte +0 -177
  9. package/src/lib/components/AvatarUpload.svelte +0 -234
  10. package/src/lib/components/Badge.svelte +0 -38
  11. package/src/lib/components/BookingForm.svelte +0 -278
  12. package/src/lib/components/BurndownChart.svelte +0 -778
  13. package/src/lib/components/Button.svelte +0 -81
  14. package/src/lib/components/CalendarPicker.svelte +0 -284
  15. package/src/lib/components/Card.svelte +0 -67
  16. package/src/lib/components/CharacterCounter.svelte +0 -82
  17. package/src/lib/components/ChipInput.svelte +0 -609
  18. package/src/lib/components/Collapse.svelte +0 -82
  19. package/src/lib/components/ColorSelector.svelte +0 -167
  20. package/src/lib/components/CommentThread.svelte +0 -773
  21. package/src/lib/components/ConfirmDialog.svelte +0 -348
  22. package/src/lib/components/ConfirmModal.svelte +0 -142
  23. package/src/lib/components/ContextMenu.svelte +0 -188
  24. package/src/lib/components/CountdownTimer.svelte +0 -94
  25. package/src/lib/components/DateRangePicker.svelte +0 -194
  26. package/src/lib/components/Drawer.svelte +0 -143
  27. package/src/lib/components/EmojiPicker.svelte +0 -159
  28. package/src/lib/components/EntityPreviewCard.svelte +0 -104
  29. package/src/lib/components/FileDropzone.svelte +0 -493
  30. package/src/lib/components/FilePicker.svelte +0 -488
  31. package/src/lib/components/FileThumbnail.svelte +0 -80
  32. package/src/lib/components/FilterDropdown.svelte +0 -203
  33. package/src/lib/components/FloatingActionBar.svelte +0 -354
  34. package/src/lib/components/GPSTracker.svelte +0 -202
  35. package/src/lib/components/HunkDiffView.svelte +0 -348
  36. package/src/lib/components/ImageLightbox.svelte +0 -274
  37. package/src/lib/components/ImageUpload.svelte +0 -147
  38. package/src/lib/components/InlineEdit.svelte +0 -291
  39. package/src/lib/components/InputDialog.svelte +0 -327
  40. package/src/lib/components/KeyboardShortcutsOverlay.svelte +0 -296
  41. package/src/lib/components/LazyImage.svelte +0 -123
  42. package/src/lib/components/LinkShortener.svelte +0 -274
  43. package/src/lib/components/LoadingSpinner.svelte +0 -106
  44. package/src/lib/components/LocationMap.svelte +0 -186
  45. package/src/lib/components/MapView.svelte +0 -341
  46. package/src/lib/components/MarkupEditor.svelte +0 -485
  47. package/src/lib/components/MarkupOverlay.svelte +0 -55
  48. package/src/lib/components/MediaWorkbench.svelte +0 -871
  49. package/src/lib/components/MilestoneCard.svelte +0 -98
  50. package/src/lib/components/MilestoneTimeline.svelte +0 -91
  51. package/src/lib/components/Modal.svelte +0 -244
  52. package/src/lib/components/PDFViewer.svelte +0 -105
  53. package/src/lib/components/PdfThumbnail.svelte +0 -63
  54. package/src/lib/components/PhoneInput.svelte +0 -212
  55. package/src/lib/components/ResizableDivider.svelte +0 -307
  56. package/src/lib/components/ResizablePanel.svelte +0 -305
  57. package/src/lib/components/SearchDropdown.svelte +0 -421
  58. package/src/lib/components/SelectInput.svelte +0 -237
  59. package/src/lib/components/SidebarUserFooter.svelte +0 -34
  60. package/src/lib/components/SignaturePad.svelte +0 -175
  61. package/src/lib/components/SmartImageEditor.svelte +0 -720
  62. package/src/lib/components/SortDropdown.svelte +0 -156
  63. package/src/lib/components/Sparkline.svelte +0 -116
  64. package/src/lib/components/SpeechForm.svelte +0 -114
  65. package/src/lib/components/StatusBadge.svelte +0 -157
  66. package/src/lib/components/TaskTypeIcon.svelte +0 -54
  67. package/src/lib/components/TextArea.svelte +0 -162
  68. package/src/lib/components/TextInput.svelte +0 -131
  69. package/src/lib/components/ThemeSelector.svelte +0 -206
  70. package/src/lib/components/TimeSlotPicker.svelte +0 -162
  71. package/src/lib/components/UserAvatar.svelte +0 -72
  72. package/src/lib/components/VariablePicker.svelte +0 -170
  73. package/src/lib/components/VoicePlayer.svelte +0 -422
  74. package/src/lib/components/linked-columns/LinkedColumns.svelte +0 -526
  75. package/src/lib/components/markup.ts +0 -287
  76. package/src/lib/components/messaging/Avatar.svelte +0 -81
  77. package/src/lib/components/messaging/ChannelInfoModal.svelte +0 -163
  78. package/src/lib/components/messaging/ChannelList.svelte +0 -107
  79. package/src/lib/components/messaging/ChannelMemberAvatarStack.svelte +0 -69
  80. package/src/lib/components/messaging/ChannelMembersModal.svelte +0 -182
  81. package/src/lib/components/messaging/CreateChannelModal.svelte +0 -190
  82. package/src/lib/components/messaging/DirectMessageList.svelte +0 -145
  83. package/src/lib/components/messaging/EmojiSelector.svelte +0 -261
  84. package/src/lib/components/messaging/MentionAutocomplete.svelte +0 -193
  85. package/src/lib/components/messaging/MessageAttachment.svelte +0 -273
  86. package/src/lib/components/messaging/MessageAttachmentUpload.svelte +0 -243
  87. package/src/lib/components/messaging/MessageInput.svelte +0 -452
  88. package/src/lib/components/messaging/MessageItem.svelte +0 -341
  89. package/src/lib/components/messaging/MessageThread.svelte +0 -306
  90. package/src/lib/components/messaging/NotificationSettingsModal.svelte +0 -234
  91. package/src/lib/components/messaging/QuotedMessageDisplay.svelte +0 -123
  92. package/src/lib/components/messaging/StartDMModal.svelte +0 -100
  93. package/src/lib/components/messaging/ThreadPanel.svelte +0 -153
  94. package/src/lib/components/pipeline/DESIGN.md +0 -361
  95. package/src/lib/components/pipeline/Pipeline.svelte +0 -391
  96. package/src/lib/components/pipeline/PipelineCard.svelte +0 -94
  97. package/src/lib/components/pipeline/PipelineColumn.svelte +0 -163
  98. package/src/lib/components/replay/ChapterTimeline.svelte +0 -326
  99. package/src/lib/components/session-nav/transcriptModel.ts +0 -352
  100. package/src/lib/data/emojis.ts +0 -1208
  101. package/src/lib/index.ts +0 -405
  102. package/src/lib/stores/confirmDialog.svelte.ts +0 -48
  103. package/src/lib/stores/inputDialog.svelte.ts +0 -51
  104. package/src/lib/styles/rail.css +0 -63
  105. package/src/lib/types/annotation.ts +0 -38
  106. package/src/lib/types/booking.ts +0 -143
  107. package/src/lib/types/comments.ts +0 -97
  108. package/src/lib/types/entityPreview.ts +0 -45
  109. package/src/lib/types/filePicker.ts +0 -16
  110. package/src/lib/types/googleMaps.d.ts +0 -51
  111. package/src/lib/types/maps.ts +0 -43
  112. package/src/lib/types/messaging.ts +0 -459
  113. package/src/lib/types/milestone.ts +0 -22
  114. package/src/lib/types/pipeline.ts +0 -150
  115. package/src/lib/types/smartImageEditor.ts +0 -39
  116. package/src/lib/types/templateVars.ts +0 -36
  117. package/src/lib/utils/currency.ts +0 -20
  118. package/src/lib/utils/daisyuiColors.ts +0 -243
  119. package/src/lib/utils/dateFormatters.ts +0 -196
  120. package/src/lib/utils/googleMapsLoader.ts +0 -84
  121. package/src/lib/utils/mentionParser.ts +0 -188
  122. package/src/lib/utils/phone.ts +0 -80
  123. package/src/lib/utils/phoneFormat.ts +0 -74
  124. 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>