@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,348 +0,0 @@
1
- <script lang="ts">
2
- import { fly, fade } from 'svelte/transition';
3
- import { cubicOut } from 'svelte/easing';
4
- import { tick } from 'svelte';
5
- import { getPending, confirmDialog, cancelDialog } from '../stores/confirmDialog.svelte';
6
-
7
- const pending = $derived(getPending());
8
- const opts = $derived(pending?.options ?? null);
9
- const visible = $derived(pending !== null);
10
-
11
- // Countdown state
12
- let remaining = $state(0);
13
- let total = $state(0);
14
- let intervalId: ReturnType<typeof setInterval> | null = null;
15
- let cancelRef = $state<HTMLButtonElement | null>(null);
16
-
17
- $effect(() => {
18
- if (!visible || !opts) {
19
- if (intervalId) { clearInterval(intervalId); intervalId = null; }
20
- remaining = 0;
21
- total = 0;
22
- return;
23
- }
24
-
25
- const ms = opts.timeoutMs ?? 8000;
26
- if (ms > 0) {
27
- total = ms;
28
- remaining = ms;
29
- intervalId = setInterval(() => {
30
- remaining -= 60;
31
- if (remaining <= 0) {
32
- remaining = 0;
33
- clearInterval(intervalId!);
34
- intervalId = null;
35
- cancelDialog();
36
- }
37
- }, 60);
38
- } else {
39
- total = 0;
40
- remaining = 0;
41
- }
42
-
43
- // Focus the cancel button (safe default for destructive actions)
44
- tick().then(() => cancelRef?.focus());
45
-
46
- return () => {
47
- if (intervalId) { clearInterval(intervalId); intervalId = null; }
48
- };
49
- });
50
-
51
- const pct = $derived(total > 0 ? (remaining / total) * 100 : 0);
52
- const secs = $derived(total > 0 ? Math.ceil(remaining / 1000) : 0);
53
- const isDanger = $derived(opts?.danger !== false); // default true
54
-
55
- function handleKeydown(e: KeyboardEvent) {
56
- if (!visible) return;
57
- if (e.key === 'Escape') { e.preventDefault(); e.stopPropagation(); cancelDialog(); }
58
- if (e.key === 'Enter') { e.preventDefault(); e.stopPropagation(); confirmDialog(); }
59
- }
60
-
61
- $effect(() => {
62
- window.addEventListener('keydown', handleKeydown, true);
63
- return () => window.removeEventListener('keydown', handleKeydown, true);
64
- });
65
- </script>
66
-
67
- {#if visible && opts}
68
- <!-- Scrim -->
69
- <div
70
- class="cd-scrim"
71
- transition:fade={{ duration: 120 }}
72
- role="presentation"
73
- onclick={cancelDialog}
74
- ></div>
75
-
76
- <!-- Dialog -->
77
- <div
78
- class="cd-wrap"
79
- role="alertdialog"
80
- aria-modal="true"
81
- aria-label="{opts.title}. {isDanger ? 'Destructive action.' : ''} Press Enter to confirm or Escape to cancel."
82
- in:fly={{ y: -20, duration: 200, easing: cubicOut }}
83
- out:fade={{ duration: 120 }}
84
- >
85
- <div class="cd-box" class:cd-danger={isDanger}>
86
- <!-- Icon + title -->
87
- <div class="cd-header">
88
- <div class="cd-icon" class:cd-icon-danger={isDanger}>
89
- {#if isDanger}
90
- <!-- Terminal/skull icon for destructive actions -->
91
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
92
- <rect x="2" y="3" width="20" height="14" rx="2"/>
93
- <path d="M8 21h8M12 17v4"/>
94
- <path d="M9 8l2 2-2 2M13 12h2"/>
95
- </svg>
96
- {:else}
97
- <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" stroke-linejoin="round">
98
- <circle cx="12" cy="12" r="10"/>
99
- <line x1="12" y1="8" x2="12" y2="12"/>
100
- <line x1="12" y1="16" x2="12.01" y2="16"/>
101
- </svg>
102
- {/if}
103
- </div>
104
- <div class="cd-title-group">
105
- <h3 class="cd-title">{opts.title}</h3>
106
- {#if opts.body}
107
- <p class="cd-body">{opts.body}</p>
108
- {/if}
109
- </div>
110
- </div>
111
-
112
- <!-- Actions -->
113
- <div class="cd-actions">
114
- <button
115
- bind:this={cancelRef}
116
- type="button"
117
- class="cd-btn cd-btn-cancel"
118
- onclick={cancelDialog}
119
- >
120
- <kbd class="cd-kbd">Esc</kbd>
121
- <span>{opts.cancelLabel ?? 'Cancel'}</span>
122
- </button>
123
-
124
- <button
125
- type="button"
126
- class="cd-btn cd-btn-confirm"
127
- class:cd-btn-danger={isDanger}
128
- onclick={confirmDialog}
129
- >
130
- <kbd class="cd-kbd">{isDanger ? '⏎' : 'Enter'}</kbd>
131
- <span>{opts.confirmLabel ?? (isDanger ? 'Confirm' : 'OK')}</span>
132
- </button>
133
-
134
- {#if total > 0}
135
- <span class="cd-timer" aria-live="off">{secs}s</span>
136
- {/if}
137
- </div>
138
-
139
- <!-- Progress bar (drains to 0; auto-cancels at 0) -->
140
- {#if total > 0}
141
- <div class="cd-progress" aria-hidden="true">
142
- <div class="cd-progress-fill" class:cd-progress-danger={isDanger} style="width: {pct}%"></div>
143
- </div>
144
- {/if}
145
- </div>
146
- </div>
147
- {/if}
148
-
149
- <style>
150
- .cd-scrim {
151
- position: fixed;
152
- inset: 0;
153
- background: oklch(0 0 0 / 0.45);
154
- z-index: 9990;
155
- backdrop-filter: blur(2px);
156
- }
157
-
158
- .cd-wrap {
159
- position: fixed;
160
- top: 5rem;
161
- left: 50%;
162
- transform: translateX(-50%);
163
- z-index: 9991;
164
- pointer-events: auto;
165
- width: max-content;
166
- max-width: min(92vw, 460px);
167
- }
168
-
169
- .cd-box {
170
- display: flex;
171
- flex-direction: column;
172
- gap: 0.625rem;
173
- padding: 1rem 1.125rem 0.625rem;
174
- background: color-mix(in oklch, var(--color-base-100) 98%, transparent);
175
- border: 1px solid var(--color-base-300);
176
- border-radius: 0.875rem;
177
- box-shadow:
178
- 0 20px 60px oklch(0 0 0 / 0.55),
179
- 0 0 0 1px color-mix(in oklch, var(--color-base-300) 50%, transparent);
180
- backdrop-filter: blur(16px);
181
- }
182
-
183
- .cd-box.cd-danger {
184
- border-color: color-mix(in oklch, var(--color-error) 55%, transparent);
185
- box-shadow:
186
- 0 20px 60px oklch(0 0 0 / 0.6),
187
- 0 0 0 1px color-mix(in oklch, var(--color-error) 25%, transparent),
188
- 0 0 30px color-mix(in oklch, var(--color-error) 12%, transparent);
189
- }
190
-
191
- .cd-header {
192
- display: flex;
193
- align-items: flex-start;
194
- gap: 0.875rem;
195
- }
196
-
197
- .cd-icon {
198
- flex-shrink: 0;
199
- width: 2.25rem;
200
- height: 2.25rem;
201
- border-radius: 0.5rem;
202
- display: flex;
203
- align-items: center;
204
- justify-content: center;
205
- background: color-mix(in oklch, var(--color-info) 35%, transparent);
206
- border: 1px solid color-mix(in oklch, var(--color-info) 40%, transparent);
207
- color: var(--color-info);
208
- }
209
-
210
- .cd-icon svg {
211
- width: 1.125rem;
212
- height: 1.125rem;
213
- }
214
-
215
- .cd-icon.cd-icon-danger {
216
- background: color-mix(in oklch, var(--color-error) 35%, transparent);
217
- border-color: color-mix(in oklch, var(--color-error) 40%, transparent);
218
- color: var(--color-error-content);
219
- }
220
-
221
- .cd-title-group {
222
- display: flex;
223
- flex-direction: column;
224
- gap: 0.25rem;
225
- flex: 1;
226
- min-width: 0;
227
- }
228
-
229
- .cd-title {
230
- margin: 0;
231
- font-size: 0.9375rem;
232
- font-weight: 600;
233
- color: var(--color-base-content);
234
- line-height: 1.3;
235
- }
236
-
237
- .cd-body {
238
- margin: 0;
239
- font-size: 0.8rem;
240
- color: color-mix(in oklch, var(--color-base-content) 65%, transparent);
241
- line-height: 1.5;
242
- }
243
-
244
- .cd-actions {
245
- display: flex;
246
- align-items: center;
247
- gap: 0.375rem;
248
- padding-top: 0.125rem;
249
- }
250
-
251
- .cd-btn {
252
- display: inline-flex;
253
- align-items: center;
254
- gap: 0.375rem;
255
- padding: 0.3125rem 0.75rem;
256
- font-size: 0.8rem;
257
- font-weight: 500;
258
- border-radius: 0.5rem;
259
- border: 1px solid transparent;
260
- background: transparent;
261
- color: color-mix(in oklch, var(--color-base-content) 85%, transparent);
262
- cursor: pointer;
263
- transition: background 0.1s ease, border-color 0.1s ease, color 0.1s ease;
264
- white-space: nowrap;
265
- }
266
-
267
- .cd-btn:hover {
268
- background: var(--color-base-200);
269
- border-color: var(--color-base-300);
270
- }
271
-
272
- .cd-btn-cancel:focus-visible {
273
- outline: 2px solid var(--color-primary);
274
- outline-offset: 2px;
275
- }
276
-
277
- .cd-btn-confirm {
278
- background: color-mix(in oklch, var(--color-base-300) 60%, transparent);
279
- border-color: var(--color-base-300);
280
- }
281
-
282
- .cd-btn-confirm:hover {
283
- background: var(--color-base-200);
284
- border-color: color-mix(in oklch, var(--color-base-content) 30%, transparent);
285
- }
286
-
287
- .cd-btn-danger {
288
- background: color-mix(in oklch, var(--color-error) 45%, transparent);
289
- border-color: color-mix(in oklch, var(--color-error) 55%, transparent);
290
- color: var(--color-error-content);
291
- }
292
-
293
- .cd-btn-danger:hover {
294
- background: color-mix(in oklch, var(--color-error) 65%, transparent);
295
- border-color: color-mix(in oklch, var(--color-error) 70%, transparent);
296
- color: var(--color-error-content);
297
- }
298
-
299
- .cd-btn-confirm:focus-visible,
300
- .cd-btn-danger:focus-visible {
301
- outline: 2px solid var(--color-error);
302
- outline-offset: 2px;
303
- }
304
-
305
- .cd-kbd {
306
- font-family: ui-monospace, monospace;
307
- font-size: 0.65rem;
308
- padding: 0.0625rem 0.3125rem;
309
- background: var(--color-base-200);
310
- border: 1px solid var(--color-base-300);
311
- border-bottom-width: 2px;
312
- border-radius: 0.25rem;
313
- color: color-mix(in oklch, var(--color-base-content) 70%, transparent);
314
- line-height: 1.6;
315
- }
316
-
317
- .cd-timer {
318
- margin-left: auto;
319
- font-family: ui-monospace, monospace;
320
- font-size: 0.7rem;
321
- color: color-mix(in oklch, var(--color-base-content) 45%, transparent);
322
- font-variant-numeric: tabular-nums;
323
- min-width: 2ch;
324
- text-align: right;
325
- }
326
-
327
- .cd-progress {
328
- height: 2px;
329
- background: var(--color-base-200);
330
- border-radius: 999px;
331
- overflow: hidden;
332
- }
333
-
334
- .cd-progress-fill {
335
- height: 100%;
336
- background: linear-gradient(90deg, var(--color-info), var(--color-info));
337
- border-radius: 999px;
338
- transition: width 60ms linear;
339
- }
340
-
341
- .cd-progress-fill.cd-progress-danger {
342
- background: linear-gradient(90deg, var(--color-error), var(--color-error));
343
- }
344
-
345
- @media (prefers-reduced-motion: reduce) {
346
- .cd-progress-fill { transition: none; }
347
- }
348
- </style>
@@ -1,142 +0,0 @@
1
- <script lang="ts">
2
- import { fly, fade } from 'svelte/transition';
3
- import { cubicOut } from 'svelte/easing';
4
-
5
- let {
6
- open = $bindable(true),
7
- title = "Confirm Action",
8
- message = "Are you sure you want to proceed?",
9
- confirmText = "Confirm",
10
- cancelText = "Cancel",
11
- confirmButtonClass = "btn-primary",
12
- isLoading = false,
13
- iconClass = "text-warning",
14
- onconfirm,
15
- oncancel,
16
- } = $props<{
17
- open?: boolean
18
- title?: string
19
- message?: string
20
- confirmText?: string
21
- cancelText?: string
22
- confirmButtonClass?: string
23
- isLoading?: boolean
24
- iconClass?: string
25
- onconfirm?: () => void
26
- oncancel?: () => void
27
- }>()
28
-
29
- // Respect prefers-reduced-motion: skip the entrance/exit transitions.
30
- const reducedMotion =
31
- typeof window !== 'undefined' &&
32
- window.matchMedia?.('(prefers-reduced-motion: reduce)').matches
33
-
34
- // Focus management: move focus into the dialog on open, trap Tab within it,
35
- // and restore focus to the trigger on close. Escape is handled window-level
36
- // so it works regardless of which control inside the box currently has focus.
37
- let modalBox = $state<HTMLElement | null>(null)
38
- let previousFocus: HTMLElement | null = null
39
-
40
- const FOCUSABLE =
41
- 'button:not([disabled]), [href], input:not([disabled]), select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'
42
-
43
- $effect(() => {
44
- if (typeof document === 'undefined') return
45
- if (open) {
46
- previousFocus = document.activeElement as HTMLElement
47
- requestAnimationFrame(() => {
48
- if (modalBox?.contains(document.activeElement)) return
49
- const first = modalBox?.querySelector<HTMLElement>(FOCUSABLE)
50
- ;(first ?? modalBox)?.focus()
51
- })
52
- } else if (previousFocus) {
53
- previousFocus.focus?.()
54
- previousFocus = null
55
- }
56
- })
57
-
58
- function confirm() {
59
- onconfirm?.()
60
- }
61
-
62
- function cancel() {
63
- open = false
64
- oncancel?.()
65
- }
66
-
67
- function onWindowKeydown(e: KeyboardEvent) {
68
- if (open && e.key === 'Escape') {
69
- e.preventDefault()
70
- e.stopImmediatePropagation()
71
- cancel()
72
- }
73
- }
74
-
75
- function trapTab(e: KeyboardEvent) {
76
- if (e.key !== 'Tab' || !modalBox) return
77
- const f = Array.from(modalBox.querySelectorAll<HTMLElement>(FOCUSABLE))
78
- if (f.length === 0) return
79
- const first = f[0]
80
- const last = f[f.length - 1]
81
- if (e.shiftKey && document.activeElement === first) {
82
- e.preventDefault()
83
- last.focus()
84
- } else if (!e.shiftKey && document.activeElement === last) {
85
- e.preventDefault()
86
- first.focus()
87
- }
88
- }
89
- </script>
90
-
91
- <svelte:window onkeydown={onWindowKeydown} />
92
-
93
- {#if open}
94
- <div class="modal modal-open" transition:fade={{ duration: reducedMotion ? 0 : 200 }}>
95
- <div
96
- bind:this={modalBox}
97
- class="modal-box max-w-md"
98
- role="dialog"
99
- aria-modal="true"
100
- aria-label={title}
101
- tabindex="-1"
102
- onkeydown={trapTab}
103
- in:fly={{ y: reducedMotion ? 0 : 10, duration: reducedMotion ? 0 : 220, easing: cubicOut }}
104
- out:fade={{ duration: reducedMotion ? 0 : 150 }}
105
- >
106
- <div class="flex justify-between items-center mb-4">
107
- <h3 class="font-semibold">{title}</h3>
108
- <button class="btn btn-ghost btn-sm" onclick={cancel} aria-label="Close">
109
- <svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" 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>
110
- </button>
111
- </div>
112
-
113
- <div class="py-4">
114
- <div class="flex justify-center mb-4">
115
- <div class="p-4 rounded-full bg-base-200">
116
- <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={iconClass}><path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3"/><path d="M12 9v4"/><path d="M12 17h.01"/></svg>
117
- </div>
118
- </div>
119
-
120
- <p class="text-center mb-6">{message}</p>
121
-
122
- <div class="flex gap-2">
123
- <button type="button" class="btn btn-outline flex-1" onclick={cancel} disabled={isLoading}>
124
- {cancelText}
125
- </button>
126
- <button type="button" class={`btn ${confirmButtonClass} flex-1`} onclick={confirm} disabled={isLoading}>
127
- {#if isLoading}
128
- <span class="loading loading-spinner loading-xs"></span>
129
- Processing...
130
- {:else}
131
- {confirmText}
132
- {/if}
133
- </button>
134
- </div>
135
- </div>
136
- </div>
137
-
138
- <!-- svelte-ignore a11y_click_events_have_key_events -->
139
- <!-- svelte-ignore a11y_no_static_element_interactions -->
140
- <div class="modal-backdrop" onclick={cancel}></div>
141
- </div>
142
- {/if}
@@ -1,188 +0,0 @@
1
- <script lang="ts">
2
- /**
3
- * ContextMenu Component
4
- *
5
- * Floating right-click (or programmatic) menu positioned at a cursor point.
6
- * Headless primitive that handles positioning, viewport clamping, and
7
- * dismiss behavior (click-outside, Escape, right-click elsewhere).
8
- *
9
- * Viewport clamping is measured from the rendered DOM (offsetWidth /
10
- * offsetHeight) — no estimatedWidth/estimatedHeight props needed. The menu
11
- * stays inside the viewport regardless of how many items are passed in.
12
- * `transform-origin: top left` is set on the menu element so a caller's
13
- * scale-based entrance animation (via `class`) cannot shift the visual
14
- * rect away from the layout-box position used for clamping.
15
- *
16
- * Usage:
17
- * let cm = $state({ open: false, x: 0, y: 0, target: null })
18
- *
19
- * <button oncontextmenu={(e) => {
20
- * e.preventDefault()
21
- * cm = { open: true, x: e.clientX, y: e.clientY, target: row }
22
- * }}>Row</button>
23
- *
24
- * <ContextMenu
25
- * bind:open={cm.open}
26
- * x={cm.x}
27
- * y={cm.y}
28
- * header={cm.target?.name}
29
- * items={[
30
- * { label: "Preview", onSelect: () => preview(cm.target) },
31
- * { label: "Copy URL", onSelect: () => copy(cm.target) },
32
- * { divider: true },
33
- * { label: "Delete", variant: "danger", onSelect: () => del(cm.target) },
34
- * ]}
35
- * />
36
- */
37
-
38
- import { scale, fade } from 'svelte/transition';
39
- import { cubicOut } from 'svelte/easing';
40
-
41
- type ActionItem = {
42
- label: string;
43
- onSelect: () => void;
44
- variant?: 'default' | 'danger';
45
- disabled?: boolean;
46
- };
47
- type DividerItem = { divider: true };
48
- export type ContextMenuItem = ActionItem | DividerItem;
49
-
50
- interface Props {
51
- /** Whether the menu is currently shown. Bindable. */
52
- open: boolean;
53
- /** Viewport x coordinate to anchor the top-left corner to (e.g. event.clientX). */
54
- x: number;
55
- /** Viewport y coordinate to anchor the top-left corner to (e.g. event.clientY). */
56
- y: number;
57
- /** Ordered list of items and dividers to render. */
58
- items: ContextMenuItem[];
59
- /** Optional small italic label rendered above the items (e.g. a filename). */
60
- header?: string;
61
- /** Stacking context z-index. Default 70 (above DaisyUI modals at 50). */
62
- zIndex?: number;
63
- /** Additional CSS class for the menu element. */
64
- class?: string;
65
- /** Called after the menu closes, regardless of cause. */
66
- onClose?: () => void;
67
- }
68
-
69
- let {
70
- open = $bindable(false),
71
- x,
72
- y,
73
- items,
74
- header,
75
- zIndex = 70,
76
- class: className = '',
77
- onClose
78
- }: Props = $props();
79
-
80
- function close() {
81
- if (!open) return;
82
- open = false;
83
- onClose?.();
84
- }
85
-
86
- function handleSelect(item: ActionItem) {
87
- if (item.disabled) return;
88
- item.onSelect();
89
- close();
90
- }
91
-
92
- // Position is measured from the rendered menu so it stays correct as items
93
- // are added/removed. Initial values match the cursor; the post-render
94
- // effect below corrects them once the layout box is known.
95
- let menuEl = $state<HTMLDivElement | null>(null);
96
- // svelte-ignore state_referenced_locally
97
- let clampedX = $state(x);
98
- // svelte-ignore state_referenced_locally
99
- let clampedY = $state(y);
100
-
101
- function measureAndClamp() {
102
- if (typeof window === 'undefined' || !menuEl) return;
103
- // offset* gives the layout box, ignoring any transform: scale() the
104
- // caller might apply via a className. getBoundingClientRect would
105
- // return the post-transform rect and underestimate during entrance
106
- // animations.
107
- const w = menuEl.offsetWidth;
108
- const h = menuEl.offsetHeight;
109
- const margin = 8;
110
- clampedX =
111
- x + w > window.innerWidth - margin
112
- ? Math.max(margin, window.innerWidth - w - margin)
113
- : Math.max(margin, x);
114
- clampedY =
115
- y + h > window.innerHeight - margin
116
- ? Math.max(margin, window.innerHeight - h - margin)
117
- : Math.max(margin, y);
118
- }
119
-
120
- // Re-measure whenever the menu opens, the anchor moves, or the item list
121
- // changes (which can change the menu height).
122
- $effect(() => {
123
- if (!open) return;
124
- // Touch reactive deps so the effect re-runs.
125
- void x; void y; void items.length;
126
- // Wait for the menu element to mount before measuring.
127
- if (menuEl) {
128
- measureAndClamp();
129
- } else {
130
- // Element not mounted yet — fall back to cursor position; the next
131
- // effect tick will fire once `bind:this` resolves.
132
- clampedX = x;
133
- clampedY = y;
134
- }
135
- });
136
- </script>
137
-
138
- {#if open}
139
- <!-- Invisible full-viewport overlay — captures click/right-click to dismiss -->
140
- <!-- svelte-ignore a11y_click_events_have_key_events -->
141
- <!-- svelte-ignore a11y_no_static_element_interactions -->
142
- <div
143
- class="fixed inset-0"
144
- style="z-index: {zIndex - 1};"
145
- onclick={close}
146
- oncontextmenu={(e) => {
147
- e.preventDefault();
148
- close();
149
- }}
150
- ></div>
151
-
152
- <div
153
- bind:this={menuEl}
154
- role="menu"
155
- tabindex="-1"
156
- class="fixed min-w-[200px] bg-base-200 border border-base-300 rounded-md py-1 {className}"
157
- style="left: {clampedX}px; top: {clampedY}px; z-index: {zIndex}; transform-origin: top left;"
158
- in:scale={{ start: 0.92, duration: 140, easing: cubicOut }}
159
- out:fade={{ duration: 100 }}
160
- onkeydown={(e) => e.key === 'Escape' && close()}
161
- >
162
- {#if header}
163
- <p
164
- class="px-3 py-1.5 text-[11px] italic text-base-content/60 truncate border-b border-base-300"
165
- >
166
- {header}
167
- </p>
168
- {/if}
169
-
170
- {#each items as item}
171
- {#if 'divider' in item}
172
- <div class="border-t border-base-300 my-1"></div>
173
- {:else}
174
- <button
175
- type="button"
176
- role="menuitem"
177
- class="w-full text-left px-3 py-1.5 text-sm transition-colors
178
- {item.variant === 'danger' ? 'text-error hover:bg-error/10' : 'hover:bg-base-200'}
179
- {item.disabled ? 'opacity-50 cursor-not-allowed' : ''}"
180
- disabled={item.disabled}
181
- onclick={() => handleSelect(item)}
182
- >
183
- {item.label}
184
- </button>
185
- {/if}
186
- {/each}
187
- </div>
188
- {/if}