@makolabs/ripple 3.0.1 → 3.0.3
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/dist/charts/Chart.svelte +8 -36
- package/dist/drawer/Drawer.svelte +29 -36
- package/dist/drawer/drawer.js +2 -2
- package/dist/elements/accordion/Accordion.svelte +1 -1
- package/dist/elements/combobox/ComboBox.svelte +49 -16
- package/dist/elements/dropdown/Select.svelte +104 -70
- package/dist/elements/popover/Popover.svelte +48 -45
- package/dist/filters/CompactFilters.svelte +1 -1
- package/dist/forms/DateRange.svelte +218 -199
- package/dist/forms/NumberInput.svelte +148 -39
- package/dist/forms/SegmentedControl.svelte +1 -1
- package/dist/forms/Toggle.svelte +18 -18
- package/dist/forms/calendar/Calendar.svelte +2 -1
- package/dist/forms/date-picker/DatePicker.svelte +8 -1
- package/dist/forms/form-types.d.ts +28 -0
- package/dist/forms/month-picker/MonthPicker.svelte +306 -0
- package/dist/forms/month-picker/MonthPicker.svelte.d.ts +4 -0
- package/dist/forms/month-picker/month-picker-types.d.ts +22 -0
- package/dist/forms/month-picker/month-picker-types.js +1 -0
- package/dist/forms/segmented-control.d.ts +2 -2
- package/dist/forms/segmented-control.js +7 -5
- package/dist/index.d.ts +2 -0
- package/dist/index.js +1 -0
- package/dist/layout/activity-list/ActivityList.svelte +1 -1
- package/dist/modal/modal.js +4 -4
- package/package.json +1 -1
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
import Portal from '../utils/Portal.svelte';
|
|
7
7
|
import { fly } from 'svelte/transition';
|
|
8
8
|
import { quintOut } from 'svelte/easing';
|
|
9
|
+
import { onMount } from 'svelte';
|
|
9
10
|
|
|
10
11
|
let {
|
|
11
12
|
startDate = $bindable(),
|
|
@@ -27,6 +28,16 @@
|
|
|
27
28
|
|
|
28
29
|
const tokens = $derived(formSizeTokens[size]);
|
|
29
30
|
|
|
31
|
+
let isMobile = $state(
|
|
32
|
+
typeof window !== 'undefined' && window.matchMedia('(max-width: 639.98px)').matches
|
|
33
|
+
);
|
|
34
|
+
onMount(() => {
|
|
35
|
+
const mql = window.matchMedia('(max-width: 639.98px)');
|
|
36
|
+
const handler = (e: MediaQueryListEvent) => (isMobile = e.matches);
|
|
37
|
+
mql.addEventListener('change', handler);
|
|
38
|
+
return () => mql.removeEventListener('change', handler);
|
|
39
|
+
});
|
|
40
|
+
|
|
30
41
|
let isOpen = $state(false);
|
|
31
42
|
let hoveredDate = $state<Date | null>(null);
|
|
32
43
|
let datePickerRef = $state<HTMLDivElement | null>(null);
|
|
@@ -309,213 +320,221 @@
|
|
|
309
320
|
</div>
|
|
310
321
|
{/if}
|
|
311
322
|
|
|
312
|
-
{#if isOpen}
|
|
323
|
+
{#if isOpen && !isMobile}
|
|
313
324
|
<Portal target={datePickerRef}>
|
|
314
325
|
<div
|
|
315
326
|
bind:this={calendarRef}
|
|
316
327
|
class="ring-opacity-5 ring-default-300 absolute z-10 mt-1 w-full origin-top-left rounded-md bg-white p-4 shadow-lg ring-1 focus:outline-none"
|
|
317
328
|
transition:fly={{ y: -8, duration: 300, easing: quintOut }}
|
|
318
329
|
>
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
aria-label="Previous month"
|
|
324
|
-
class="text-default-500 hover:bg-default-100 inline-flex items-center rounded-md p-1 text-sm"
|
|
325
|
-
onclick={prevMonth}
|
|
326
|
-
>
|
|
327
|
-
<svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
|
328
|
-
<path
|
|
329
|
-
fill-rule="evenodd"
|
|
330
|
-
d="M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z"
|
|
331
|
-
clip-rule="evenodd"
|
|
332
|
-
/>
|
|
333
|
-
</svg>
|
|
334
|
-
</button>
|
|
335
|
-
<button
|
|
336
|
-
type="button"
|
|
337
|
-
class="text-default-700 hover:bg-default-100 inline-flex items-center rounded-md px-2 py-1 text-sm font-medium"
|
|
338
|
-
onclick={showMonths}
|
|
339
|
-
>
|
|
340
|
-
{getMonthName(viewDate.getMonth())}
|
|
341
|
-
{viewDate.getFullYear()}
|
|
342
|
-
</button>
|
|
343
|
-
<button
|
|
344
|
-
type="button"
|
|
345
|
-
aria-label="Next month"
|
|
346
|
-
class="text-default-500 hover:bg-default-100 inline-flex items-center rounded-md p-1 text-sm"
|
|
347
|
-
onclick={nextMonth}
|
|
348
|
-
>
|
|
349
|
-
<svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
|
350
|
-
<path
|
|
351
|
-
fill-rule="evenodd"
|
|
352
|
-
d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z"
|
|
353
|
-
clip-rule="evenodd"
|
|
354
|
-
/>
|
|
355
|
-
</svg>
|
|
356
|
-
</button>
|
|
357
|
-
{:else if viewMode === 'months'}
|
|
358
|
-
<button
|
|
359
|
-
type="button"
|
|
360
|
-
aria-label="Previous year"
|
|
361
|
-
class="text-default-500 hover:bg-default-100 inline-flex items-center rounded-md p-1 text-sm"
|
|
362
|
-
onclick={() =>
|
|
363
|
-
(viewDate = new Date(viewDate.getFullYear() - 1, viewDate.getMonth(), 1))}
|
|
364
|
-
>
|
|
365
|
-
<svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
|
366
|
-
<path
|
|
367
|
-
fill-rule="evenodd"
|
|
368
|
-
d="M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z"
|
|
369
|
-
clip-rule="evenodd"
|
|
370
|
-
/>
|
|
371
|
-
</svg>
|
|
372
|
-
</button>
|
|
373
|
-
<button
|
|
374
|
-
type="button"
|
|
375
|
-
aria-label="Current year"
|
|
376
|
-
class="text-default-700 inline-flex items-center rounded-md px-2 py-1 text-sm font-medium"
|
|
377
|
-
onclick={showYears}
|
|
378
|
-
>
|
|
379
|
-
{viewDate.getFullYear()}
|
|
380
|
-
</button>
|
|
381
|
-
<button
|
|
382
|
-
type="button"
|
|
383
|
-
aria-label="Next year"
|
|
384
|
-
class="text-default-500 hover:bg-default-100 inline-flex items-center rounded-md p-1 text-sm"
|
|
385
|
-
onclick={() =>
|
|
386
|
-
(viewDate = new Date(viewDate.getFullYear() + 1, viewDate.getMonth(), 1))}
|
|
387
|
-
>
|
|
388
|
-
<svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
|
389
|
-
<path
|
|
390
|
-
fill-rule="evenodd"
|
|
391
|
-
d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z"
|
|
392
|
-
clip-rule="evenodd"
|
|
393
|
-
/>
|
|
394
|
-
</svg>
|
|
395
|
-
</button>
|
|
396
|
-
{:else if viewMode === 'years'}
|
|
397
|
-
<button
|
|
398
|
-
type="button"
|
|
399
|
-
aria-label="Previous year"
|
|
400
|
-
class="text-default-500 hover:bg-default-100 inline-flex items-center rounded-md p-1 text-sm"
|
|
401
|
-
onclick={() =>
|
|
402
|
-
(viewDate = new Date(viewDate.getFullYear() - 12, viewDate.getMonth(), 1))}
|
|
403
|
-
>
|
|
404
|
-
<svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
|
405
|
-
<path
|
|
406
|
-
fill-rule="evenodd"
|
|
407
|
-
d="M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z"
|
|
408
|
-
clip-rule="evenodd"
|
|
409
|
-
/>
|
|
410
|
-
</svg>
|
|
411
|
-
</button>
|
|
412
|
-
<button
|
|
413
|
-
type="button"
|
|
414
|
-
aria-label="Current year range"
|
|
415
|
-
class="text-default-700 inline-flex items-center rounded-md px-2 py-1 text-sm font-medium"
|
|
416
|
-
>
|
|
417
|
-
{viewDate.getFullYear() - 6} - {viewDate.getFullYear() + 5}
|
|
418
|
-
</button>
|
|
419
|
-
<button
|
|
420
|
-
type="button"
|
|
421
|
-
aria-label="Next year"
|
|
422
|
-
class="text-default-500 hover:bg-default-100 inline-flex items-center rounded-md p-1 text-sm"
|
|
423
|
-
onclick={() =>
|
|
424
|
-
(viewDate = new Date(viewDate.getFullYear() + 12, viewDate.getMonth(), 1))}
|
|
425
|
-
>
|
|
426
|
-
<svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
|
|
427
|
-
<path
|
|
428
|
-
fill-rule="evenodd"
|
|
429
|
-
d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z"
|
|
430
|
-
clip-rule="evenodd"
|
|
431
|
-
/>
|
|
432
|
-
</svg>
|
|
433
|
-
</button>
|
|
434
|
-
{/if}
|
|
435
|
-
</div>
|
|
330
|
+
{@render calendarContent()}
|
|
331
|
+
</div>
|
|
332
|
+
</Portal>
|
|
333
|
+
{/if}
|
|
436
334
|
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
!isCurrentMonth && !isSelected && !isInRange ? 'text-default-400' : '',
|
|
457
|
-
isToday && !isSelected ? 'border-primary-500 border' : ''
|
|
458
|
-
)}
|
|
459
|
-
onclick={() => handleDateClick(date)}
|
|
460
|
-
onmouseenter={() => handleDateHover(date)}
|
|
461
|
-
disabled={isDisabled}
|
|
462
|
-
>
|
|
463
|
-
{date.getDate()}
|
|
464
|
-
</button>
|
|
465
|
-
{/each}
|
|
466
|
-
</div>
|
|
467
|
-
{:else if viewMode === 'months'}
|
|
468
|
-
<div class="grid grid-cols-3 gap-2">
|
|
469
|
-
<!-- eslint-disable-next-line @typescript-eslint/no-unused-vars -->
|
|
470
|
-
{#each Array(12).fill(0) as _, month (month)}
|
|
471
|
-
<button
|
|
472
|
-
type="button"
|
|
473
|
-
class={cn(
|
|
474
|
-
'flex items-center justify-center rounded-md px-2 py-1 text-sm font-medium',
|
|
475
|
-
viewDate.getMonth() === month
|
|
476
|
-
? 'bg-primary-500 text-white'
|
|
477
|
-
: 'text-default-700 hover:bg-default-100'
|
|
478
|
-
)}
|
|
479
|
-
onclick={() => selectMonth(month)}
|
|
480
|
-
>
|
|
481
|
-
{getMonthName(month).substring(0, 3)}
|
|
482
|
-
</button>
|
|
483
|
-
{/each}
|
|
484
|
-
</div>
|
|
485
|
-
{:else if viewMode === 'years'}
|
|
486
|
-
<div class="grid grid-cols-3 gap-2">
|
|
487
|
-
<!-- eslint-disable-next-line @typescript-eslint/no-unused-vars -->
|
|
488
|
-
{#each Array(12).fill(0) as _, i (i)}
|
|
489
|
-
{@const year = viewDate.getFullYear() - 6 + i}
|
|
490
|
-
<button
|
|
491
|
-
type="button"
|
|
492
|
-
class={cn(
|
|
493
|
-
'flex items-center justify-center rounded-md px-2 py-1 text-sm font-medium',
|
|
494
|
-
viewDate.getFullYear() === year
|
|
495
|
-
? 'bg-primary-500 text-white'
|
|
496
|
-
: 'text-default-700 hover:bg-default-100'
|
|
497
|
-
)}
|
|
498
|
-
onclick={() => selectYear(year)}
|
|
499
|
-
>
|
|
500
|
-
{year}
|
|
501
|
-
</button>
|
|
502
|
-
{/each}
|
|
503
|
-
</div>
|
|
504
|
-
{/if}
|
|
505
|
-
|
|
506
|
-
{#if startDate || endDate}
|
|
507
|
-
<div
|
|
508
|
-
class="border-default-200 text-default-500 mt-4 flex flex-wrap justify-between gap-x-4 gap-y-1 border-t pt-3 text-xs"
|
|
509
|
-
>
|
|
510
|
-
<div>
|
|
511
|
-
{startDate ? `${startLabel}: ${formatDate(startDate)}` : ''}
|
|
512
|
-
</div>
|
|
513
|
-
<div>
|
|
514
|
-
{endDate ? `${endLabel}: ${formatDate(endDate)}` : ''}
|
|
515
|
-
</div>
|
|
516
|
-
</div>
|
|
517
|
-
{/if}
|
|
335
|
+
{#if isOpen && isMobile}
|
|
336
|
+
<Portal target={datePickerRef}>
|
|
337
|
+
<button
|
|
338
|
+
type="button"
|
|
339
|
+
class="fixed inset-0 z-[9998] bg-black/40 backdrop-blur-sm"
|
|
340
|
+
aria-label="Close"
|
|
341
|
+
onclick={() => (isOpen = false)}
|
|
342
|
+
></button>
|
|
343
|
+
<div
|
|
344
|
+
class="fixed inset-x-0 bottom-0 z-[9999] flex max-h-[85vh] min-h-48 flex-col overflow-hidden rounded-t-2xl bg-white shadow-2xl"
|
|
345
|
+
transition:fly={{ y: 300, duration: 200, easing: quintOut }}
|
|
346
|
+
bind:this={calendarRef}
|
|
347
|
+
>
|
|
348
|
+
<div class="flex justify-center py-2">
|
|
349
|
+
<div class="bg-default-300 h-1 w-8 rounded-full"></div>
|
|
350
|
+
</div>
|
|
351
|
+
<div class="flex-1 cursor-pointer overflow-y-auto p-4">
|
|
352
|
+
{@render calendarContent()}
|
|
353
|
+
</div>
|
|
518
354
|
</div>
|
|
519
355
|
</Portal>
|
|
520
356
|
{/if}
|
|
521
357
|
</div>
|
|
358
|
+
|
|
359
|
+
{#snippet calendarContent()}
|
|
360
|
+
<div class="mb-2 flex items-center justify-between">
|
|
361
|
+
{#if viewMode === 'days'}
|
|
362
|
+
<button
|
|
363
|
+
type="button"
|
|
364
|
+
aria-label="Previous month"
|
|
365
|
+
class="text-default-500 hover:bg-default-100 inline-flex cursor-pointer items-center rounded-md p-1 text-sm"
|
|
366
|
+
onclick={prevMonth}
|
|
367
|
+
>
|
|
368
|
+
<svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor"
|
|
369
|
+
><path
|
|
370
|
+
fill-rule="evenodd"
|
|
371
|
+
d="M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z"
|
|
372
|
+
clip-rule="evenodd"
|
|
373
|
+
/></svg
|
|
374
|
+
>
|
|
375
|
+
</button>
|
|
376
|
+
<button
|
|
377
|
+
type="button"
|
|
378
|
+
class="text-default-700 hover:bg-default-100 inline-flex cursor-pointer items-center rounded-md px-2 py-1 text-sm font-medium"
|
|
379
|
+
onclick={showMonths}
|
|
380
|
+
>
|
|
381
|
+
{getMonthName(viewDate.getMonth())}
|
|
382
|
+
{viewDate.getFullYear()}
|
|
383
|
+
</button>
|
|
384
|
+
<button
|
|
385
|
+
type="button"
|
|
386
|
+
aria-label="Next month"
|
|
387
|
+
class="text-default-500 hover:bg-default-100 inline-flex cursor-pointer items-center rounded-md p-1 text-sm"
|
|
388
|
+
onclick={nextMonth}
|
|
389
|
+
>
|
|
390
|
+
<svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor"
|
|
391
|
+
><path
|
|
392
|
+
fill-rule="evenodd"
|
|
393
|
+
d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z"
|
|
394
|
+
clip-rule="evenodd"
|
|
395
|
+
/></svg
|
|
396
|
+
>
|
|
397
|
+
</button>
|
|
398
|
+
{:else if viewMode === 'months'}
|
|
399
|
+
<button
|
|
400
|
+
type="button"
|
|
401
|
+
aria-label="Previous year"
|
|
402
|
+
class="text-default-500 hover:bg-default-100 inline-flex cursor-pointer items-center rounded-md p-1 text-sm"
|
|
403
|
+
onclick={() => (viewDate = new Date(viewDate.getFullYear() - 1, viewDate.getMonth(), 1))}
|
|
404
|
+
>
|
|
405
|
+
<svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor"
|
|
406
|
+
><path
|
|
407
|
+
fill-rule="evenodd"
|
|
408
|
+
d="M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z"
|
|
409
|
+
clip-rule="evenodd"
|
|
410
|
+
/></svg
|
|
411
|
+
>
|
|
412
|
+
</button>
|
|
413
|
+
<button
|
|
414
|
+
type="button"
|
|
415
|
+
class="text-default-700 inline-flex cursor-pointer items-center rounded-md px-2 py-1 text-sm font-medium"
|
|
416
|
+
onclick={showYears}>{viewDate.getFullYear()}</button
|
|
417
|
+
>
|
|
418
|
+
<button
|
|
419
|
+
type="button"
|
|
420
|
+
aria-label="Next year"
|
|
421
|
+
class="text-default-500 hover:bg-default-100 inline-flex cursor-pointer items-center rounded-md p-1 text-sm"
|
|
422
|
+
onclick={() => (viewDate = new Date(viewDate.getFullYear() + 1, viewDate.getMonth(), 1))}
|
|
423
|
+
>
|
|
424
|
+
<svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor"
|
|
425
|
+
><path
|
|
426
|
+
fill-rule="evenodd"
|
|
427
|
+
d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z"
|
|
428
|
+
clip-rule="evenodd"
|
|
429
|
+
/></svg
|
|
430
|
+
>
|
|
431
|
+
</button>
|
|
432
|
+
{:else if viewMode === 'years'}
|
|
433
|
+
<button
|
|
434
|
+
type="button"
|
|
435
|
+
aria-label="Previous years"
|
|
436
|
+
class="text-default-500 hover:bg-default-100 inline-flex cursor-pointer items-center rounded-md p-1 text-sm"
|
|
437
|
+
onclick={() => (viewDate = new Date(viewDate.getFullYear() - 12, viewDate.getMonth(), 1))}
|
|
438
|
+
>
|
|
439
|
+
<svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor"
|
|
440
|
+
><path
|
|
441
|
+
fill-rule="evenodd"
|
|
442
|
+
d="M12.707 5.293a1 1 0 010 1.414L9.414 10l3.293 3.293a1 1 0 01-1.414 1.414l-4-4a1 1 0 010-1.414l4-4a1 1 0 011.414 0z"
|
|
443
|
+
clip-rule="evenodd"
|
|
444
|
+
/></svg
|
|
445
|
+
>
|
|
446
|
+
</button>
|
|
447
|
+
<button
|
|
448
|
+
type="button"
|
|
449
|
+
class="text-default-700 inline-flex items-center rounded-md px-2 py-1 text-sm font-medium"
|
|
450
|
+
>{viewDate.getFullYear() - 6} - {viewDate.getFullYear() + 5}</button
|
|
451
|
+
>
|
|
452
|
+
<button
|
|
453
|
+
type="button"
|
|
454
|
+
aria-label="Next years"
|
|
455
|
+
class="text-default-500 hover:bg-default-100 inline-flex cursor-pointer items-center rounded-md p-1 text-sm"
|
|
456
|
+
onclick={() => (viewDate = new Date(viewDate.getFullYear() + 12, viewDate.getMonth(), 1))}
|
|
457
|
+
>
|
|
458
|
+
<svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor"
|
|
459
|
+
><path
|
|
460
|
+
fill-rule="evenodd"
|
|
461
|
+
d="M7.293 14.707a1 1 0 010-1.414L10.586 10 7.293 6.707a1 1 0 011.414-1.414l4 4a1 1 0 010 1.414l-4 4a1 1 0 01-1.414 0z"
|
|
462
|
+
clip-rule="evenodd"
|
|
463
|
+
/></svg
|
|
464
|
+
>
|
|
465
|
+
</button>
|
|
466
|
+
{/if}
|
|
467
|
+
</div>
|
|
468
|
+
|
|
469
|
+
{#if viewMode === 'days'}
|
|
470
|
+
<div class="text-default-500 mb-1 grid grid-cols-7 gap-1 text-center text-xs font-medium">
|
|
471
|
+
<div>Su</div>
|
|
472
|
+
<div>Mo</div>
|
|
473
|
+
<div>Tu</div>
|
|
474
|
+
<div>We</div>
|
|
475
|
+
<div>Th</div>
|
|
476
|
+
<div>Fr</div>
|
|
477
|
+
<div>Sa</div>
|
|
478
|
+
</div>
|
|
479
|
+
<div class="grid grid-cols-7 gap-1">
|
|
480
|
+
{#each getDaysInMonth(viewDate) as { date, isCurrentMonth, isToday, isSelected, isInRange, isDisabled } (date.getTime())}
|
|
481
|
+
<button
|
|
482
|
+
type="button"
|
|
483
|
+
class={cn(
|
|
484
|
+
'flex h-8 w-8 cursor-pointer items-center justify-center rounded-full text-sm font-medium',
|
|
485
|
+
isDisabled ? 'text-default-300 cursor-not-allowed' : 'hover:bg-default-100',
|
|
486
|
+
isSelected ? 'bg-primary-500 hover:bg-primary-600 text-white' : '',
|
|
487
|
+
isInRange && !isSelected ? 'bg-primary-100 text-primary-800' : '',
|
|
488
|
+
!isCurrentMonth && !isSelected && !isInRange ? 'text-default-400' : '',
|
|
489
|
+
isToday && !isSelected ? 'border-primary-500 border' : ''
|
|
490
|
+
)}
|
|
491
|
+
onclick={() => handleDateClick(date)}
|
|
492
|
+
onmouseenter={() => handleDateHover(date)}
|
|
493
|
+
disabled={isDisabled}>{date.getDate()}</button
|
|
494
|
+
>
|
|
495
|
+
{/each}
|
|
496
|
+
</div>
|
|
497
|
+
{:else if viewMode === 'months'}
|
|
498
|
+
<div class="grid grid-cols-3 gap-2">
|
|
499
|
+
<!-- eslint-disable-next-line @typescript-eslint/no-unused-vars -->
|
|
500
|
+
{#each Array(12).fill(0) as _, month (month)}
|
|
501
|
+
<button
|
|
502
|
+
type="button"
|
|
503
|
+
class={cn(
|
|
504
|
+
'flex cursor-pointer items-center justify-center rounded-md px-2 py-1 text-sm font-medium',
|
|
505
|
+
viewDate.getMonth() === month
|
|
506
|
+
? 'bg-primary-500 text-white'
|
|
507
|
+
: 'text-default-700 hover:bg-default-100'
|
|
508
|
+
)}
|
|
509
|
+
onclick={() => selectMonth(month)}>{getMonthName(month).substring(0, 3)}</button
|
|
510
|
+
>
|
|
511
|
+
{/each}
|
|
512
|
+
</div>
|
|
513
|
+
{:else if viewMode === 'years'}
|
|
514
|
+
<div class="grid grid-cols-3 gap-2">
|
|
515
|
+
<!-- eslint-disable-next-line @typescript-eslint/no-unused-vars -->
|
|
516
|
+
{#each Array(12).fill(0) as _, i (i)}
|
|
517
|
+
{@const year = viewDate.getFullYear() - 6 + i}
|
|
518
|
+
<button
|
|
519
|
+
type="button"
|
|
520
|
+
class={cn(
|
|
521
|
+
'flex cursor-pointer items-center justify-center rounded-md px-2 py-1 text-sm font-medium',
|
|
522
|
+
viewDate.getFullYear() === year
|
|
523
|
+
? 'bg-primary-500 text-white'
|
|
524
|
+
: 'text-default-700 hover:bg-default-100'
|
|
525
|
+
)}
|
|
526
|
+
onclick={() => selectYear(year)}>{year}</button
|
|
527
|
+
>
|
|
528
|
+
{/each}
|
|
529
|
+
</div>
|
|
530
|
+
{/if}
|
|
531
|
+
|
|
532
|
+
{#if startDate || endDate}
|
|
533
|
+
<div
|
|
534
|
+
class="border-default-200 text-default-500 mt-4 flex flex-wrap justify-between gap-x-4 gap-y-1 border-t pt-3 text-xs"
|
|
535
|
+
>
|
|
536
|
+
<div>{startDate ? `${startLabel}: ${formatDate(startDate)}` : ''}</div>
|
|
537
|
+
<div>{endDate ? `${endLabel}: ${formatDate(endDate)}` : ''}</div>
|
|
538
|
+
</div>
|
|
539
|
+
{/if}
|
|
540
|
+
{/snippet}
|
|
@@ -13,20 +13,53 @@
|
|
|
13
13
|
placeholder = 'Enter a number',
|
|
14
14
|
size = Size.MD,
|
|
15
15
|
class: className = '',
|
|
16
|
+
icon: LeadingIcon,
|
|
17
|
+
iconPreset,
|
|
16
18
|
units = [],
|
|
17
19
|
errors,
|
|
18
20
|
disabled = false,
|
|
19
21
|
dropdownIcon: DropdownIcon,
|
|
20
22
|
onunitchange: onUnitChange,
|
|
23
|
+
formatThousands = true,
|
|
24
|
+
locale = 'en-US',
|
|
21
25
|
testId,
|
|
22
26
|
...restProps
|
|
23
27
|
}: NumberInputProps = $props();
|
|
24
28
|
|
|
29
|
+
const showPresetIcon = $derived(!LeadingIcon && iconPreset);
|
|
30
|
+
|
|
25
31
|
let showUnitDropdown = $state(false);
|
|
26
32
|
let containerRef = $state<HTMLDivElement | null>(null);
|
|
33
|
+
let inputFocused = $state(false);
|
|
27
34
|
|
|
28
35
|
const selectedOption = $derived(units.find((u) => u.value === unit));
|
|
29
36
|
|
|
37
|
+
// Only render the dropdown trigger when there's something to switch
|
|
38
|
+
// between. A single-unit field looks like a static currency label
|
|
39
|
+
// rather than an editable selector — no chevron, no click target.
|
|
40
|
+
const hasMultipleUnits = $derived(units.length > 1);
|
|
41
|
+
|
|
42
|
+
// While focused, show the raw number so typing stays predictable
|
|
43
|
+
// (typed commas would otherwise fight the formatter). On blur we
|
|
44
|
+
// reformat with locale thousands separators for readability.
|
|
45
|
+
const displayValue = $derived(
|
|
46
|
+
value == null || Number.isNaN(value)
|
|
47
|
+
? ''
|
|
48
|
+
: inputFocused || !formatThousands
|
|
49
|
+
? String(value)
|
|
50
|
+
: value.toLocaleString(locale)
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
function handleInput(e: Event) {
|
|
54
|
+
const raw = (e.currentTarget as HTMLInputElement).value.replace(/[^0-9.-]/g, '');
|
|
55
|
+
if (raw === '' || raw === '-') {
|
|
56
|
+
value = 0;
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
const num = Number(raw);
|
|
60
|
+
if (!Number.isNaN(num)) value = num;
|
|
61
|
+
}
|
|
62
|
+
|
|
30
63
|
const tokens = $derived(formSizeTokens[size]);
|
|
31
64
|
|
|
32
65
|
const containerClass = $derived(
|
|
@@ -87,27 +120,27 @@
|
|
|
87
120
|
>
|
|
88
121
|
{/if}
|
|
89
122
|
<div class={containerClass} bind:this={containerRef}>
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
stroke-width="2"
|
|
103
|
-
d="M17 9V7a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v6a2 2 0 0 0 2 2h2m2 4h10a2 2 0 0 0 2-2v-6a2 2 0 0 0-2-2H9a2 2 0 0 0-2 2v6a2 2 0 0 0 2 2m7-5a2 2 0 1 1-4 0a2 2 0 0 1 4 0"
|
|
104
|
-
/>
|
|
105
|
-
</svg>
|
|
123
|
+
{#if LeadingIcon}
|
|
124
|
+
<span class={cn('text-default-500 flex shrink-0 items-center', tokens.padX)}>
|
|
125
|
+
<LeadingIcon class={tokens.iconSize} />
|
|
126
|
+
</span>
|
|
127
|
+
{:else if showPresetIcon}
|
|
128
|
+
<span
|
|
129
|
+
class={cn('text-default-500 flex shrink-0 items-center', tokens.padX)}
|
|
130
|
+
aria-hidden="true"
|
|
131
|
+
>
|
|
132
|
+
{@render presetIconSvg(iconPreset)}
|
|
133
|
+
</span>
|
|
134
|
+
{/if}
|
|
106
135
|
<input
|
|
107
136
|
{name}
|
|
108
137
|
id={name}
|
|
109
|
-
|
|
110
|
-
|
|
138
|
+
type="text"
|
|
139
|
+
inputmode="decimal"
|
|
140
|
+
value={displayValue}
|
|
141
|
+
oninput={handleInput}
|
|
142
|
+
onfocus={() => (inputFocused = true)}
|
|
143
|
+
onblur={() => (inputFocused = false)}
|
|
111
144
|
{placeholder}
|
|
112
145
|
{disabled}
|
|
113
146
|
class={inputClass}
|
|
@@ -115,27 +148,41 @@
|
|
|
115
148
|
{...restProps}
|
|
116
149
|
/>
|
|
117
150
|
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
{
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
<
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
151
|
+
{#if hasMultipleUnits}
|
|
152
|
+
<!-- Clickable selector: renders only when there's more than one
|
|
153
|
+
unit to pick from. Otherwise the unit is static text below. -->
|
|
154
|
+
<button
|
|
155
|
+
type="button"
|
|
156
|
+
class="hover:bg-default-100 flex items-center gap-1 rounded px-1"
|
|
157
|
+
onclick={handleUnitToggle}
|
|
158
|
+
{disabled}
|
|
159
|
+
>
|
|
160
|
+
{#if selectedOption?.icon}
|
|
161
|
+
{@const Icon = selectedOption.icon}
|
|
162
|
+
<Icon />
|
|
163
|
+
{/if}
|
|
164
|
+
<span class={cn(tokens.text)}>{unit}</span>
|
|
165
|
+
{#if DropdownIcon}
|
|
166
|
+
<DropdownIcon class={iconClass} />
|
|
167
|
+
{:else}
|
|
168
|
+
<svg class={iconClass} fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
|
169
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M19 9l-7 7-7-7" />
|
|
170
|
+
</svg>
|
|
171
|
+
{/if}
|
|
172
|
+
</button>
|
|
173
|
+
{:else if unit}
|
|
174
|
+
<!-- Static unit label — no chevron, not clickable. Matches the
|
|
175
|
+
field's text size so it sits inline. -->
|
|
176
|
+
<span class={cn('text-default-500 flex items-center gap-1 pr-2', tokens.text)}>
|
|
177
|
+
{#if selectedOption?.icon}
|
|
178
|
+
{@const Icon = selectedOption.icon}
|
|
179
|
+
<Icon />
|
|
180
|
+
{/if}
|
|
181
|
+
{unit}
|
|
182
|
+
</span>
|
|
183
|
+
{/if}
|
|
184
|
+
|
|
185
|
+
{#if showUnitDropdown && hasMultipleUnits}
|
|
139
186
|
<div class={dropdownClass}>
|
|
140
187
|
{#each units as unitOption (unitOption.value)}
|
|
141
188
|
<button
|
|
@@ -163,3 +210,65 @@
|
|
|
163
210
|
{/each}
|
|
164
211
|
{/if}
|
|
165
212
|
</div>
|
|
213
|
+
|
|
214
|
+
{#snippet presetIconSvg(preset: string | undefined)}
|
|
215
|
+
{#if preset === 'currency'}
|
|
216
|
+
<svg
|
|
217
|
+
class={tokens.iconSize}
|
|
218
|
+
viewBox="0 0 24 24"
|
|
219
|
+
fill="none"
|
|
220
|
+
stroke="currentColor"
|
|
221
|
+
stroke-width="2"
|
|
222
|
+
stroke-linecap="round"
|
|
223
|
+
stroke-linejoin="round"
|
|
224
|
+
>
|
|
225
|
+
<path
|
|
226
|
+
d="M17 9V7a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v6a2 2 0 0 0 2 2h2m2 4h10a2 2 0 0 0 2-2v-6a2 2 0 0 0-2-2H9a2 2 0 0 0-2 2v6a2 2 0 0 0 2 2m7-5a2 2 0 1 1-4 0a2 2 0 0 1 4 0"
|
|
227
|
+
/>
|
|
228
|
+
</svg>
|
|
229
|
+
{:else if preset === 'quantity'}
|
|
230
|
+
<span class={cn(tokens.iconSize, 'flex items-center justify-center font-bold')}>#</span>
|
|
231
|
+
{:else if preset === 'percentage'}
|
|
232
|
+
<svg
|
|
233
|
+
class={tokens.iconSize}
|
|
234
|
+
viewBox="0 0 24 24"
|
|
235
|
+
fill="none"
|
|
236
|
+
stroke="currentColor"
|
|
237
|
+
stroke-width="2"
|
|
238
|
+
stroke-linecap="round"
|
|
239
|
+
stroke-linejoin="round"
|
|
240
|
+
>
|
|
241
|
+
<path d="M19 5 5 19" /><circle cx="6.5" cy="6.5" r="2.5" /><circle
|
|
242
|
+
cx="17.5"
|
|
243
|
+
cy="17.5"
|
|
244
|
+
r="2.5"
|
|
245
|
+
/>
|
|
246
|
+
</svg>
|
|
247
|
+
{:else if preset === 'weight'}
|
|
248
|
+
<svg
|
|
249
|
+
class={tokens.iconSize}
|
|
250
|
+
viewBox="0 0 24 24"
|
|
251
|
+
fill="none"
|
|
252
|
+
stroke="currentColor"
|
|
253
|
+
stroke-width="2"
|
|
254
|
+
stroke-linecap="round"
|
|
255
|
+
stroke-linejoin="round"
|
|
256
|
+
>
|
|
257
|
+
<path
|
|
258
|
+
d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"
|
|
259
|
+
/><path d="M3.27 6.96 12 12.01l8.73-5.05M12 22.08V12" />
|
|
260
|
+
</svg>
|
|
261
|
+
{:else if preset === 'temperature'}
|
|
262
|
+
<svg
|
|
263
|
+
class={tokens.iconSize}
|
|
264
|
+
viewBox="0 0 24 24"
|
|
265
|
+
fill="none"
|
|
266
|
+
stroke="currentColor"
|
|
267
|
+
stroke-width="2"
|
|
268
|
+
stroke-linecap="round"
|
|
269
|
+
stroke-linejoin="round"
|
|
270
|
+
>
|
|
271
|
+
<path d="M14 14.76V3.5a2.5 2.5 0 0 0-5 0v11.26a4.5 4.5 0 1 0 5 0z" />
|
|
272
|
+
</svg>
|
|
273
|
+
{/if}
|
|
274
|
+
{/snippet}
|
|
@@ -41,7 +41,7 @@
|
|
|
41
41
|
|
|
42
42
|
const rootClass = $derived(
|
|
43
43
|
cn(
|
|
44
|
-
orientation === 'auto' ? '@container w-full' : 'w-fit',
|
|
44
|
+
orientation === 'auto' ? '@container w-full' : 'w-full sm:w-fit',
|
|
45
45
|
labelLayout === 'inline' ? 'flex flex-row items-center gap-2' : 'flex flex-col gap-2',
|
|
46
46
|
orientation === 'auto' &&
|
|
47
47
|
labelLayout === 'inline' &&
|