@intinyagroup/calendar 0.1.1-alpha.8 → 0.1.1

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.
@@ -1,7 +1,7 @@
1
1
  <script lang="ts">
2
- import { ChevronLeft, ChevronRight } from 'lucide-svelte';
3
- import { Button } from '@intinyagroup/ui';
4
- import { cn } from '@intinyagroup/ui/utils';
2
+ import { ChevronLeft, ChevronRight } from "lucide-svelte";
3
+ import { Button } from "@intinyagroup/ui";
4
+ import { cn } from "@intinyagroup/ui/utils";
5
5
 
6
6
  export type CalendarDate = { year: number; month: number; day: number };
7
7
 
@@ -25,13 +25,27 @@
25
25
  let viewYear = $state(value?.year ?? today.getFullYear());
26
26
  let viewMonth = $state(value?.month ?? today.getMonth());
27
27
 
28
- const monthNames = ['January','February','March','April','May','June','July','August','September','October','November','December'];
29
- const dayNames = ['Su','Mo','Tu','We','Th','Fr','Sa'];
28
+ const monthNames = [
29
+ "January",
30
+ "February",
31
+ "March",
32
+ "April",
33
+ "May",
34
+ "June",
35
+ "July",
36
+ "August",
37
+ "September",
38
+ "October",
39
+ "November",
40
+ "December",
41
+ ];
42
+ const dayNames = ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"];
30
43
  const daysInMonth = $derived(new Date(viewYear, viewMonth + 1, 0).getDate());
31
44
  const firstDayOfMonth = $derived(new Date(viewYear, viewMonth, 1).getDay());
32
45
 
