your personal website on atproto - mirror blento.app
at update-link-card 244 lines 8.0 kB view raw
1<script lang="ts"> 2 // @ts-nocheck 3 import { DatePicker } from 'bits-ui'; 4 import { CalendarDate, type DateValue } from '@internationalized/date'; 5 import { untrack } from 'svelte'; 6 7 let { 8 value = $bindable(''), 9 required = false, 10 minValue = '', 11 locale = 'en', 12 onSelect 13 }: { 14 value: string; 15 required?: boolean; 16 minValue?: string; 17 locale?: string; 18 onSelect?: () => void; 19 } = $props(); 20 21 let isOpen = $state(false); 22 23 const currentYear = new Date().getFullYear(); 24 const yearRange = Array.from({ length: 7 }, (_, i) => currentYear - 1 + i); 25 const today = new Date(); 26 const todayDay = today.getDate(); 27 const todayMonth = today.getMonth() + 1; 28 const todayYear = today.getFullYear(); 29 30 let internalValue: CalendarDate | undefined = $state(undefined); 31 32 function parseDateStr(str: string): CalendarDate | undefined { 33 if (!str) return undefined; 34 const [yearStr, monthStr, dayStr] = str.split('-'); 35 const year = parseInt(yearStr, 10); 36 const month = parseInt(monthStr, 10); 37 const day = parseInt(dayStr, 10); 38 if (isNaN(year) || isNaN(month) || isNaN(day)) return undefined; 39 return new CalendarDate(year, month, day); 40 } 41 42 function formatDateStr(dt: CalendarDate): string { 43 const y = String(dt.year).padStart(4, '0'); 44 const m = String(dt.month).padStart(2, '0'); 45 const d = String(dt.day).padStart(2, '0'); 46 return `${y}-${m}-${d}`; 47 } 48 49 let internalMinValue: CalendarDate | undefined = $derived.by(() => { 50 return parseDateStr(minValue); 51 }); 52 53 $effect(() => { 54 const parsed = parseDateStr(value); 55 untrack(() => { 56 if (parsed) { 57 if ( 58 !internalValue || 59 parsed.year !== internalValue.year || 60 parsed.month !== internalValue.month || 61 parsed.day !== internalValue.day 62 ) { 63 internalValue = parsed; 64 } 65 } else { 66 internalValue = undefined; 67 } 68 }); 69 }); 70 71 function handleValueChange(newVal: DateValue | undefined) { 72 if (newVal && newVal instanceof CalendarDate) { 73 internalValue = newVal; 74 value = formatDateStr(newVal); 75 } 76 } 77 78 function handleOpenChange(open: boolean) { 79 isOpen = open; 80 } 81 82 function handleOpenChangeComplete(open: boolean) { 83 if (!open && internalValue) { 84 onSelect?.(); 85 } 86 } 87</script> 88 89<DatePicker.Root 90 bind:value={internalValue} 91 onValueChange={handleValueChange} 92 onOpenChange={handleOpenChange} 93 onOpenChangeComplete={handleOpenChangeComplete} 94 minValue={internalMinValue} 95 granularity="day" 96 fixedWeeks={true} 97 weekdayFormat="short" 98 {locale} 99 {required} 100> 101 <div 102 class="border-base-300 bg-base-100 text-base-900 focus-within:border-accent-500 dark:border-base-700 dark:bg-base-800 dark:text-base-100 dark:focus-within:border-accent-400 flex items-center rounded-xl border px-2.5 py-1.5 text-sm transition-colors" 103 > 104 <DatePicker.Input> 105 {#snippet children({ segments })} 106 {#each segments as segment, i (segment.part + i)} 107 {#if segment.part === 'literal'} 108 <span class="text-base-400 dark:text-base-500">{segment.value}</span> 109 {:else} 110 <DatePicker.Segment 111 part={segment.part} 112 class="hover:bg-base-200 focus:bg-base-200 dark:hover:bg-base-700 dark:focus:bg-base-700 rounded px-0.5 focus:outline-none" 113 > 114 {segment.value} 115 </DatePicker.Segment> 116 {/if} 117 {/each} 118 {/snippet} 119 </DatePicker.Input> 120 121 <DatePicker.Trigger 122 class="text-base-400 hover:text-base-600 dark:text-base-500 dark:hover:text-base-300 ml-auto cursor-pointer pl-1.5" 123 > 124 <svg 125 xmlns="http://www.w3.org/2000/svg" 126 fill="none" 127 viewBox="0 0 24 24" 128 stroke-width="1.5" 129 stroke="currentColor" 130 class="size-4" 131 > 132 <path 133 stroke-linecap="round" 134 stroke-linejoin="round" 135 d="M6.75 3v2.25M17.25 3v2.25M3 18.75V7.5a2.25 2.25 0 0 1 2.25-2.25h13.5A2.25 2.25 0 0 1 21 7.5v11.25m-18 0A2.25 2.25 0 0 0 5.25 21h13.5A2.25 2.25 0 0 0 21 18.75m-18 0v-7.5A2.25 2.25 0 0 1 5.25 9h13.5A2.25 2.25 0 0 1 21 11.25v7.5" 136 /> 137 </svg> 138 </DatePicker.Trigger> 139 </div> 140 141 <DatePicker.Content 142 class="border-base-200 bg-base-50 dark:border-base-700 dark:bg-base-900 z-50 rounded-2xl border p-4 shadow-lg" 143 > 144 <DatePicker.Calendar> 145 {#snippet children({ months, weekdays })} 146 <DatePicker.Header class="flex items-center justify-between"> 147 <DatePicker.PrevButton 148 class="text-base-500 hover:bg-base-200 dark:text-base-400 dark:hover:bg-base-700 inline-flex size-8 items-center justify-center rounded-lg" 149 > 150 <svg 151 xmlns="http://www.w3.org/2000/svg" 152 viewBox="0 0 20 20" 153 fill="currentColor" 154 class="size-5" 155 > 156 <path 157 fill-rule="evenodd" 158 d="M11.78 5.22a.75.75 0 0 1 0 1.06L8.06 10l3.72 3.72a.75.75 0 1 1-1.06 1.06l-4.25-4.25a.75.75 0 0 1 0-1.06l4.25-4.25a.75.75 0 0 1 1.06 0Z" 159 clip-rule="evenodd" 160 /> 161 </svg> 162 </DatePicker.PrevButton> 163 164 <div class="flex items-center gap-1.5"> 165 <DatePicker.MonthSelect 166 monthFormat="long" 167 class="text-base-900 dark:text-base-100 hover:text-accent-500 dark:hover:text-accent-400 cursor-pointer border-0 bg-transparent text-sm font-medium outline-none focus:ring-0 focus:outline-none" 168 /> 169 <DatePicker.YearSelect 170 years={yearRange} 171 class="text-base-900 dark:text-base-100 hover:text-accent-500 dark:hover:text-accent-400 cursor-pointer border-0 bg-transparent text-sm font-medium outline-none focus:ring-0 focus:outline-none" 172 /> 173 </div> 174 175 <DatePicker.NextButton 176 class="text-base-500 hover:bg-base-200 dark:text-base-400 dark:hover:bg-base-700 inline-flex size-8 items-center justify-center rounded-lg" 177 > 178 <svg 179 xmlns="http://www.w3.org/2000/svg" 180 viewBox="0 0 20 20" 181 fill="currentColor" 182 class="size-5" 183 > 184 <path 185 fill-rule="evenodd" 186 d="M8.22 5.22a.75.75 0 0 1 1.06 0l4.25 4.25a.75.75 0 0 1 0 1.06l-4.25 4.25a.75.75 0 1 1-1.06-1.06L11.94 10 8.22 6.28a.75.75 0 0 1 0-1.06Z" 187 clip-rule="evenodd" 188 /> 189 </svg> 190 </DatePicker.NextButton> 191 </DatePicker.Header> 192 193 {#each months as month (month.value.month)} 194 <DatePicker.Grid class="mt-3 w-full"> 195 <DatePicker.GridHead> 196 <DatePicker.GridRow class="flex w-full"> 197 {#each weekdays as weekday, i (i)} 198 <DatePicker.HeadCell 199 class="text-base-400 dark:text-base-500 flex-1 text-center text-xs font-medium" 200 > 201 {weekday} 202 </DatePicker.HeadCell> 203 {/each} 204 </DatePicker.GridRow> 205 </DatePicker.GridHead> 206 207 <DatePicker.GridBody> 208 {#each month.weeks as week, weekIndex (weekIndex)} 209 <DatePicker.GridRow class="flex w-full"> 210 {#each week as day (day.toString())} 211 <DatePicker.Cell date={day} month={month.value} class="flex-1 p-0.5"> 212 <DatePicker.Day> 213 {#snippet children({ selected, disabled, day: dayText })} 214 <div 215 class="relative flex size-9 items-center justify-center rounded-lg text-sm 216 {selected 217 ? 'bg-accent-500 font-medium text-white' 218 : disabled 219 ? 'text-base-300 dark:text-base-600 pointer-events-none' 220 : day.month !== month.value.month 221 ? 'text-base-300 dark:text-base-600' 222 : 'text-base-700 hover:bg-base-200 dark:text-base-300 dark:hover:bg-base-700'}" 223 > 224 {dayText} 225 {#if day.day === todayDay && day.month === todayMonth && day.year === todayYear} 226 <span 227 class="bg-accent-500 absolute bottom-1 left-1/2 size-1 -translate-x-1/2 rounded-full" 228 class:bg-white={selected} 229 ></span> 230 {/if} 231 </div> 232 {/snippet} 233 </DatePicker.Day> 234 </DatePicker.Cell> 235 {/each} 236 </DatePicker.GridRow> 237 {/each} 238 </DatePicker.GridBody> 239 </DatePicker.Grid> 240 {/each} 241 {/snippet} 242 </DatePicker.Calendar> 243 </DatePicker.Content> 244</DatePicker.Root>