@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.
Files changed (91) hide show
  1. package/README.md +123 -0
  2. package/package.json +3 -1
  3. package/src/lib/actions/railNav.ts +473 -0
  4. package/src/lib/components/AnnotationLayer.svelte +108 -0
  5. package/src/lib/components/AnnotationPanel.svelte +319 -0
  6. package/src/lib/components/AudioWaveform.svelte +9 -5
  7. package/src/lib/components/AvailabilityModal.svelte +7 -3
  8. package/src/lib/components/AvatarUpload.svelte +27 -4
  9. package/src/lib/components/BookingForm.svelte +11 -9
  10. package/src/lib/components/BurndownChart.svelte +778 -0
  11. package/src/lib/components/Button.svelte +10 -1
  12. package/src/lib/components/CalendarPicker.svelte +3 -3
  13. package/src/lib/components/Card.svelte +2 -2
  14. package/src/lib/components/ChipInput.svelte +21 -15
  15. package/src/lib/components/ColorSelector.svelte +17 -13
  16. package/src/lib/components/CommentThread.svelte +773 -0
  17. package/src/lib/components/ConfirmDialog.svelte +348 -0
  18. package/src/lib/components/ConfirmModal.svelte +78 -11
  19. package/src/lib/components/ContextMenu.svelte +188 -0
  20. package/src/lib/components/CountdownTimer.svelte +1 -1
  21. package/src/lib/components/DateRangePicker.svelte +6 -4
  22. package/src/lib/components/Drawer.svelte +36 -3
  23. package/src/lib/components/EntityPreviewCard.svelte +104 -0
  24. package/src/lib/components/FileDropzone.svelte +493 -0
  25. package/src/lib/components/FilePicker.svelte +83 -14
  26. package/src/lib/components/FileThumbnail.svelte +80 -0
  27. package/src/lib/components/FilterDropdown.svelte +11 -11
  28. package/src/lib/components/HunkDiffView.svelte +348 -0
  29. package/src/lib/components/ImageLightbox.svelte +274 -0
  30. package/src/lib/components/ImageUpload.svelte +58 -9
  31. package/src/lib/components/InlineEdit.svelte +15 -9
  32. package/src/lib/components/InputDialog.svelte +327 -0
  33. package/src/lib/components/LazyImage.svelte +1 -0
  34. package/src/lib/components/LinkShortener.svelte +1 -1
  35. package/src/lib/components/LoadingSpinner.svelte +6 -2
  36. package/src/lib/components/MarkupEditor.svelte +485 -0
  37. package/src/lib/components/MarkupOverlay.svelte +55 -0
  38. package/src/lib/components/MediaWorkbench.svelte +871 -0
  39. package/src/lib/components/MilestoneCard.svelte +1 -1
  40. package/src/lib/components/MilestoneTimeline.svelte +1 -1
  41. package/src/lib/components/Modal.svelte +39 -4
  42. package/src/lib/components/PDFViewer.svelte +105 -0
  43. package/src/lib/components/PdfThumbnail.svelte +3 -1
  44. package/src/lib/components/PhoneInput.svelte +183 -63
  45. package/src/lib/components/ResizablePanel.svelte +4 -4
  46. package/src/lib/components/SearchDropdown.svelte +26 -13
  47. package/src/lib/components/SelectInput.svelte +26 -4
  48. package/src/lib/components/SidebarUserFooter.svelte +1 -1
  49. package/src/lib/components/SignaturePad.svelte +8 -4
  50. package/src/lib/components/SmartImageEditor.svelte +720 -0
  51. package/src/lib/components/SortDropdown.svelte +9 -3
  52. package/src/lib/components/Sparkline.svelte +9 -0
  53. package/src/lib/components/StatusBadge.svelte +20 -18
  54. package/src/lib/components/TextArea.svelte +24 -5
  55. package/src/lib/components/TextInput.svelte +29 -6
  56. package/src/lib/components/ThemeSelector.svelte +15 -4
  57. package/src/lib/components/TimeSlotPicker.svelte +7 -7
  58. package/src/lib/components/UserAvatar.svelte +14 -1
  59. package/src/lib/components/VariablePicker.svelte +170 -0
  60. package/src/lib/components/VoicePlayer.svelte +4 -3
  61. package/src/lib/components/markup.ts +287 -0
  62. package/src/lib/components/messaging/ChannelInfoModal.svelte +9 -9
  63. package/src/lib/components/messaging/ChannelList.svelte +1 -1
  64. package/src/lib/components/messaging/ChannelMembersModal.svelte +1 -1
  65. package/src/lib/components/messaging/CreateChannelModal.svelte +1 -1
  66. package/src/lib/components/messaging/DirectMessageList.svelte +1 -1
  67. package/src/lib/components/messaging/EmojiSelector.svelte +2 -1
  68. package/src/lib/components/messaging/MentionAutocomplete.svelte +1 -1
  69. package/src/lib/components/messaging/MessageAttachment.svelte +3 -3
  70. package/src/lib/components/messaging/MessageAttachmentUpload.svelte +3 -3
  71. package/src/lib/components/messaging/MessageInput.svelte +1 -1
  72. package/src/lib/components/messaging/MessageItem.svelte +6 -3
  73. package/src/lib/components/messaging/NotificationSettingsModal.svelte +1 -1
  74. package/src/lib/components/messaging/QuotedMessageDisplay.svelte +6 -1
  75. package/src/lib/components/messaging/StartDMModal.svelte +1 -1
  76. package/src/lib/components/pipeline/Pipeline.svelte +4 -4
  77. package/src/lib/components/pipeline/PipelineCard.svelte +1 -1
  78. package/src/lib/components/pipeline/PipelineColumn.svelte +8 -3
  79. package/src/lib/index.ts +105 -1
  80. package/src/lib/stores/confirmDialog.svelte.ts +48 -0
  81. package/src/lib/stores/inputDialog.svelte.ts +51 -0
  82. package/src/lib/styles/rail.css +63 -0
  83. package/src/lib/types/annotation.ts +38 -0
  84. package/src/lib/types/comments.ts +97 -0
  85. package/src/lib/types/entityPreview.ts +45 -0
  86. package/src/lib/types/filePicker.ts +2 -0
  87. package/src/lib/types/smartImageEditor.ts +39 -0
  88. package/src/lib/types/templateVars.ts +36 -0
  89. package/src/lib/utils/dateFormatters.ts +12 -10
  90. package/src/lib/utils/phone.ts +80 -0
  91. package/src/lib/utils/taskUtils.ts +21 -7