33
46
  const calendarDays = $derived.by(() => {
34
- const days: { day: number; currentMonth: boolean; date: CalendarDate }[] = [];
47
+ const days: { day: number; currentMonth: boolean; date: CalendarDate }[] =
48
+ [];
35
49
  const prevMonthDays = new Date(viewYear, viewMonth, 0).getDate();
36
50
  for (let i = firstDayOfMonth - 1; i >= 0; i--) {
37
51
  const day = prevMonthDays - i;
@@ -40,7 +54,11 @@
40
54
  days.push({ day, currentMonth: false, date: { year, month, day } });
41
55
  }
42
56
  for (let day = 1; day <= daysInMonth; day++) {
43
- days.push({ day, currentMonth: true, date: { year: viewYear, month: viewMonth, day } });
57
+ days.push({
58
+ day,
59
+ currentMonth: true,
60
+ date: { year: viewYear, month: viewMonth, day },
61
+ });
44
62
  }
45
63
  const remaining = 42 - days.length;
46
64
  for (let day = 1; day <= remaining; day++) {
@@ -55,48 +73,142 @@
55
73
  return a?.year === b.year && a?.month === b.month && a?.day === b.day;
56
74
  }
57
75
  function isToday(date: CalendarDate) {
58
- return date.year === today.getFullYear() && date.month === today.getMonth() && date.day === today.getDate();
76
+ return (
77
+ date.year === today.getFullYear() &&
78
+ date.month === today.getMonth() &&
79
+ date.day === today.getDate()
80
+ );
59
81
  }
60
82
  function isDisabled(date: CalendarDate) {
61
- if (min && (date.year < min.year || (date.year === min.year && date.month < min.month) || (date.year === min.year && date.month === min.month && date.day < min.day))) return true;
62
- if (max && (date.year > max.year || (date.year === max.year && date.month > max.month) || (date.year === max.year && date.month === max.month && date.day > max.day))) return true;
83
+ if (
84
+ min &&
85
+ (date.year < min.year ||
86
+ (date.year === min.year && date.month < min.month) ||
87
+ (date.year === min.year &&
88
+ date.month === min.month &&
89
+ date.day < min.day))
90
+ )
91
+ return true;
92
+ if (
93
+ max &&
94
+ (date.year > max.year ||
95
+ (date.year === max.year && date.month > max.month) ||
96
+ (date.year === max.year &&
97
+ date.month === max.month &&
98
+ date.day > max.day))
99
+ )
100
+ return true;
63
101
  return false;
64
102
  }
65
- function prevMonth() { if (viewMonth === 0) { viewMonth = 11; viewYear--; } else { viewMonth--; } }
66
- function nextMonth() { if (viewMonth === 11) { viewMonth = 0; viewYear++; } else { viewMonth++; } }
67
- function selectDate(date: CalendarDate) { if (disabled || isDisabled(date)) return; value = date; onSelect?.(date); }
68
- function goToday() { viewYear = today.getFullYear(); viewMonth = today.getMonth(); selectDate({ year: today.getFullYear(), month: today.getMonth(), day: today.getDate() }); }
103
+ function prevMonth() {
104
+ if (viewMonth === 0) {
105
+ viewMonth = 11;
106
+ viewYear--;
107
+ } else {
108
+ viewMonth--;
109
+ }
110
+ }
111
+ function nextMonth() {
112
+ if (viewMonth === 11) {
113
+ viewMonth = 0;
114
+ viewYear++;
115
+ } else {
116
+ viewMonth++;
117
+ }
118
+ }
119
+ function selectDate(date: CalendarDate) {
120
+ if (disabled || isDisabled(date)) return;
121
+ value = date;
122
+ onSelect?.(date);
123
+ }
124
+ function goToday() {
125
+ viewYear = today.getFullYear();
126
+ viewMonth = today.getMonth();
127
+ selectDate({
128
+ year: today.getFullYear(),
129
+ month: today.getMonth(),
130
+ day: today.getDate(),
131
+ });
132
+ }
69
133
  </script>
70
134
 
71
- <div class={cn('inline-flex flex-col rounded-xl border border-[var(--ui-border)] bg-[var(--ui-card)] p-4 shadow-sm', className)}>
72
- <div class="flex items-center justify-between mb-4">
73
- <Button variant="ghost" size="sm" onclick={prevMonth} {disabled}><ChevronLeft class="size-4" /></Button>
74
- <div class="flex items-center gap-2">
75
- <span class="text-sm font-semibold text-[var(--ui-foreground)]">{monthNames[viewMonth]} {viewYear}</span>
76
- <Button variant="ghost" size="sm" onclick={goToday} {disabled} class="text-xs h-6 px-2">Today</Button>
135
+ <div
136
+ class={cn(
137
+ "inline-flex flex-col rounded-xl border border-[var(--ui-border)] bg-[var(--ui-card)] p-4 shadow-sm select-none",
138
+ className,
139
+ )}
140
+ style="min-width: 280px; max-width: 320px; box-sizing: border-box;"
141
+ >
142
+ <div
143
+ class="flex items-center justify-between mb-4"
144
+ style="display: flex; align-items: center; justify-content: space-between; margin-bottom: 1rem;"
145
+ >
146
+ <Button variant="ghost" size="sm" onclick={prevMonth} {disabled}
147
+ ><ChevronLeft class="size-4" /></Button
148
+ >
149
+ <div
150
+ class="flex items-center gap-2"
151
+ style="display: flex; align-items: center; gap: 0.5rem;"
152
+ >
153
+ <span class="text-sm font-semibold text-[var(--ui-foreground)]"
154
+ >{monthNames[viewMonth]} {viewYear}</span
155
+ >
156
+ <Button
157
+ variant="ghost"
158
+ size="sm"
159
+ onclick={goToday}
160
+ {disabled}
161
+ class="text-xs h-6 px-2">Today</Button
162
+ >
77
163
  </div>
78
- <Button variant="ghost" size="sm" onclick={nextMonth} {disabled}><ChevronRight class="size-4" /></Button>
164
+ <Button variant="ghost" size="sm" onclick={nextMonth} {disabled}
165
+ ><ChevronRight class="size-4" /></Button
166
+ >
79
167
  </div>
80
168
 
81
- <div class="grid grid-cols-7 gap-1 mb-2">
82
- {#each dayNames as d}<div class="text-center text-[11px] font-medium text-[var(--ui-muted-foreground)] py-1">{d}</div>{/each}
169
+ <div
170
+ class="grid grid-cols-7 gap-1 mb-2 text-center"
171
+ style="display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); gap: 0.25rem; margin-bottom: 0.5rem; text-align: center;"
172
+ >
173
+ {#each dayNames as d}
174
+ <div
175
+ class="text-center text-[11px] font-medium text-[var(--ui-muted-foreground)] py-1"
176
+ style="text-align: center; font-size: 0.6875rem; padding: 0.25rem 0;"
177
+ >
178
+ {d}
179
+ </div>
180
+ {/each}
83
181
  </div>
84
182
 
85
- <div class="grid grid-cols-7 gap-1">
86
- {#each calendarDays as { day, currentMonth, date } (date.year + '-' + date.month + '-' + date.day)}
183
+ <div
184
+ class="grid grid-cols-7 gap-1"
185
+ style="display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); gap: 0.25rem;"
186
+ >
187
+ {#each calendarDays as { day, currentMonth, date } (date.year + "-" + date.month + "-" + date.day)}
87
188
  {@const isSelected = isSameDate(value, date)}
88
189
  {@const isTodayDate = isToday(date)}
89
190
  {@const isDisabledDate = isDisabled(date)}
90
191
  <button
192
+ type="button"
91
193
  onclick={() => selectDate(date)}
92
194
  disabled={disabled || isDisabledDate}
195
+ style="width: 2.25rem; height: 2.25rem; display: inline-flex; align-items: center; justify-content: center; box-sizing: border-box;"
93
196
  class="flex items-center justify-center size-9 rounded-lg text-sm transition-colors cursor-pointer
94
- {isSelected ? 'bg-[var(--ui-primary)] text-[var(--ui-primary-foreground)] font-semibold' : ''}
95
- {!isSelected && currentMonth ? 'text-[var(--ui-foreground)] hover:bg-[var(--ui-secondary)]' : ''}
96
- {!isSelected && !currentMonth ? 'text-[var(--ui-muted-foreground)]/40' : ''}
97
- {isTodayDate && !isSelected ? 'ring-2 ring-[var(--ui-primary)]/30 font-medium' : ''}
197
+ {isSelected
198
+ ? 'bg-[var(--ui-primary)] text-[var(--ui-primary-foreground)] font-semibold'
199
+ : ''}
200
+ {!isSelected && currentMonth
201
+ ? 'text-[var(--ui-foreground)] hover:bg-[var(--ui-secondary)]'
202
+ : ''}
203
+ {!isSelected && !currentMonth
204
+ ? 'text-[var(--ui-muted-foreground)]/40'
205
+ : ''}
206
+ {isTodayDate && !isSelected
207
+ ? 'ring-2 ring-[var(--ui-primary)]/30 font-medium'
208
+ : ''}
98
209
  {isDisabledDate || disabled ? 'opacity-40 cursor-not-allowed' : ''}"
99
- >{day}</button>
210
+ >{day}</button
211
+ >
100
212
  {/each}
101
213
  </div>
102
214
  </div>
@@ -1,13 +1,23 @@
1
1
  <script lang="ts">
2
- import * as Popover from '@intinyagroup/ui';
3
- import { Button, Input } from '@intinyagroup/ui';
4
- import { Calendar, type CalendarDate } from '@intinyagroup/calendar';
5
- import { Calendar as CalendarIcon, X } from 'lucide-svelte';
6
- import { cn } from '@intinyagroup/grid-core/utils';
2
+ import {
3
+ Popover as PopoverRoot,
4
+ PopoverTrigger,
5
+ PopoverContent,
6
+ Button,
7
+ Input,
8
+ } from "@intinyagroup/ui";
9
+ const Popover = {
10
+ Root: PopoverRoot,
11
+ Trigger: PopoverTrigger,
12
+ Content: PopoverContent,
13
+ };
14
+ import Calendar, { type CalendarDate } from "./Calendar.svelte";
15
+ import { Calendar as CalendarIcon, X } from "lucide-svelte";
16
+ import { cn } from "@intinyagroup/ui/utils";
7
17
 
8
18
  let {
9
19
  value = $bindable(null),
10
- placeholder = 'Select date...',
20
+ placeholder = "Select date...",
11
21
  disabled = false,
12
22
  class: className,
13
23
  onSelect,
@@ -22,9 +32,13 @@
22
32
  let open = $state(false);
23
33
 
24
34
  function formatDateDisplay(date: CalendarDate | null): string {
25
- if (!date) return '';
35
+ if (!date) return "";
26
36
  const d = new Date(date.year, date.month, date.day);
27
- return d.toLocaleDateString('en-GB', { day: 'numeric', month: 'short', year: 'numeric' });
37
+ return d.toLocaleDateString("en-GB", {
38
+ day: "numeric",
39
+ month: "short",
40
+ year: "numeric",
41
+ });
28
42
  }
29
43
 
30
44
  function handleSelect(date: CalendarDate) {
@@ -45,14 +59,14 @@
45
59
  <button
46
60
  {...props}
47
61
  class={cn(
48
- 'flex h-10 w-full items-center justify-between rounded-lg border border-[var(--ui-input)] bg-[var(--ui-background)] px-3 py-2 text-sm text-[var(--ui-foreground)] transition-colors cursor-pointer',
49
- 'hover:border-[var(--ui-primary)]/50 focus:border-[var(--ui-primary)] focus:ring-2 focus:ring-[var(--ui-ring)]/20',
50
- disabled && 'opacity-50 cursor-not-allowed',
51
- className
62
+ "flex h-10 w-full items-center justify-between rounded-lg border border-[var(--ui-input)] bg-[var(--ui-background)] px-3 py-2 text-sm text-[var(--ui-foreground)] transition-colors cursor-pointer",
63
+ "hover:border-[var(--ui-primary)]/50 focus:border-[var(--ui-primary)] focus:ring-2 focus:ring-[var(--ui-ring)]/20",
64
+ disabled && "opacity-50 cursor-not-allowed",
65
+ className,
52
66
  )}
53
67
  {disabled}
54
68
  >
55
- <span class={cn(!value && 'text-[var(--ui-muted-foreground)]')}>
69
+ <span class={cn(!value && "text-[var(--ui-muted-foreground)]")}>
56
70
  {value ? formatDateDisplay(value) : placeholder}
57
71
  </span>
58
72
  <CalendarIcon class="size-4 text-[var(--ui-muted-foreground)]" />
@@ -1,4 +1,4 @@
1
- import { type CalendarDate } from '@intinyagroup/calendar';
1
+ import { type CalendarDate } from "./Calendar.svelte";
2
2
  type $$ComponentProps = {
3
3
  value?: CalendarDate | null;
4
4
  placeholder?: string;
@@ -0,0 +1,392 @@
1
+ <script lang="ts">
2
+ import {
3
+ Calendar as CalendarIcon,
4
+ ChevronLeft,
5
+ ChevronRight,
6
+ X,
7
+ } from "lucide-svelte";
8
+ import { Button } from "@intinyagroup/ui";
9
+ import { cn } from "@intinyagroup/ui/utils";
10
+ import {
11
+ addMonths,
12
+ subMonths,
13
+ addDays,
14
+ subDays,
15
+ startOfMonth,
16
+ endOfMonth,
17
+ startOfWeek,
18
+ endOfWeek,
19
+ eachDayOfInterval,
20
+ isSameDay,
21
+ isSameMonth,
22
+ isWithinInterval,
23
+ isBefore,
24
+ format,
25
+ } from "date-fns";
26
+ import { id as localeId, enUS as localeEn } from "date-fns/locale";
27
+
28
+ export type CalendarDate = { year: number; month: number; day: number };
29
+ export type DateRange = {
30
+ start: CalendarDate | null;
31
+ end: CalendarDate | null;
32
+ };
33
+
34
+ export type DateRangePreset = {
35
+ label: string;
36
+ range: DateRange;
37
+ };
38
+
39
+ let {
40
+ value = $bindable({ start: null, end: null }),
41
+ min = null,
42
+ max = null,
43
+ disabled = false,
44
+ placeholder = "Select date range...",
45
+ locale = "en-US",
46
+ firstDayOfWeek = 0, // 0 = Sunday, 1 = Monday
47
+ presets,
48
+ class: className,
49
+ onSelect,
50
+ }: {
51
+ value?: DateRange;
52
+ min?: CalendarDate | null;
53
+ max?: CalendarDate | null;
54
+ disabled?: boolean;
55
+ placeholder?: string;
56
+ locale?: string;
57
+ firstDayOfWeek?: number;
58
+ presets?: DateRangePreset[];
59
+ class?: string;
60
+ onSelect?: (range: DateRange) => void;
61
+ } = $props();
62
+
63
+ let isOpen = $state(false);
64
+ const today = new Date();
65
+
66
+ let viewDate = $state(
67
+ value?.start
68
+ ? new Date(value.start.year, value.start.month, value.start.day)
69
+ : today,
70
+ );
71
+ let hoverDate = $state<Date | null>(null);
72
+
73
+ const activeDateFnsLocale = $derived(
74
+ locale.startsWith("id") ? localeId : localeEn,
75
+ );
76
+ const weekStartsOn = $derived(firstDayOfWeek as 0 | 1 | 2 | 3 | 4 | 5 | 6);
77
+
78
+ function toDate(d: CalendarDate | null): Date | null {
79
+ if (!d) return null;
80
+ return new Date(d.year, d.month, d.day);
81
+ }
82
+
83
+ function toCalendarDate(date: Date): CalendarDate {
84
+ return {
85
+ year: date.getFullYear(),
86
+ month: date.getMonth(),
87
+ day: date.getDate(),
88
+ };
89
+ }
90
+
91
+ // Generate day names for header using date-fns
92
+ const dayNames = $derived.by(() => {
93
+ const start = startOfWeek(viewDate, { weekStartsOn });
94
+ return Array.from({ length: 7 }, (_, i) => {
95
+ return format(addDays(start, i), "EEE", { locale: activeDateFnsLocale });
96
+ });
97
+ });
98
+
99
+ // Generate full calendar grid (days from start of week of month-start to end of week of month-end)
100
+ const calendarDays = $derived.by(() => {
101
+ const monthStart = startOfMonth(viewDate);
102
+ const monthEnd = endOfMonth(viewDate);
103
+ const gridStart = startOfWeek(monthStart, { weekStartsOn });
104
+ const gridEnd = endOfWeek(monthEnd, { weekStartsOn });
105
+
106
+ const days = eachDayOfInterval({ start: gridStart, end: gridEnd });
107
+ return days.map((d) => ({
108
+ date: d,
109
+ calendarDate: toCalendarDate(d),
110
+ currentMonth: isSameMonth(d, viewDate),
111
+ }));
112
+ });
113
+
114
+ const startDateObj = $derived(toDate(value.start));
115
+ const endDateObj = $derived(toDate(value.end));
116
+
117
+ function handleDateClick(date: Date) {
118
+ if (disabled) return;
119
+
120
+ if (!startDateObj || (startDateObj && endDateObj)) {
121
+ value = { start: toCalendarDate(date), end: null };
122
+ } else if (startDateObj && !endDateObj) {
123
+ if (isBefore(date, startDateObj)) {
124
+ value = {
125
+ start: toCalendarDate(date),
126
+ end: toCalendarDate(startDateObj),
127
+ };
128
+ } else {
129
+ value = {
130
+ start: toCalendarDate(startDateObj),
131
+ end: toCalendarDate(date),
132
+ };
133
+ }
134
+ onSelect?.(value);
135
+ isOpen = false;
136
+ }
137
+ }
138
+
139
+ function prevMonth() {
140
+ viewDate = subMonths(viewDate, 1);
141
+ }
142
+
143
+ function nextMonth() {
144
+ viewDate = addMonths(viewDate, 1);
145
+ }
146
+
147
+ function formatDisplayDate(date: CalendarDate | null): string {
148
+ const d = toDate(date);
149
+ if (!d) return "";
150
+ return format(d, "dd/MM/yyyy", { locale: activeDateFnsLocale });
151
+ }
152
+
153
+ const label = $derived.by(() => {
154
+ if (value.start && value.end) {
155
+ return `${formatDisplayDate(value.start)} - ${formatDisplayDate(value.end)}`;
156
+ }
157
+ if (value.start) {
158
+ return `${formatDisplayDate(value.start)} - ...`;
159
+ }
160
+ return placeholder;
161
+ });
162
+
163
+ function clearRange(e: MouseEvent) {
164
+ e.stopPropagation();
165
+ value = { start: null, end: null };
166
+ onSelect?.(value);
167
+ }
168
+
169
+ // Rich presets using date-fns intervals
170
+ const activePresets = $derived.by<DateRangePreset[]>(() => {
171
+ if (presets) return presets;
172
+
173
+ const y = today.getFullYear();
174
+
175
+ return [
176
+ {
177
+ label: "Today",
178
+ range: { start: toCalendarDate(today), end: toCalendarDate(today) },
179
+ },
180
+ {
181
+ label: "Next 7D",
182
+ range: {
183
+ start: toCalendarDate(today),
184
+ end: toCalendarDate(addDays(today, 7)),
185
+ },
186
+ },
187
+ {
188
+ label: "This Month",
189
+ range: {
190
+ start: toCalendarDate(startOfMonth(today)),
191
+ end: toCalendarDate(endOfMonth(today)),
192
+ },
193
+ },
194
+ {
195
+ label: "Last Month",
196
+ range: {
197
+ start: toCalendarDate(startOfMonth(subMonths(today, 1))),
198
+ end: toCalendarDate(endOfMonth(subMonths(today, 1))),
199
+ },
200
+ },
201
+ {
202
+ label: "YTD",
203
+ range: {
204
+ start: { year: y, month: 0, day: 1 },
205
+ end: toCalendarDate(today),
206
+ },
207
+ },
208
+ ];
209
+ });
210
+ </script>
211
+
212
+ <div class={cn("relative inline-block w-full max-w-sm", className)}>
213
+ <!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
214
+ <div
215
+ role="button"
216
+ tabindex={disabled ? -1 : 0}
217
+ aria-disabled={disabled}
218
+ onclick={() => {
219
+ if (!disabled) isOpen = !isOpen;
220
+ }}
221
+ onkeydown={(e) => {
222
+ if (disabled) return;
223
+ if (e.key === "Enter" || e.key === " ") {
224
+ e.preventDefault();
225
+ isOpen = !isOpen;
226
+ }
227
+ }}
228
+ class={cn(
229
+ "flex h-10 w-full items-center justify-between rounded-lg border border-[var(--ui-input)] bg-[var(--ui-background)] px-3 text-sm transition-colors text-left cursor-pointer select-none",
230
+ "hover:border-[var(--ui-border)] focus:outline-none focus:ring-2 focus:ring-[var(--ui-ring)]/20 focus:border-[var(--ui-primary)]",
231
+ disabled && "opacity-50 cursor-not-allowed pointer-events-none",
232
+ value.start
233
+ ? "text-[var(--ui-foreground)]"
234
+ : "text-[var(--ui-muted-foreground)]",
235
+ )}
236
+ >
237
+ <div class="flex items-center gap-2 truncate">
238
+ <CalendarIcon class="size-4 shrink-0 text-[var(--ui-muted-foreground)]" />
239
+ <span class="truncate">{label}</span>
240
+ </div>
241
+
242
+ {#if value.start && !disabled}
243
+ <button
244
+ type="button"
245
+ onclick={(e) => {
246
+ e.stopPropagation();
247
+ clearRange();
248
+ }}
249
+ class="p-1 rounded-full hover:bg-[var(--ui-secondary)] text-[var(--ui-muted-foreground)] hover:text-[var(--ui-foreground)]"
250
+ aria-label="Clear date range"
251
+ >
252
+ <X class="size-3.5" />
253
+ </button>
254
+ {/if}
255
+ </div>
256
+
257
+ {#if isOpen}
258
+ <!-- Backdrop for click outside -->
259
+ <!-- svelte-ignore a11y_no_static_element_interactions -->
260
+ <!-- svelte-ignore a11y_click_events_have_key_events -->
261
+ <div class="fixed inset-0 z-40" onclick={() => (isOpen = false)}></div>
262
+
263
+ <div
264
+ class="absolute left-0 top-full z-50 mt-1.5 w-80 rounded-xl border border-[var(--ui-border)] bg-[var(--ui-card)] p-3.5 shadow-xl animate-in fade-in-50 zoom-in-95 select-none"
265
+ >
266
+ <!-- Header controls -->
267
+ <div class="flex items-center justify-between mb-3">
268
+ <Button
269
+ variant="ghost"
270
+ size="sm"
271
+ class="size-7 p-0"
272
+ onclick={prevMonth}
273
+ >
274
+ <ChevronLeft class="size-4" />
275
+ </Button>
276
+ <span
277
+ class="text-xs font-semibold text-[var(--ui-foreground)] capitalize"
278
+ >
279
+ {format(viewDate, "MMMM yyyy", { locale: activeDateFnsLocale })}
280
+ </span>
281
+ <Button
282
+ variant="ghost"
283
+ size="sm"
284
+ class="size-7 p-0"
285
+ onclick={nextMonth}
286
+ >
287
+ <ChevronRight class="size-4" />
288
+ </Button>
289
+ </div>
290
+
291
+ <!-- Day names -->
292
+ <div
293
+ class="grid grid-cols-7 mb-1 text-center"
294
+ style="display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); text-align: center; margin-bottom: 0.25rem;"
295
+ >
296
+ {#each dayNames as day}
297
+ <span
298
+ class="text-[11px] font-semibold text-[var(--ui-muted-foreground)] py-1 capitalize"
299
+ style="text-align: center; font-size: 0.6875rem; padding: 0.25rem 0;"
300
+ >
301
+ {day}
302
+ </span>
303
+ {/each}
304
+ </div>
305
+
306
+ <!-- Calendar grid -->
307
+ <div
308
+ class="grid grid-cols-7 gap-y-1"
309
+ style="display: grid; grid-template-columns: repeat(7, minmax(0, 1fr)); row-gap: 0.25rem;"
310
+ >
311
+ {#each calendarDays as { date, calendarDate, currentMonth }}
312
+ {@const isStart = startDateObj
313
+ ? isSameDay(startDateObj, date)
314
+ : false}
315
+ {@const isEnd = endDateObj ? isSameDay(endDateObj, date) : false}
316
+ {@const activeRangeEnd = endDateObj || hoverDate}
317
+ {@const inRange =
318
+ startDateObj && activeRangeEnd
319
+ ? isWithinInterval(date, {
320
+ start: isBefore(startDateObj, activeRangeEnd)
321
+ ? startDateObj
322
+ : activeRangeEnd,
323
+ end: isBefore(startDateObj, activeRangeEnd)
324
+ ? activeRangeEnd
325
+ : startDateObj,
326
+ })
327
+ : false}
328
+ {@const isEdge = isStart || isEnd}
329
+
330
+ <div
331
+ role="gridcell"
332
+ tabindex="-1"
333
+ aria-selected={isEdge}
334
+ class={cn(
335
+ "relative py-0.5 flex items-center justify-center",
336
+ inRange && !isEdge && "bg-[var(--ui-primary)]/10",
337
+ isStart &&
338
+ activeRangeEnd &&
339
+ !isSameDay(startDateObj, activeRangeEnd) &&
340
+ "rounded-l-md bg-[var(--ui-primary)]/10",
341
+ isEnd &&
342
+ startDateObj &&
343
+ !isSameDay(startDateObj, endDateObj) &&
344
+ "rounded-r-md bg-[var(--ui-primary)]/10",
345
+ )}
346
+ onmouseenter={() => {
347
+ if (startDateObj && !endDateObj) hoverDate = date;
348
+ }}
349
+ >
350
+ <button
351
+ type="button"
352
+ disabled={!currentMonth}
353
+ onclick={() => handleDateClick(date)}
354
+ class={cn(
355
+ "size-8 rounded-md text-xs font-medium transition-colors flex items-center justify-center cursor-pointer",
356
+ !currentMonth && "opacity-20 cursor-default",
357
+ currentMonth &&
358
+ !isEdge &&
359
+ !inRange &&
360
+ "hover:bg-[var(--ui-secondary)] text-[var(--ui-foreground)]",
361
+ inRange && !isEdge && "text-[var(--ui-primary)] font-semibold",
362
+ isEdge &&
363
+ "bg-[var(--ui-primary)] text-[var(--ui-primary-foreground)] font-bold shadow-xs",
364
+ )}
365
+ >
366
+ {date.getDate()}
367
+ </button>
368
+ </div>
369
+ {/each}
370
+ </div>
371
+
372
+ <!-- Presets quick buttons bar -->
373
+ <div
374
+ class="flex flex-wrap items-center gap-1.5 border-t border-[var(--ui-border)] mt-3 pt-2.5"
375
+ >
376
+ {#each activePresets as preset}
377
+ <button
378
+ type="button"
379
+ onclick={() => {
380
+ value = preset.range;
381
+ onSelect?.(value);
382
+ isOpen = false;
383
+ }}
384
+ class="rounded-md border border-[var(--ui-border)] bg-[var(--ui-secondary)]/30 px-2 py-1 text-[10px] font-medium text-[var(--ui-foreground)] hover:bg-[var(--ui-secondary)] transition-colors cursor-pointer"
385
+ >
386
+ {preset.label}
387
+ </button>
388
+ {/each}
389
+ </div>
390
+ </div>
391
+ {/if}
392
+ </div>