@makolabs/ripple 3.0.0 → 3.0.2

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 (34) hide show
  1. package/dist/elements/accordion/Accordion.svelte +1 -1
  2. package/dist/elements/combobox/ComboBox.svelte +59 -29
  3. package/dist/elements/dropdown/Select.svelte +98 -62
  4. package/dist/elements/dropdown/select.d.ts +3 -108
  5. package/dist/elements/dropdown/select.js +37 -46
  6. package/dist/elements/popover/Popover.svelte +59 -36
  7. package/dist/filters/CompactFilters.svelte +1 -1
  8. package/dist/forms/Checkbox.svelte +24 -9
  9. package/dist/forms/DateRange.svelte +236 -204
  10. package/dist/forms/Input.svelte +18 -18
  11. package/dist/forms/MarketSelector.svelte +1 -1
  12. package/dist/forms/NumberInput.svelte +160 -55
  13. package/dist/forms/RadioGroup.svelte +6 -2
  14. package/dist/forms/SegmentedControl.svelte +1 -1
  15. package/dist/forms/Tags.svelte +32 -11
  16. package/dist/forms/Textarea.svelte +8 -13
  17. package/dist/forms/Toggle.svelte +22 -14
  18. package/dist/forms/calendar/Calendar.svelte +107 -8
  19. package/dist/forms/calendar/calendar-types.d.ts +8 -0
  20. package/dist/forms/date-picker/DatePicker.svelte +11 -14
  21. package/dist/forms/form-size.d.ts +37 -0
  22. package/dist/forms/form-size.js +67 -0
  23. package/dist/forms/form-types.d.ts +33 -0
  24. package/dist/forms/month-picker/MonthPicker.svelte +299 -0
  25. package/dist/forms/month-picker/MonthPicker.svelte.d.ts +4 -0
  26. package/dist/forms/month-picker/month-picker-types.d.ts +22 -0
  27. package/dist/forms/month-picker/month-picker-types.js +1 -0
  28. package/dist/forms/segmented-control.d.ts +2 -2
  29. package/dist/forms/segmented-control.js +18 -15
  30. package/dist/forms/slider.js +35 -28
  31. package/dist/index.d.ts +2 -0
  32. package/dist/index.js +1 -0
  33. package/dist/layout/activity-list/ActivityList.svelte +1 -1
  34. package/package.json +1 -1
@@ -1,9 +1,12 @@
1
1
  <script lang="ts">
2
2
  import { cn } from '../helper/cls.js';
3
+ import { Size } from '../variants.js';
4
+ import { formSizeTokens } from './form-size.js';
3
5
  import type { DateRangeProps } from '../index.js';
4
6
  import Portal from '../utils/Portal.svelte';
5
7
  import { fly } from 'svelte/transition';
6
8
  import { quintOut } from 'svelte/easing';
9
+ import { onMount } from 'svelte';
7
10
 
8
11
  let {
9
12
  startDate = $bindable(),
@@ -17,11 +20,24 @@
17
20
  endLabel = 'End date',
18
21
  format = 'MM/dd/yyyy',
19
22
  errors = [],
23
+ size = Size.MD,
20
24
  id,
21
25
  name,
22
26
  onselect
23
27
  }: DateRangeProps = $props();
24
28
 
29
+ const tokens = $derived(formSizeTokens[size]);
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
+
25
41
  let isOpen = $state(false);
26
42
  let hoveredDate = $state<Date | null>(null);
27
43
  let datePickerRef = $state<HTMLDivElement | null>(null);
@@ -239,7 +255,12 @@
239
255
  {id}
240
256
  type="button"
