@r2digisolutions/components 0.14.18 → 0.14.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.
|
@@ -81,6 +81,10 @@
|
|
|
81
81
|
let panel = $state<Panel>('days');
|
|
82
82
|
/** Which month column owns the month/year picker when months=2. */
|
|
83
83
|
let pickerOffset = $state(0);
|
|
84
|
+
/** Range drag: anchor day and live preview end (YYYY-MM-DD). */
|
|
85
|
+
let dragAnchor = $state<string | null>(null);
|
|
86
|
+
let dragPreviewEnd = $state<string | null>(null);
|
|
87
|
+
let isDragging = $state(false);
|
|
84
88
|
|
|
85
89
|
const weekDays = $derived(
|
|
86
90
|
Array.from({ length: 7 }, (_, i) =>
|
|
@@ -214,22 +218,105 @@
|
|
|
214
218
|
return false;
|
|
215
219
|
}
|
|
216
220
|
|
|
221
|
+
function rangeDisplayStart() {
|
|
222
|
+
if (mode !== 'range') return start;
|
|
223
|
+
if (isDragging && dragAnchor) {
|
|
224
|
+
if (!dragPreviewEnd) return dragAnchor;
|
|
225
|
+
return dragAnchor <= dragPreviewEnd ? dragAnchor : dragPreviewEnd;
|
|
226
|
+
}
|
|
227
|
+
return start;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
function rangeDisplayEnd() {
|
|
231
|
+
if (mode !== 'range') return end;
|
|
232
|
+
if (isDragging && dragAnchor && dragPreviewEnd) {
|
|
233
|
+
return dragAnchor <= dragPreviewEnd ? dragPreviewEnd : dragAnchor;
|
|
234
|
+
}
|
|
235
|
+
return end;
|
|
236
|
+
}
|
|
237
|
+
|
|
217
238
|
function isSelected(iso: string) {
|
|
218
239
|
if (mode === 'single') return value === iso;
|
|
219
240
|
if (mode === 'multiple') return values.includes(iso);
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
241
|
+
const rangeStart = rangeDisplayStart();
|
|
242
|
+
const rangeEnd = rangeDisplayEnd();
|
|
243
|
+
if (!rangeStart) return false;
|
|
244
|
+
if (!rangeEnd) return rangeStart === iso;
|
|
245
|
+
return iso >= rangeStart && iso <= rangeEnd;
|
|
223
246
|
}
|
|
224
247
|
|
|
225
248
|
function isRangeEdge(iso: string) {
|
|
226
|
-
|
|
249
|
+
if (mode !== 'range') return false;
|
|
250
|
+
const rangeStart = rangeDisplayStart();
|
|
251
|
+
const rangeEnd = rangeDisplayEnd();
|
|
252
|
+
return iso === rangeStart || iso === rangeEnd;
|
|
227
253
|
}
|
|
228
254
|
|
|
229
255
|
function isInRange(iso: string) {
|
|
230
|
-
|
|
256
|
+
if (mode !== 'range') return false;
|
|
257
|
+
const rangeStart = rangeDisplayStart();
|
|
258
|
+
const rangeEnd = rangeDisplayEnd();
|
|
259
|
+
return !!rangeStart && !!rangeEnd && iso > rangeStart && iso < rangeEnd;
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function dayFromTarget(target: EventTarget | null) {
|
|
263
|
+
if (!(target instanceof Element)) return null;
|
|
264
|
+
const button = target.closest('[data-calendar-day]');
|
|
265
|
+
if (!(button instanceof HTMLElement)) return null;
|
|
266
|
+
const iso = button.dataset.calendarDay;
|
|
267
|
+
return iso && !isDisabled(iso) ? iso : null;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
function beginRangeDrag(iso: string, event: PointerEvent) {
|
|
271
|
+
if (mode !== 'range' || isDisabled(iso)) return;
|
|
272
|
+
event.preventDefault();
|
|
273
|
+
isDragging = true;
|
|
274
|
+
dragAnchor = iso;
|
|
275
|
+
dragPreviewEnd = null;
|
|
231
276
|
}
|
|
232
277
|
|
|
278
|
+
function finishRangeDrag(iso: string | null) {
|
|
279
|
+
if (!dragAnchor) return;
|
|
280
|
+
|
|
281
|
+
if (dragPreviewEnd && dragPreviewEnd !== dragAnchor) {
|
|
282
|
+
const rangeStart = dragAnchor <= dragPreviewEnd ? dragAnchor : dragPreviewEnd;
|
|
283
|
+
const rangeEnd = dragAnchor <= dragPreviewEnd ? dragPreviewEnd : dragAnchor;
|
|
284
|
+
start = rangeStart;
|
|
285
|
+
end = rangeEnd;
|
|
286
|
+
emit();
|
|
287
|
+
} else {
|
|
288
|
+
pick(iso ?? dragAnchor);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
isDragging = false;
|
|
292
|
+
dragAnchor = null;
|
|
293
|
+
dragPreviewEnd = null;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
$effect(() => {
|
|
297
|
+
if (!isDragging) return;
|
|
298
|
+
|
|
299
|
+
const onMove = (event: PointerEvent) => {
|
|
300
|
+
const iso = dayFromTarget(document.elementFromPoint(event.clientX, event.clientY));
|
|
301
|
+
if (iso) dragPreviewEnd = iso;
|
|
302
|
+
};
|
|
303
|
+
|
|
304
|
+
const onUp = (event: PointerEvent) => {
|
|
305
|
+
const iso =
|
|
306
|
+
dayFromTarget(document.elementFromPoint(event.clientX, event.clientY)) ?? dragAnchor;
|
|
307
|
+
finishRangeDrag(iso);
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
window.addEventListener('pointermove', onMove);
|
|
311
|
+
window.addEventListener('pointerup', onUp);
|
|
312
|
+
window.addEventListener('pointercancel', onUp);
|
|
313
|
+
return () => {
|
|
314
|
+
window.removeEventListener('pointermove', onMove);
|
|
315
|
+
window.removeEventListener('pointerup', onUp);
|
|
316
|
+
window.removeEventListener('pointercancel', onUp);
|
|
317
|
+
};
|
|
318
|
+
});
|
|
319
|
+
|
|
233
320
|
function emit() {
|
|
234
321
|
onchange?.({ mode, value, values, start, end });
|
|
235
322
|
}
|
|
@@ -275,10 +362,13 @@
|
|
|
275
362
|
const mid = isInRange(iso);
|
|
276
363
|
const disabled = isDisabled(iso);
|
|
277
364
|
const isToday = iso === todayIso;
|
|
365
|
+
const rangeStart = rangeDisplayStart();
|
|
366
|
+
const rangeEnd = rangeDisplayEnd();
|
|
278
367
|
|
|
279
368
|
return [
|
|
280
369
|
'relative flex size-9 items-center justify-center rounded-lg text-sm transition-colors',
|
|
281
370
|
'focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-brand-500/30',
|
|
371
|
+
mode === 'range' && !disabled && 'touch-none select-none',
|
|
282
372
|
disabled && 'cursor-not-allowed opacity-30',
|
|
283
373
|
!disabled && !selected && !mid && 'text-primary hover:bg-surface-overlay',
|
|
284
374
|
mid && 'rounded-none bg-brand-500/15 text-primary',
|
|
@@ -286,17 +376,17 @@
|
|
|
286
376
|
edge && 'bg-brand-500 font-semibold text-white',
|
|
287
377
|
selected &&
|
|
288
378
|
mode === 'range' &&
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
iso ===
|
|
292
|
-
|
|
379
|
+
rangeStart &&
|
|
380
|
+
rangeEnd &&
|
|
381
|
+
iso === rangeStart &&
|
|
382
|
+
rangeEnd !== rangeStart &&
|
|
293
383
|
'rounded-r-none',
|
|
294
384
|
selected &&
|
|
295
385
|
mode === 'range' &&
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
iso ===
|
|
299
|
-
|
|
386
|
+
rangeStart &&
|
|
387
|
+
rangeEnd &&
|
|
388
|
+
iso === rangeEnd &&
|
|
389
|
+
rangeEnd !== rangeStart &&
|
|
300
390
|
'rounded-l-none',
|
|
301
391
|
isToday && !selected && !mid && 'ring-1 ring-brand-500/40'
|
|
302
392
|
];
|
|
@@ -319,8 +409,12 @@
|
|
|
319
409
|
framed && 'rounded-2xl border border-border p-3 shadow-sm',
|
|
320
410
|
!framed && 'p-0.5',
|
|
321
411
|
dual && panel === 'days' ? 'w-fit' : 'w-80',
|
|
412
|
+
isDragging && 'select-none',
|
|
322
413
|
className
|
|
323
414
|
]}
|
|
415
|
+
onpointerleave={() => {
|
|
416
|
+
if (isDragging) dragPreviewEnd = null;
|
|
417
|
+
}}
|
|
324
418
|
>
|
|
325
419
|
{#if panel === 'days'}
|
|
326
420
|
<div class={['flex', dual ? 'flex-col gap-4 sm:flex-row sm:gap-2' : 'flex-col']}>
|
|
@@ -391,7 +485,22 @@
|
|
|
391
485
|
type="button"
|
|
392
486
|
disabled={isDisabled(cell.date)}
|
|
393
487
|
aria-pressed={isSelected(cell.date)}
|
|
394
|
-
|
|
488
|
+
data-calendar-day={cell.date}
|
|
489
|
+
onclick={() => {
|
|
490
|
+
if (mode !== 'range') pick(cell.date);
|
|
491
|
+
}}
|
|
492
|
+
onpointerdown={
|
|
493
|
+
mode === 'range'
|
|
494
|
+
? (event) => beginRangeDrag(cell.date, event)
|
|
495
|
+
: undefined
|
|
496
|
+
}
|
|
497
|
+
onpointerenter={
|
|
498
|
+
mode === 'range' && isDragging && !isDisabled(cell.date)
|
|
499
|
+
? () => {
|
|
500
|
+
dragPreviewEnd = cell.date;
|
|
501
|
+
}
|
|
502
|
+
: undefined
|
|
503
|
+
}
|
|
395
504
|
class={dayClass(cell.date)}
|
|
396
505
|
>
|
|
397
506
|
<span class="relative z-10">{cell.day}</span>
|
|
@@ -79,9 +79,11 @@
|
|
|
79
79
|
'focus-visible:ring-brand-500/30 focus-visible:ring-2 focus-visible:outline-none focus-visible:ring-inset',
|
|
80
80
|
disabled && 'cursor-not-allowed opacity-40',
|
|
81
81
|
!disabled &&
|
|
82
|
-
(item.isHighlighted
|
|
82
|
+
(item.isHighlighted
|
|
83
83
|
? 'bg-brand-500 text-white'
|
|
84
|
-
:
|
|
84
|
+
: item.isSelected
|
|
85
|
+
? 'bg-brand-500/10 font-medium text-primary'
|
|
86
|
+
: 'text-primary hover:bg-surface-overlay'),
|
|
85
87
|
className
|
|
86
88
|
]}
|
|
87
89
|
>
|
|
@@ -196,12 +196,19 @@
|
|
|
196
196
|
const spaceAbove = rect.top - gap;
|
|
197
197
|
const openUp = spaceBelow < Math.min(maxHeight, 160) && spaceAbove > spaceBelow;
|
|
198
198
|
const available = Math.max(120, openUp ? spaceAbove : spaceBelow);
|
|
199
|
+
const maxWidth = Math.min(320, window.innerWidth - 16);
|
|
200
|
+
let left = rect.left;
|
|
201
|
+
if (left + maxWidth > window.innerWidth - 8) {
|
|
202
|
+
left = Math.max(8, window.innerWidth - maxWidth - 8);
|
|
203
|
+
}
|
|
199
204
|
|
|
200
205
|
listboxStyle = [
|
|
201
206
|
`top: ${openUp ? 'auto' : `${rect.bottom + gap}px`}`,
|
|
202
207
|
`bottom: ${openUp ? `${window.innerHeight - rect.top + gap}px` : 'auto'}`,
|
|
203
|
-
`left: ${
|
|
204
|
-
`width: ${rect.width}px`,
|
|
208
|
+
`left: ${left}px`,
|
|
209
|
+
`min-width: ${rect.width}px`,
|
|
210
|
+
`width: max-content`,
|
|
211
|
+
`max-width: ${maxWidth}px`,
|
|
205
212
|
`max-height: ${Math.min(maxHeight, available)}px`
|
|
206
213
|
].join('; ');
|
|
207
214
|
}
|
|
@@ -577,7 +584,7 @@
|
|
|
577
584
|
>
|
|
578
585
|
<path stroke-linecap="round" stroke-linejoin="round" d="M15 19l-7-7 7-7" />
|
|
579
586
|
</svg>
|
|
580
|
-
<span class="
|
|
587
|
+
<span class="whitespace-nowrap">{currentLevel.title}</span>
|
|
581
588
|
</button>
|
|
582
589
|
<div class="border-border mx-1.5 mb-0.5 h-px border-b" role="separator"></div>
|
|
583
590
|
{/if}
|
|
@@ -605,33 +612,41 @@
|
|
|
605
612
|
onpointerdown={(e) => handleOptionPointerDown(e, option)}
|
|
606
613
|
onclick={() => selectOption(option)}
|
|
607
614
|
onpointerenter={() => {
|
|
608
|
-
if (
|
|
615
|
+
if (option.disabled) return;
|
|
616
|
+
ignoreHover = false;
|
|
609
617
|
highlightedIndex = index;
|
|
610
618
|
}}
|
|
611
619
|
class={[
|
|
612
620
|
'group gap-2.5 rounded-lg px-2.5 py-2 text-sm relative flex w-full items-center text-left outline-none select-none',
|
|
613
621
|
'transition-[background-color,color,box-shadow] duration-75',
|
|
614
622
|
option.disabled ? 'cursor-not-allowed opacity-40' : 'cursor-pointer',
|
|
615
|
-
|
|
623
|
+
isHighlighted && !option.disabled
|
|
616
624
|
? 'bg-brand-500 text-white shadow-sm'
|
|
617
|
-
:
|
|
625
|
+
: isSelected && !option.disabled
|
|
626
|
+
? 'bg-brand-500/10 text-primary font-medium'
|
|
627
|
+
: 'text-primary hover:bg-surface-overlay'
|
|
618
628
|
]}
|
|
619
629
|
>
|
|
620
630
|
{#if !hasChildren}
|
|
621
631
|
<span
|
|
622
632
|
class={[
|
|
623
633
|
'h-4 w-4 flex shrink-0 items-center justify-center rounded-full border transition-colors duration-75',
|
|
624
|
-
|
|
634
|
+
isHighlighted && !option.disabled
|
|
625
635
|
? isSelected
|
|
626
636
|
? 'border-white bg-white'
|
|
627
637
|
: 'border-white/60 bg-transparent'
|
|
628
|
-
:
|
|
638
|
+
: isSelected && !option.disabled
|
|
639
|
+
? 'border-brand-500 bg-brand-500'
|
|
640
|
+
: 'border-border-strong bg-transparent'
|
|
629
641
|
]}
|
|
630
642
|
aria-hidden="true"
|
|
631
643
|
>
|
|
632
644
|
{#if isSelected}
|
|
633
645
|
<svg
|
|
634
|
-
class=
|
|
646
|
+
class={[
|
|
647
|
+
'h-2.5 w-2.5',
|
|
648
|
+
isHighlighted ? 'text-brand-600' : 'text-white'
|
|
649
|
+
]}
|
|
635
650
|
viewBox="0 0 24 24"
|
|
636
651
|
fill="none"
|
|
637
652
|
stroke="currentColor"
|
|
@@ -644,14 +659,12 @@
|
|
|
644
659
|
{/if}
|
|
645
660
|
|
|
646
661
|
<span class="min-w-0 flex-1">
|
|
647
|
-
<span class=
|
|
648
|
-
{option.label}
|
|
649
|
-
</span>
|
|
662
|
+
<span class="block whitespace-nowrap">{option.label}</span>
|
|
650
663
|
{#if secondary}
|
|
651
664
|
<span
|
|
652
665
|
class={[
|
|
653
|
-
'mt-0.5 block
|
|
654
|
-
|
|
666
|
+
'mt-0.5 block text-[11px] whitespace-nowrap',
|
|
667
|
+
isHighlighted && !option.disabled
|
|
655
668
|
? 'text-white/75'
|
|
656
669
|
: 'text-secondary'
|
|
657
670
|
]}
|
|
@@ -665,7 +678,7 @@
|
|
|
665
678
|
<svg
|
|
666
679
|
class={[
|
|
667
680
|
'h-4 w-4 shrink-0',
|
|
668
|
-
|
|
681
|
+
isHighlighted && !option.disabled
|
|
669
682
|
? 'text-white/80'
|
|
670
683
|
: 'text-secondary'
|
|
671
684
|
]}
|
|
@@ -76,6 +76,8 @@
|
|
|
76
76
|
}: DialogProps = $props();
|
|
77
77
|
|
|
78
78
|
let dialogEl = $state<HTMLDialogElement | null>(null);
|
|
79
|
+
/** Ignora clics en backdrop justo tras cerrar un diálogo nativo (p. ej. selector de archivos). */
|
|
80
|
+
let suppressBackdropUntil = 0;
|
|
79
81
|
const titleId = $derived(`${id}-title`);
|
|
80
82
|
const descriptionId = $derived(`${id}-description`);
|
|
81
83
|
|
|
@@ -152,6 +154,15 @@
|
|
|
152
154
|
};
|
|
153
155
|
});
|
|
154
156
|
|
|
157
|
+
$effect(() => {
|
|
158
|
+
if (!open || typeof window === 'undefined') return;
|
|
159
|
+
const onWindowFocus = () => {
|
|
160
|
+
suppressBackdropUntil = Date.now() + 400;
|
|
161
|
+
};
|
|
162
|
+
window.addEventListener('focus', onWindowFocus);
|
|
163
|
+
return () => window.removeEventListener('focus', onWindowFocus);
|
|
164
|
+
});
|
|
165
|
+
|
|
155
166
|
function close(reason: 'close' | 'cancel' | 'confirm' | 'backdrop' | 'escape' = 'close') {
|
|
156
167
|
if (!open) return;
|
|
157
168
|
open = false;
|
|
@@ -186,6 +197,7 @@
|
|
|
186
197
|
|
|
187
198
|
function handleBackdropClick(event: MouseEvent) {
|
|
188
199
|
if (!dialogEl) return;
|
|
200
|
+
if (Date.now() < suppressBackdropUntil) return;
|
|
189
201
|
const rect = dialogEl.getBoundingClientRect();
|
|
190
202
|
const clickedInDialog =
|
|
191
203
|
event.clientX >= rect.left &&
|