@@ -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}
@@ -14,6 +14,38 @@
14
14
  }>()
15
15
 
16
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
+ }
17
49
 
18
50
  async function handleFileInput(event: Event) {
19
51
  error = ""
@@ -71,17 +103,34 @@
71
103
  </button>
72
104
  </div>
73
105
  {:else}
106
+ <!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
74
107
  <label
75
- class="flex flex-col items-center justify-center w-full h-32 border-2 border-dashed rounded-md cursor-pointer bg-base-200 hover:bg-base-300"
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}
76
115
  >
77
- <div class="flex flex-col items-center justify-center pt-5 pb-6">
78
- <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"><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"/></svg>
79
- <p class="mb-2 text-sm">
80
- <span class="font-semibold">Click to upload</span> or drag and drop
81
- </p>
82
- <p class="text-xs opacity-70">
83
- PNG, JPG or JPEG (max {formatFileSize(maxSize)})
84
- </p>
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}
85
134
  </div>
86
135
  <input
87
136
  type="file"
@@ -1,3 +1,12 @@
1
+ <script module lang="ts">
2
+ /** A display segment for formula-aware rendering */
3
+ export interface DisplaySegment {
4
+ type: 'text' | 'formula';
5
+ display: string;
6
+ tooltip?: string;
7
+ }
8
+ </script>
9
+
1
10
  <script lang="ts">
2
11
  /**
3
12
  * InlineEdit Component
@@ -14,13 +23,6 @@
14
23
  * - Optional formula-aware display: segments with type/display/tooltip
15
24
  */
16
25
 
17
- /** A display segment for formula-aware rendering */
18
- export interface DisplaySegment {
19
- type: 'text' | 'formula';
20
- display: string;
21
- tooltip?: string;
22
- }
23
-
24
26
  interface Props {
25
27
  /** Current value */
26
28
  value: string;
@@ -62,6 +64,7 @@
62
64
 
63
65
  // Internal state
64
66
  let isEditing = $state(false);
67
+ // svelte-ignore state_referenced_locally
65
68
  let editValue = $state(value);
66
69
  let isSaving = $state(false);
67
70
  let inputElement = $state<HTMLInputElement | HTMLTextAreaElement | null>(null);
@@ -136,6 +139,8 @@
136
139
  });
137
140
  }
138
141
 
142
+ import { fade } from 'svelte/transition';
143
+
139
144
  // Auto-resize textarea to fit content
140
145
  function autoresize(node: HTMLTextAreaElement) {
141
146
  function resize() {
@@ -157,14 +162,14 @@
157
162
 
158
163
  {#if isEditing}
159
164
  <!-- Edit mode -->
160
- <div class="inline-edit-container w-full {className}">
165
+ <div class="inline-edit-container w-full {className}" in:fade={{ duration: 120 }}>
161
166
  {#if type === 'textarea'}
162
167
  <textarea
163
168
  bind:this={inputElement}
164
169
  bind:value={editValue}
165
170
  {placeholder}
166
171
  {rows}
167
- class="textarea textarea-bordered w-full text-sm resize-none overflow-hidden"
172
+ class="textarea textarea-bordered w-full text-[0.9375rem] resize-none overflow-hidden"
168
173
  style="min-height: {rows * 1.5}rem;"
169
174
  disabled={isSaving}
170
175
  onkeydown={handleKeyDown}
@@ -217,6 +222,7 @@
217
222
  <!-- Display mode -->
218
223
  <button
219
224
  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}"
225
+ in:fade={{ duration: 120 }}
220
226
  onclick={startEditing}
221
227
  disabled={disabled}
222
228
  type="button"