241
257
  class={cn(
242
- 'border-default-300 flex w-full items-center justify-between rounded-lg border bg-white px-3 py-2 text-sm shadow-xs',
258
+ 'border-default-300 flex w-full items-center justify-between border bg-white',
259
+ tokens.height,
260
+ tokens.padX,
261
+ tokens.text,
262
+ tokens.radius,
263
+ tokens.shadow,
243
264
  disabled
244
265
  ? 'bg-default-100 text-default-400 cursor-not-allowed opacity-50'
245
266
  : errors?.length
@@ -259,7 +280,7 @@
259
280
  ? `${formatDate(startDate)} - Select end date`
260
281
  : placeholder}
261
282
  </span>
262
- <svg class="text-default-400 h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
283
+ <svg class={cn('text-default-400', tokens.iconSize)} viewBox="0 0 20 20" fill="currentColor">
263
284
  <path
264
285
  fill-rule="evenodd"
265
286
  d="M6 2a1 1 0 00-1 1v1H4a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2h-1V3a1 1 0 10-2 0v1H7V3a1 1 0 00-1-1zm0 5a1 1 0 000 2h8a1 1 0 100-2H6z"
@@ -271,11 +292,16 @@
271
292
  {#if startDate || endDate}
272
293
  <button
273
294
  type="button"
274
- class="text-default-400 hover:text-default-500 absolute top-1/2 right-10 -translate-y-1/2"
295
+ class={cn(
296
+ 'text-default-400 hover:text-default-500 absolute top-1/2 -translate-y-1/2',
297
+ // Sit just left of the calendar icon; use the token gap
298
+ // so the clear button stays visually tied to the icon.
299
+ 'right-8'
300
+ )}
275
301
  onclick={clearDates}
276
302
  aria-label="Clear dates"
277
303
  >
278
- <svg class="h-4 w-4" viewBox="0 0 20 20" fill="currentColor">
304
+ <svg class={cn(tokens.iconSize)} viewBox="0 0 20 20" fill="currentColor">
279
305
  <path
280
306
  fill-rule="evenodd"
281
307
  d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z"
@@ -294,213 +320,219 @@
294
320
  </div>
295
321
  {/if}
296
322
 
297
- {#if isOpen}
323
+ {#if isOpen && !isMobile}
298
324
  <Portal target={datePickerRef}>
299
325
  <div
300
326
  bind:this={calendarRef}
301
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"
302
328
  transition:fly={{ y: -8, duration: 300, easing: quintOut }}
303
329
  >
304
- <div class="mb-2 flex items-center justify-between">
305
- {#if viewMode === 'days'}
306
- <button
307
- type="button"
308
- aria-label="Previous month"
309
- class="text-default-500 hover:bg-default-100 inline-flex items-center rounded-md p-1 text-sm"
310
- onclick={prevMonth}
311
- >
312
- <svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
313
- <path
314
- fill-rule="evenodd"
315
- 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"
316
- clip-rule="evenodd"
317
- />
318
- </svg>
319
- </button>
320
- <button
321
- type="button"
322
- class="text-default-700 hover:bg-default-100 inline-flex items-center rounded-md px-2 py-1 text-sm font-medium"
323
- onclick={showMonths}
324
- >
325
- {getMonthName(viewDate.getMonth())}
326
- {viewDate.getFullYear()}
327
- </button>
328
- <button
329
- type="button"
330
- aria-label="Next month"
331
- class="text-default-500 hover:bg-default-100 inline-flex items-center rounded-md p-1 text-sm"
332
- onclick={nextMonth}
333
- >
334
- <svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
335
- <path
336
- fill-rule="evenodd"
337
- 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"
338
- clip-rule="evenodd"
339
- />
340
- </svg>
341
- </button>
342
- {:else if viewMode === 'months'}
343
- <button
344
- type="button"
345
- aria-label="Previous year"
346
- class="text-default-500 hover:bg-default-100 inline-flex items-center rounded-md p-1 text-sm"
347
- onclick={() =>
348
- (viewDate = new Date(viewDate.getFullYear() - 1, viewDate.getMonth(), 1))}
349
- >
350
- <svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
351
- <path
352
- fill-rule="evenodd"
353
- 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"
354
- clip-rule="evenodd"
355
- />
356
- </svg>
357
- </button>
358
- <button
359
- type="button"
360
- aria-label="Current year"
361
- class="text-default-700 inline-flex items-center rounded-md px-2 py-1 text-sm font-medium"
362
- onclick={showYears}
363
- >
364
- {viewDate.getFullYear()}
365
- </button>
366
- <button
367
- type="button"
368
- aria-label="Next year"
369
- class="text-default-500 hover:bg-default-100 inline-flex items-center rounded-md p-1 text-sm"
370
- onclick={() =>
371
- (viewDate = new Date(viewDate.getFullYear() + 1, viewDate.getMonth(), 1))}
372
- >
373
- <svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
374
- <path
375
- fill-rule="evenodd"
376
- 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"
377
- clip-rule="evenodd"
378
- />
379
- </svg>
380
- </button>
381
- {:else if viewMode === 'years'}
382
- <button
383
- type="button"
384
- aria-label="Previous year"
385
- class="text-default-500 hover:bg-default-100 inline-flex items-center rounded-md p-1 text-sm"
386
- onclick={() =>
387
- (viewDate = new Date(viewDate.getFullYear() - 12, viewDate.getMonth(), 1))}
388
- >
389
- <svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
390
- <path
391
- fill-rule="evenodd"
392
- 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"
393
- clip-rule="evenodd"
394
- />
395
- </svg>
396
- </button>
397
- <button
398
- type="button"
399
- aria-label="Current year range"
400
- class="text-default-700 inline-flex items-center rounded-md px-2 py-1 text-sm font-medium"
401
- >
402
- {viewDate.getFullYear() - 6} - {viewDate.getFullYear() + 5}
403
- </button>
404
- <button
405
- type="button"
406
- aria-label="Next year"
407
- class="text-default-500 hover:bg-default-100 inline-flex items-center rounded-md p-1 text-sm"
408
- onclick={() =>
409
- (viewDate = new Date(viewDate.getFullYear() + 12, viewDate.getMonth(), 1))}
410
- >
411
- <svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor">
412
- <path
413
- fill-rule="evenodd"
414
- 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"
415
- clip-rule="evenodd"
416
- />
417
- </svg>
418
- </button>
419
- {/if}
420
- </div>
421
-
422
- {#if viewMode === 'days'}
423
- <div class="text-default-500 mb-1 grid grid-cols-7 gap-1 text-center text-xs font-medium">
424
- <div>Su</div>
425
- <div>Mo</div>
426
- <div>Tu</div>
427
- <div>We</div>
428
- <div>Th</div>
429
- <div>Fr</div>
430
- <div>Sa</div>
431
- </div>
432
- <div class="grid grid-cols-7 gap-1">
433
- {#each getDaysInMonth(viewDate) as { date, isCurrentMonth, isToday, isSelected, isInRange, isDisabled } (date.getTime())}
434
- <button
435
- type="button"
436
- class={cn(
437
- 'flex h-8 w-8 items-center justify-center rounded-full text-sm font-medium',
438
- isDisabled ? 'text-default-300 cursor-not-allowed' : 'hover:bg-default-100',
439
- isSelected ? 'bg-primary-500 hover:bg-primary-600 text-white' : '',
440
- isInRange && !isSelected ? 'bg-primary-100 text-primary-800' : '',
441
- !isCurrentMonth && !isSelected && !isInRange ? 'text-default-400' : '',
442
- isToday && !isSelected ? 'border-primary-500 border' : ''
443
- )}
444
- onclick={() => handleDateClick(date)}
445
- onmouseenter={() => handleDateHover(date)}
446
- disabled={isDisabled}
447
- >
448
- {date.getDate()}
449
- </button>
450
- {/each}
451
- </div>
452
- {:else if viewMode === 'months'}
453
- <div class="grid grid-cols-3 gap-2">
454
- <!-- eslint-disable-next-line @typescript-eslint/no-unused-vars -->
455
- {#each Array(12).fill(0) as _, month (month)}
456
- <button
457
- type="button"
458
- class={cn(
459
- 'flex items-center justify-center rounded-md px-2 py-1 text-sm font-medium',
460
- viewDate.getMonth() === month
461
- ? 'bg-primary-500 text-white'
462
- : 'text-default-700 hover:bg-default-100'
463
- )}
464
- onclick={() => selectMonth(month)}
465
- >
466
- {getMonthName(month).substring(0, 3)}
467
- </button>
468
- {/each}
469
- </div>
470
- {:else if viewMode === 'years'}
471
- <div class="grid grid-cols-3 gap-2">
472
- <!-- eslint-disable-next-line @typescript-eslint/no-unused-vars -->
473
- {#each Array(12).fill(0) as _, i (i)}
474
- {@const year = viewDate.getFullYear() - 6 + i}
475
- <button
476
- type="button"
477
- class={cn(
478
- 'flex items-center justify-center rounded-md px-2 py-1 text-sm font-medium',
479
- viewDate.getFullYear() === year
480
- ? 'bg-primary-500 text-white'
481
- : 'text-default-700 hover:bg-default-100'
482
- )}
483
- onclick={() => selectYear(year)}
484
- >
485
- {year}
486
- </button>
487
- {/each}
488
- </div>
489
- {/if}
490
-
491
- {#if startDate || endDate}
492
- <div
493
- 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"
494
- >
495
- <div>
496
- {startDate ? `${startLabel}: ${formatDate(startDate)}` : ''}
497
- </div>
498
- <div>
499
- {endDate ? `${endLabel}: ${formatDate(endDate)}` : ''}
500
- </div>
501
- </div>
502
- {/if}
330
+ {@render calendarContent()}
503
331
  </div>
504
332
  </Portal>
505
333
  {/if}
334
+
335
+ {#if isOpen && isMobile}
336
+ <button
337
+ type="button"
338
+ class="fixed inset-0 z-[9998] bg-black/40 backdrop-blur-sm"
339
+ aria-label="Close"
340
+ onclick={() => (isOpen = false)}
341
+ ></button>
342
+ <div
343
+ 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"
344
+ transition:fly={{ y: 300, duration: 200, easing: quintOut }}
345
+ bind:this={calendarRef}
346
+ >
347
+ <div class="flex justify-center py-2">
348
+ <div class="bg-default-300 h-1 w-8 rounded-full"></div>
349
+ </div>
350
+ <div class="flex-1 cursor-pointer overflow-y-auto p-4">
351
+ {@render calendarContent()}
352
+ </div>
353
+ </div>
354
+ {/if}
506
355
  </div>
356
+
357
+ {#snippet calendarContent()}
358
+ <div class="mb-2 flex items-center justify-between">
359
+ {#if viewMode === 'days'}
360
+ <button
361
+ type="button"
362
+ aria-label="Previous month"
363
+ class="text-default-500 hover:bg-default-100 inline-flex cursor-pointer items-center rounded-md p-1 text-sm"
364
+ onclick={prevMonth}
365
+ >
366
+ <svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor"
367
+ ><path
368
+ fill-rule="evenodd"
369
+ 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"
370
+ clip-rule="evenodd"
371
+ /></svg
372
+ >
373
+ </button>
374
+ <button
375
+ type="button"
376
+ class="text-default-700 hover:bg-default-100 inline-flex cursor-pointer items-center rounded-md px-2 py-1 text-sm font-medium"
377
+ onclick={showMonths}
378
+ >
379
+ {getMonthName(viewDate.getMonth())}
380
+ {viewDate.getFullYear()}
381
+ </button>
382
+ <button
383
+ type="button"
384
+ aria-label="Next month"
385
+ class="text-default-500 hover:bg-default-100 inline-flex cursor-pointer items-center rounded-md p-1 text-sm"
386
+ onclick={nextMonth}
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
+ /></svg
394
+ >
395
+ </button>
396
+ {:else if viewMode === 'months'}
397
+ <button
398
+ type="button"
399
+ aria-label="Previous year"
400
+ class="text-default-500 hover:bg-default-100 inline-flex cursor-pointer items-center rounded-md p-1 text-sm"
401
+ onclick={() => (viewDate = new Date(viewDate.getFullYear() - 1, viewDate.getMonth(), 1))}
402
+ >
403
+ <svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor"
404
+ ><path
405
+ fill-rule="evenodd"
406
+ 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"
407
+ clip-rule="evenodd"
408
+ /></svg
409
+ >
410
+ </button>
411
+ <button
412
+ type="button"
413
+ class="text-default-700 inline-flex cursor-pointer items-center rounded-md px-2 py-1 text-sm font-medium"
414
+ onclick={showYears}>{viewDate.getFullYear()}</button
415
+ >
416
+ <button
417
+ type="button"
418
+ aria-label="Next year"
419
+ class="text-default-500 hover:bg-default-100 inline-flex cursor-pointer items-center rounded-md p-1 text-sm"
420
+ onclick={() => (viewDate = new Date(viewDate.getFullYear() + 1, viewDate.getMonth(), 1))}
421
+ >
422
+ <svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor"
423
+ ><path
424
+ fill-rule="evenodd"
425
+ 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"
426
+ clip-rule="evenodd"
427
+ /></svg
428
+ >
429
+ </button>
430
+ {:else if viewMode === 'years'}
431
+ <button
432
+ type="button"
433
+ aria-label="Previous years"
434
+ class="text-default-500 hover:bg-default-100 inline-flex cursor-pointer items-center rounded-md p-1 text-sm"
435
+ onclick={() => (viewDate = new Date(viewDate.getFullYear() - 12, viewDate.getMonth(), 1))}
436
+ >
437
+ <svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor"
438
+ ><path
439
+ fill-rule="evenodd"
440
+ 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"
441
+ clip-rule="evenodd"
442
+ /></svg
443
+ >
444
+ </button>
445
+ <button
446
+ type="button"
447
+ class="text-default-700 inline-flex items-center rounded-md px-2 py-1 text-sm font-medium"
448
+ >{viewDate.getFullYear() - 6} - {viewDate.getFullYear() + 5}</button
449
+ >
450
+ <button
451
+ type="button"
452
+ aria-label="Next years"
453
+ class="text-default-500 hover:bg-default-100 inline-flex cursor-pointer items-center rounded-md p-1 text-sm"
454
+ onclick={() => (viewDate = new Date(viewDate.getFullYear() + 12, viewDate.getMonth(), 1))}
455
+ >
456
+ <svg class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor"
457
+ ><path
458
+ fill-rule="evenodd"
459
+ 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"
460
+ clip-rule="evenodd"
461
+ /></svg
462
+ >
463
+ </button>
464
+ {/if}
465
+ </div>
466
+
467
+ {#if viewMode === 'days'}
468
+ <div class="text-default-500 mb-1 grid grid-cols-7 gap-1 text-center text-xs font-medium">
469
+ <div>Su</div>
470
+ <div>Mo</div>
471
+ <div>Tu</div>
472
+ <div>We</div>
473
+ <div>Th</div>
474
+ <div>Fr</div>
475
+ <div>Sa</div>
476
+ </div>
477
+ <div class="grid grid-cols-7 gap-1">
478
+ {#each getDaysInMonth(viewDate) as { date, isCurrentMonth, isToday, isSelected, isInRange, isDisabled } (date.getTime())}
479
+ <button
480
+ type="button"
481
+ class={cn(
482
+ 'flex h-8 w-8 cursor-pointer items-center justify-center rounded-full text-sm font-medium',
483
+ isDisabled ? 'text-default-300 cursor-not-allowed' : 'hover:bg-default-100',
484
+ isSelected ? 'bg-primary-500 hover:bg-primary-600 text-white' : '',
485
+ isInRange && !isSelected ? 'bg-primary-100 text-primary-800' : '',
486
+ !isCurrentMonth && !isSelected && !isInRange ? 'text-default-400' : '',
487
+ isToday && !isSelected ? 'border-primary-500 border' : ''
488
+ )}
489
+ onclick={() => handleDateClick(date)}
490
+ onmouseenter={() => handleDateHover(date)}
491
+ disabled={isDisabled}>{date.getDate()}</button
492
+ >
493
+ {/each}
494
+ </div>
495
+ {:else if viewMode === 'months'}
496
+ <div class="grid grid-cols-3 gap-2">
497
+ <!-- eslint-disable-next-line @typescript-eslint/no-unused-vars -->
498
+ {#each Array(12).fill(0) as _, month (month)}
499
+ <button
500
+ type="button"
501
+ class={cn(
502
+ 'flex cursor-pointer items-center justify-center rounded-md px-2 py-1 text-sm font-medium',
503
+ viewDate.getMonth() === month
504
+ ? 'bg-primary-500 text-white'
505
+ : 'text-default-700 hover:bg-default-100'
506
+ )}
507
+ onclick={() => selectMonth(month)}>{getMonthName(month).substring(0, 3)}</button
508
+ >
509
+ {/each}
510
+ </div>
511
+ {:else if viewMode === 'years'}
512
+ <div class="grid grid-cols-3 gap-2">
513
+ <!-- eslint-disable-next-line @typescript-eslint/no-unused-vars -->
514
+ {#each Array(12).fill(0) as _, i (i)}
515
+ {@const year = viewDate.getFullYear() - 6 + i}
516
+ <button
517
+ type="button"
518
+ class={cn(
519
+ 'flex cursor-pointer items-center justify-center rounded-md px-2 py-1 text-sm font-medium',
520
+ viewDate.getFullYear() === year
521
+ ? 'bg-primary-500 text-white'
522
+ : 'text-default-700 hover:bg-default-100'
523
+ )}
524
+ onclick={() => selectYear(year)}>{year}</button
525
+ >
526
+ {/each}
527
+ </div>
528
+ {/if}
529
+
530
+ {#if startDate || endDate}
531
+ <div
532
+ 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"
533
+ >
534
+ <div>{startDate ? `${startLabel}: ${formatDate(startDate)}` : ''}</div>
535
+ <div>{endDate ? `${endLabel}: ${formatDate(endDate)}` : ''}</div>
536
+ </div>
537
+ {/if}
538
+ {/snippet}
@@ -2,6 +2,7 @@
2
2
  import { cn } from '../helper/cls.js';
3
3
  import { buildTestId } from '../helper/testid.js';
4
4
  import { Size } from '../variants.js';
5
+ import { formSizeTokens } from './form-size.js';
5
6
  import type { InputProps } from '../index.js';
6
7
 
7
8
  let {
@@ -20,26 +21,25 @@
20
21
  }: InputProps = $props();
21
22
 
22
23
  const BASIC_TYPES = ['text', 'email', 'password', 'number', 'tel', 'url', 'date', 'textarea'];
24
+ const tokens = $derived(formSizeTokens[size]);
23
25
  const inputClasses = $derived(
24
26
  cn(
25
- 'transition-colors placeholder:text-default-400',
26
- {
27
- 'border rounded-lg shadow-xs w-full bg-white px-3 py-2 text-sm focus-within:outline-none focus-within:ring-2 focus-within:ring-offset-2':
28
- BASIC_TYPES.includes(type),
29
- 'w-full bg-white px-3 py-2 text-sm resize-y min-h-[100px]': type === 'textarea',
30
- 'border-danger-300 focus-within:border-danger-500 focus-within:ring-danger-500':
31
- errors.length,
32
- 'opacity-50 cursor-not-allowed': disabled,
33
- ...(BASIC_TYPES.includes(type)
34
- ? {
35
- 'h-8 text-sm': size === Size.SM,
36
- 'h-10 text-base': size === Size.MD,
37
- 'h-12 text-lg': size === Size.LG
38
- }
39
- : {}),
40
- 'border-default-300 focus-within:border-primary-500 focus-within:ring-primary-500':
41
- !errors.length
42
- },
27
+ 'w-full bg-white transition-colors placeholder:text-default-400',
28
+ tokens.padX,
29
+ tokens.text,
30
+ // All basic types (including textarea) get the bordered look.
31
+ BASIC_TYPES.includes(type) && [
32
+ 'border focus-within:outline-none focus-within:ring-2 focus-within:ring-offset-2',
33
+ tokens.radius,
34
+ tokens.shadow
35
+ ],
36
+ // Single-line types get a fixed height; textarea is content-driven.
37
+ type !== 'textarea' && tokens.height,
38
+ type === 'textarea' && ['resize-y min-h-[100px]', tokens.padY],
39
+ errors.length
40
+ ? 'border-danger-300 focus-within:border-danger-500 focus-within:ring-danger-500'
41
+ : 'border-default-300 focus-within:border-primary-500 focus-within:ring-primary-500',
42
+ disabled && 'opacity-50 cursor-not-allowed',
43
43
  className
44
44
  )
45
45
  );
@@ -19,7 +19,7 @@
19
19
  labelLayout = 'inline',
20
20
  labelClass = undefined,
21
21
  color = Color.PRIMARY,
22
- size = Size.SM,
22
+ size = Size.MD,
23
23
  compact = false,
24
24
  flagsOnly = false,
25
25
  disabled = false,