@joewinke/jatui 0.1.11 → 0.1.20
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +123 -0
- package/package.json +2 -1
- package/src/lib/actions/railNav.ts +473 -0
- package/src/lib/components/AnnotationLayer.svelte +108 -0
- package/src/lib/components/AnnotationPanel.svelte +319 -0
- package/src/lib/components/AudioWaveform.svelte +9 -5
- package/src/lib/components/AvailabilityModal.svelte +7 -3
- package/src/lib/components/AvatarUpload.svelte +27 -4
- package/src/lib/components/BookingForm.svelte +11 -9
- package/src/lib/components/BurndownChart.svelte +778 -0
- package/src/lib/components/Button.svelte +10 -1
- package/src/lib/components/CalendarPicker.svelte +3 -3
- package/src/lib/components/Card.svelte +2 -2
- package/src/lib/components/ChipInput.svelte +8 -3
- package/src/lib/components/ColorSelector.svelte +17 -13
- package/src/lib/components/CommentThread.svelte +773 -0
- package/src/lib/components/ConfirmDialog.svelte +348 -0
- package/src/lib/components/ConfirmModal.svelte +78 -11
- package/src/lib/components/ContextMenu.svelte +59 -19
- package/src/lib/components/CountdownTimer.svelte +1 -1
- package/src/lib/components/DateRangePicker.svelte +6 -4
- package/src/lib/components/Drawer.svelte +36 -3
- package/src/lib/components/EntityPreviewCard.svelte +104 -0
- package/src/lib/components/FileDropzone.svelte +493 -0
- package/src/lib/components/FilePicker.svelte +83 -14
- package/src/lib/components/FileThumbnail.svelte +80 -0
- package/src/lib/components/FilterDropdown.svelte +11 -11
- package/src/lib/components/GPSTracker.svelte +202 -0
- package/src/lib/components/HunkDiffView.svelte +348 -0
- package/src/lib/components/ImageLightbox.svelte +274 -0
- package/src/lib/components/ImageUpload.svelte +58 -9
- package/src/lib/components/InlineEdit.svelte +6 -2
- package/src/lib/components/InputDialog.svelte +327 -0
- package/src/lib/components/KeyboardShortcutsOverlay.svelte +296 -0
- package/src/lib/components/LazyImage.svelte +1 -0
- package/src/lib/components/LinkShortener.svelte +1 -1
- package/src/lib/components/LoadingSpinner.svelte +6 -2
- package/src/lib/components/LocationMap.svelte +186 -0
- package/src/lib/components/MapView.svelte +341 -0
- package/src/lib/components/MarkupEditor.svelte +485 -0
- package/src/lib/components/MarkupOverlay.svelte +55 -0
- package/src/lib/components/MediaWorkbench.svelte +871 -0
- package/src/lib/components/MilestoneCard.svelte +1 -1
- package/src/lib/components/MilestoneTimeline.svelte +1 -1
- package/src/lib/components/Modal.svelte +39 -4
- package/src/lib/components/PDFViewer.svelte +105 -0
- package/src/lib/components/PdfThumbnail.svelte +3 -1
- package/src/lib/components/PhoneInput.svelte +1 -1
- package/src/lib/components/ResizablePanel.svelte +4 -4
- package/src/lib/components/SearchDropdown.svelte +26 -13
- package/src/lib/components/SelectInput.svelte +26 -4
- package/src/lib/components/SidebarUserFooter.svelte +1 -1
- package/src/lib/components/SignaturePad.svelte +8 -4
- package/src/lib/components/SmartImageEditor.svelte +720 -0
- package/src/lib/components/SortDropdown.svelte +9 -3
- package/src/lib/components/Sparkline.svelte +9 -0
- package/src/lib/components/StatusBadge.svelte +20 -18
- package/src/lib/components/TextArea.svelte +24 -5
- package/src/lib/components/TextInput.svelte +29 -6
- package/src/lib/components/ThemeSelector.svelte +15 -4
- package/src/lib/components/TimeSlotPicker.svelte +7 -7
- package/src/lib/components/UserAvatar.svelte +14 -1
- package/src/lib/components/VariablePicker.svelte +170 -0
- package/src/lib/components/VoicePlayer.svelte +4 -3
- package/src/lib/components/linked-columns/LinkedColumns.svelte +520 -0
- package/src/lib/components/markup.ts +287 -0
- package/src/lib/components/messaging/ChannelInfoModal.svelte +9 -9
- package/src/lib/components/messaging/ChannelList.svelte +1 -1
- package/src/lib/components/messaging/ChannelMembersModal.svelte +1 -1
- package/src/lib/components/messaging/CreateChannelModal.svelte +1 -1
- package/src/lib/components/messaging/DirectMessageList.svelte +1 -1
- package/src/lib/components/messaging/EmojiSelector.svelte +2 -1
- package/src/lib/components/messaging/MentionAutocomplete.svelte +1 -1
- package/src/lib/components/messaging/MessageAttachment.svelte +3 -3
- package/src/lib/components/messaging/MessageAttachmentUpload.svelte +3 -3
- package/src/lib/components/messaging/MessageInput.svelte +1 -1
- package/src/lib/components/messaging/MessageItem.svelte +6 -3
- package/src/lib/components/messaging/NotificationSettingsModal.svelte +1 -1
- package/src/lib/components/messaging/QuotedMessageDisplay.svelte +6 -1
- package/src/lib/components/messaging/StartDMModal.svelte +1 -1
- package/src/lib/components/pipeline/Pipeline.svelte +4 -4
- package/src/lib/components/pipeline/PipelineCard.svelte +1 -1
- package/src/lib/components/pipeline/PipelineColumn.svelte +8 -3
- package/src/lib/components/replay/ChapterTimeline.svelte +326 -0
- package/src/lib/components/session-nav/transcriptModel.ts +352 -0
- package/src/lib/index.ts +138 -0
- package/src/lib/stores/confirmDialog.svelte.ts +48 -0
- package/src/lib/stores/inputDialog.svelte.ts +51 -0
- package/src/lib/styles/rail.css +63 -0
- package/src/lib/types/annotation.ts +38 -0
- package/src/lib/types/comments.ts +97 -0
- package/src/lib/types/entityPreview.ts +45 -0
- package/src/lib/types/filePicker.ts +2 -0
- package/src/lib/types/googleMaps.d.ts +51 -0
- package/src/lib/types/maps.ts +43 -0
- package/src/lib/types/smartImageEditor.ts +39 -0
- package/src/lib/types/templateVars.ts +36 -0
- package/src/lib/utils/dateFormatters.ts +12 -10
- package/src/lib/utils/googleMapsLoader.ts +84 -0
- package/src/lib/utils/taskUtils.ts +21 -7
|
@@ -0,0 +1,348 @@
|
|
|
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,5 +1,9 @@
|
|
|
1
1
|
<script lang="ts">
|
|
2
|
+
import { fly, fade } from 'svelte/transition';
|
|
3
|
+
import { cubicOut } from 'svelte/easing';
|
|
4
|
+
|
|
2
5
|
let {
|
|
6
|
+
open = $bindable(true),
|
|
3
7
|
title = "Confirm Action",
|
|
4
8
|
message = "Are you sure you want to proceed?",
|
|
5
9
|
confirmText = "Confirm",
|
|
@@ -10,6 +14,7 @@
|
|
|
10
14
|
onconfirm,
|
|
11
15
|
oncancel,
|
|
12
16
|
} = $props<{
|
|
17
|
+
open?: boolean
|
|
13
18
|
title?: string
|
|
14
19
|
message?: string
|
|
15
20
|
confirmText?: string
|
|
@@ -21,19 +26,85 @@
|
|
|
21
26
|
oncancel?: () => void
|
|
22
27
|
}>()
|
|
23
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
|
+
|
|
24
58
|
function confirm() {
|
|
25
59
|
onconfirm?.()
|
|
26
60
|
}
|
|
27
61
|
|
|
28
62
|
function cancel() {
|
|
63
|
+
open = false
|
|
29
64
|
oncancel?.()
|
|
30
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
|
+
}
|
|
31
89
|
</script>
|
|
32
90
|
|
|
33
|
-
<
|
|
34
|
-
|
|
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
|
+
>
|
|
35
106
|
<div class="flex justify-between items-center mb-4">
|
|
36
|
-
<h3 class="font-
|
|
107
|
+
<h3 class="font-semibold">{title}</h3>
|
|
37
108
|
<button class="btn btn-ghost btn-sm" onclick={cancel} aria-label="Close">
|
|
38
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>
|
|
39
110
|
</button>
|
|
@@ -64,12 +135,8 @@
|
|
|
64
135
|
</div>
|
|
65
136
|
</div>
|
|
66
137
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
onkeydown={(e) => { if (e.key === 'Escape') cancel() }}
|
|
71
|
-
role="button"
|
|
72
|
-
tabindex="0"
|
|
73
|
-
aria-label="Close modal"
|
|
74
|
-
></div>
|
|
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>
|
|
75
141
|
</div>
|
|
142
|
+
{/if}
|
|
@@ -6,6 +6,13 @@
|
|
|
6
6
|
* Headless primitive that handles positioning, viewport clamping, and
|
|
7
7
|
* dismiss behavior (click-outside, Escape, right-click elsewhere).
|
|
8
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
|
+
*
|
|
9
16
|
* Usage:
|
|
10
17
|
* let cm = $state({ open: false, x: 0, y: 0, target: null })
|
|
11
18
|
*
|
|
@@ -28,6 +35,9 @@
|
|
|
28
35
|
* />
|
|
29
36
|
*/
|
|
30
37
|
|
|
38
|
+
import { scale, fade } from 'svelte/transition';
|
|
39
|
+
import { cubicOut } from 'svelte/easing';
|
|
40
|
+
|
|
31
41
|
type ActionItem = {
|
|
32
42
|
label: string;
|
|
33
43
|
onSelect: () => void;
|
|
@@ -48,10 +58,6 @@
|
|
|
48
58
|
items: ContextMenuItem[];
|
|
49
59
|
/** Optional small italic label rendered above the items (e.g. a filename). */
|
|
50
60
|
header?: string;
|
|
51
|
-
/** Estimated menu width in px, used for viewport clamping. Default 200. */
|
|
52
|
-
estimatedWidth?: number;
|
|
53
|
-
/** Estimated menu height in px, used for viewport clamping. Default 200. */
|
|
54
|
-
estimatedHeight?: number;
|
|
55
61
|
/** Stacking context z-index. Default 70 (above DaisyUI modals at 50). */
|
|
56
62
|
zIndex?: number;
|
|
57
63
|
/** Additional CSS class for the menu element. */
|
|
@@ -66,8 +72,6 @@
|
|
|
66
72
|
y,
|
|
67
73
|
items,
|
|
68
74
|
header,
|
|
69
|
-
estimatedWidth = 200,
|
|
70
|
-
estimatedHeight = 200,
|
|
71
75
|
zIndex = 70,
|
|
72
76
|
class: className = '',
|
|
73
77
|
onClose
|
|
@@ -85,17 +89,50 @@
|
|
|
85
89
|
close();
|
|
86
90
|
}
|
|
87
91
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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);
|
|
91
118
|
}
|
|
92
119
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
)
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
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
|
+
});
|
|
99
136
|
</script>
|
|
100
137
|
|
|
101
138
|
{#if open}
|
|
@@ -113,15 +150,18 @@
|
|
|
113
150
|
></div>
|
|
114
151
|
|
|
115
152
|
<div
|
|
153
|
+
bind:this={menuEl}
|
|
116
154
|
role="menu"
|
|
117
155
|
tabindex="-1"
|
|
118
|
-
class="fixed min-w-[200px] bg-base-
|
|
119
|
-
style="left: {clampedX}px; top: {clampedY}px; z-index: {zIndex};"
|
|
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 }}
|
|
120
160
|
onkeydown={(e) => e.key === 'Escape' && close()}
|
|
121
161
|
>
|
|
122
162
|
{#if header}
|
|
123
163
|
<p
|
|
124
|
-
class="px-3 py-1.5 text-[11px] italic text-base-content/60 truncate border-b border-base-
|
|
164
|
+
class="px-3 py-1.5 text-[11px] italic text-base-content/60 truncate border-b border-base-300"
|
|
125
165
|
>
|
|
126
166
|
{header}
|
|
127
167
|
</p>
|
|
@@ -129,7 +169,7 @@
|
|
|
129
169
|
|
|
130
170
|
{#each items as item}
|
|
131
171
|
{#if 'divider' in item}
|
|
132
|
-
<div class="border-t border-base-
|
|
172
|
+
<div class="border-t border-base-300 my-1"></div>
|
|
133
173
|
{:else}
|
|
134
174
|
<button
|
|
135
175
|
type="button"
|
|
@@ -23,7 +23,9 @@
|
|
|
23
23
|
}: Props = $props();
|
|
24
24
|
|
|
25
25
|
// Local state for custom date inputs
|
|
26
|
+
// svelte-ignore state_referenced_locally
|
|
26
27
|
let localFrom = $state(customFrom || '');
|
|
28
|
+
// svelte-ignore state_referenced_locally
|
|
27
29
|
let localTo = $state(customTo || '');
|
|
28
30
|
let showCustom = $state(false);
|
|
29
31
|
|
|
@@ -129,7 +131,7 @@
|
|
|
129
131
|
<!-- svelte-ignore a11y_no_noninteractive_tabindex -->
|
|
130
132
|
<div
|
|
131
133
|
tabindex="0"
|
|
132
|
-
class="dropdown-content rounded-box z-40 w-72 p-3
|
|
134
|
+
class="dropdown-content rounded-box z-40 w-72 p-3 mt-1 bg-base-200 border border-base-300"
|
|
133
135
|
>
|
|
134
136
|
<!-- Quick Presets -->
|
|
135
137
|
<div class="flex flex-wrap gap-1.5 mb-3">
|
|
@@ -147,7 +149,7 @@
|
|
|
147
149
|
<!-- Divider -->
|
|
148
150
|
<div class="my-2 flex items-center gap-2">
|
|
149
151
|
<div class="flex-1 h-px bg-base-300"></div>
|
|
150
|
-
<span class="text-
|
|
152
|
+
<span class="text-[0.75rem] text-base-content/45">or custom</span>
|
|
151
153
|
<div class="flex-1 h-px bg-base-300"></div>
|
|
152
154
|
</div>
|
|
153
155
|
|
|
@@ -156,7 +158,7 @@
|
|
|
156
158
|
<div class="flex gap-2">
|
|
157
159
|
<div class="form-control flex-1">
|
|
158
160
|
<label class="label py-0.5" for="date-range-from">
|
|
159
|
-
<span class="
|
|
161
|
+
<span class="text-[0.8125rem] text-base-content/45">From</span>
|
|
160
162
|
</label>
|
|
161
163
|
<input
|
|
162
164
|
id="date-range-from"
|
|
@@ -168,7 +170,7 @@
|
|
|
168
170
|
</div>
|
|
169
171
|
<div class="form-control flex-1">
|
|
170
172
|
<label class="label py-0.5" for="date-range-to">
|
|
171
|
-
<span class="
|
|
173
|
+
<span class="text-[0.8125rem] text-base-content/45">To</span>
|
|
172
174
|
</label>
|
|
173
175
|
<input
|
|
174
176
|
id="date-range-to"
|