@tuturuuu/ui 0.36.1 → 0.37.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.
Files changed (43) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/package.json +7 -5
  3. package/src/components/ui/date-time-picker-timezone.test.tsx +258 -0
  4. package/src/components/ui/date-time-picker-values.ts +205 -0
  5. package/src/components/ui/date-time-picker.tsx +70 -167
  6. package/src/components/ui/legacy/calendar/agenda-view.tsx +29 -94
  7. package/src/components/ui/legacy/calendar/all-day-color.test.tsx +48 -0
  8. package/src/components/ui/legacy/calendar/all-day-event-bar.tsx +29 -41
  9. package/src/components/ui/legacy/calendar/calendar-cell-color.test.tsx +70 -0
  10. package/src/components/ui/legacy/calendar/calendar-cell.tsx +4 -5
  11. package/src/components/ui/legacy/calendar/calendar-content.test.tsx +82 -3
  12. package/src/components/ui/legacy/calendar/calendar-content.tsx +37 -34
  13. package/src/components/ui/legacy/calendar/calendar-day-bridge.test.tsx +94 -0
  14. package/src/components/ui/legacy/calendar/calendar-day-display.test.tsx +106 -0
  15. package/src/components/ui/legacy/calendar/calendar-header.tsx +14 -5
  16. package/src/components/ui/legacy/calendar/calendar-matrix.tsx +4 -9
  17. package/src/components/ui/legacy/calendar/calendar-views.test.tsx +120 -6
  18. package/src/components/ui/legacy/calendar/day-title.tsx +6 -6
  19. package/src/components/ui/legacy/calendar/dynamic-island.test.tsx +140 -0
  20. package/src/components/ui/legacy/calendar/dynamic-island.tsx +27 -24
  21. package/src/components/ui/legacy/calendar/event-card.test.tsx +206 -7
  22. package/src/components/ui/legacy/calendar/event-card.tsx +27 -49
  23. package/src/components/ui/legacy/calendar/event-modal.tsx +6 -26
  24. package/src/components/ui/legacy/calendar/event-picker-bounds.ts +16 -0
  25. package/src/components/ui/legacy/calendar/event-preview-popover.test.tsx +139 -0
  26. package/src/components/ui/legacy/calendar/event-preview-popover.tsx +13 -17
  27. package/src/components/ui/legacy/calendar/event-preview-time.ts +41 -0
  28. package/src/components/ui/legacy/calendar/location-timeline.tsx +9 -17
  29. package/src/components/ui/legacy/calendar/month-calendar.tsx +20 -10
  30. package/src/components/ui/legacy/calendar/time-indicator-line.tsx +3 -20
  31. package/src/components/ui/legacy/calendar/time-indicator.tsx +11 -3
  32. package/src/components/ui/legacy/calendar/weekday-bar.tsx +2 -8
  33. package/src/components/ui/legacy/calendar/year-calendar.tsx +27 -39
  34. package/src/hooks/__tests__/use-calendar-sync.test.tsx +45 -0
  35. package/src/hooks/calendar-event-lookup.test.ts +39 -0
  36. package/src/hooks/calendar-event-lookup.ts +22 -8
  37. package/src/hooks/calendar-sync-range.test.ts +92 -0
  38. package/src/hooks/calendar-sync-range.ts +40 -0
  39. package/src/hooks/use-calendar-clock.ts +49 -0
  40. package/src/hooks/use-calendar-day-zone.tsx +12 -0
  41. package/src/hooks/use-calendar-sync.tsx +42 -80
  42. package/src/hooks/use-calendar.tsx +3 -3
  43. package/src/lib/calendar-day.ts +67 -0
@@ -22,7 +22,7 @@ import {
22
22
  resolveTaskTimezone,
23
23
  } from '@tuturuuu/utils/task-date-timezone';
24
24
  import { getTimeFormatPattern } from '@tuturuuu/utils/time-helper';
25
- import { format, parse } from 'date-fns';
25
+ import { format } from 'date-fns';
26
26
  import {
27
27
  type ReactNode,
28
28
  useEffect,
@@ -32,6 +32,14 @@ import {
32
32
  useState,
33
33
  } from 'react';
34
34
  import { DATE_TIME_PICKER_LAYOUT_CLASS_NAMES } from './date-time-picker-layout';
35
+ import {
36
+ createPickerTimeOptions,
37
+ filterPickerTimeOptions,
38
+ pickerCalendarBounds,
39
+ pickerCalendarDate,
40
+ pickerTimeWithMinimum,
41
+ pickerWallTime,
42
+ } from './date-time-picker-values';
35
43
  import { Separator } from './separator';
36
44
 
37
45
  interface DateTimePickerProps {
@@ -102,6 +110,7 @@ export function DateTimePicker({
102
110
  const [selectedDate, setSelectedDate] = useState<Date | undefined>(date);
103
111
  const [isCalendarOpen, setIsCalendarOpen] = useState(false);
104
112
  const [isManualTimeEntry, setIsManualTimeEntry] = useState(false);
113
+ const [invalidManualTime, setInvalidManualTime] = useState(false);
105
114
  const [manualTimeInput, setManualTimeInput] = useState(
106
115
  date ? format(date, 'HH:mm') : ''
107
116
  );
@@ -200,14 +209,30 @@ export function DateTimePicker({
200
209
  if (tz) {
201
210
  const hour = date ? getDatePartsInTimezone(date, tz).hour : 0;
202
211
  const minute = date ? getDatePartsInTimezone(date, tz).minute : 0;
203
- next = buildDateInTimezone(
204
- selectedDate.getFullYear(),
205
- selectedDate.getMonth() + 1,
206
- selectedDate.getDate(),
207
- hour,
208
- minute,
209
- tz
210
- );
212
+ if (
213
+ date &&
214
+ pickerCalendarDate(date, tz).getTime() === selectedDate.getTime()
215
+ ) {
216
+ next = new Date(date);
217
+ } else {
218
+ next = buildDateInTimezone(
219
+ selectedDate.getFullYear(),
220
+ selectedDate.getMonth() + 1,
221
+ selectedDate.getDate(),
222
+ hour,
223
+ minute,
224
+ tz
225
+ );
226
+ const parts = getDatePartsInTimezone(next, tz);
227
+ if (
228
+ parts.year !== selectedDate.getFullYear() ||
229
+ parts.month !== selectedDate.getMonth() + 1 ||
230
+ parts.day !== selectedDate.getDate() ||
231
+ parts.hour !== hour ||
232
+ parts.minute !== minute
233
+ )
234
+ return;
235
+ }
211
236
  } else {
212
237
  if (date) {
213
238
  selectedDate.setHours(date.getHours());
@@ -237,40 +262,14 @@ export function DateTimePicker({
237
262
  if (Number.isNaN(hours) || Number.isNaN(minutes)) return;
238
263
 
239
264
  const baseDate = selectedDate ?? date ?? minDate ?? new Date();
240
- let newDate: Date;
241
-
242
- if (tz) {
243
- const p = getDatePartsInTimezone(baseDate, tz);
244
- newDate = buildDateInTimezone(p.year, p.month, p.day, hours, minutes, tz);
245
- if (minDate && newDate.getTime() <= minDate.getTime()) {
246
- const minParts = getDatePartsInTimezone(minDate, tz);
247
- newDate = buildDateInTimezone(
248
- minParts.year,
249
- minParts.month,
250
- minParts.day + 1,
251
- hours,
252
- minutes,
253
- tz
254
- );
255
- }
256
- } else {
257
- newDate = new Date(baseDate);
258
- newDate.setHours(hours);
259
- newDate.setMinutes(minutes);
260
-
261
- if (
262
- minDate &&
263
- newDate.getFullYear() === minDate.getFullYear() &&
264
- newDate.getMonth() === minDate.getMonth() &&
265
- newDate.getDate() === minDate.getDate()
266
- ) {
267
- const minTimeValue = minDate.getHours() * 60 + minDate.getMinutes();
268
- const newTimeValue = newDate.getHours() * 60 + newDate.getMinutes();
269
- if (newTimeValue <= minTimeValue) {
270
- newDate.setDate(newDate.getDate() + 1);
271
- }
272
- }
273
- }
265
+ const newDate = pickerTimeWithMinimum(
266
+ baseDate,
267
+ hours,
268
+ minutes,
269
+ tz,
270
+ minDate
271
+ );
272
+ if (!newDate) return;
274
273
 
275
274
  setSelectedDate(newDate);
276
275
  updateDate(newDate);
@@ -292,23 +291,12 @@ export function DateTimePicker({
292
291
  !Number.isNaN(m)
293
292
  ) {
294
293
  const baseDate = selectedDate ?? date ?? minDate ?? new Date();
295
- const baseParts = tz ? getDatePartsInTimezone(baseDate, tz) : null;
296
- const newDate =
297
- tz && baseParts
298
- ? buildDateInTimezone(
299
- baseParts.year,
300
- baseParts.month,
301
- baseParts.day,
302
- h,
303
- m,
304
- tz
305
- )
306
- : (() => {
307
- const d = new Date(baseDate);
308
- d.setHours(h);
309
- d.setMinutes(m);
310
- return d;
311
- })();
294
+ const newDate = pickerWallTime(baseDate, h, m, tz);
295
+ if (!newDate) {
296
+ setInvalidManualTime(true);
297
+ return;
298
+ }
299
+ setInvalidManualTime(false);
312
300
  setSelectedDate(newDate);
313
301
  updateDate(newDate);
314
302
  setIsManualTimeEntry(false);
@@ -338,88 +326,25 @@ export function DateTimePicker({
338
326
  const timeFormat = preferences?.timeFormat ?? '12h';
339
327
  const timePattern = getTimeFormatPattern(timeFormat);
340
328
 
341
- // Generate time options in 15-minute increments
342
329
  const timeOptions = useMemo(
330
+ () => createPickerTimeOptions(timeFormat),
331
+ [timeFormat]
332
+ );
333
+ const filteredTimeOptions = useMemo(
343
334
  () =>
344
- Array.from({ length: 24 * 4 }, (_, i) => {
345
- const hour = Math.floor(i / 4);
346
- const minute = (i % 4) * 15;
347
- const formattedHour = hour.toString().padStart(2, '0');
348
- const formattedMinute = minute.toString().padStart(2, '0');
349
- const value = `${formattedHour}:${formattedMinute}`;
350
- const display = format(parse(value, 'HH:mm', new Date()), timePattern);
351
- return { value, display };
335
+ filterPickerTimeOptions({
336
+ date,
337
+ minDate,
338
+ maxDate,
339
+ minTime,
340
+ zone: tz,
341
+ pattern: timePattern,
342
+ timeFormat,
343
+ options: timeOptions,
352
344
  }),
353
- [timePattern]
345
+ [date, minDate, maxDate, minTime, tz, timePattern, timeFormat, timeOptions]
354
346
  );
355
347
 
356
- // Filter time options for end time picker
357
- const filteredTimeOptions = useMemo(() => {
358
- let options = timeOptions;
359
-
360
- // Filter based on minDate and minTime
361
- if (minTime && date && minDate) {
362
- // Only restrict if the selected date is the same as minDate
363
- if (
364
- date.getFullYear() === minDate.getFullYear() &&
365
- date.getMonth() === minDate.getMonth() &&
366
- date.getDate() === minDate.getDate()
367
- ) {
368
- options = options.filter((time) => time.value > minTime);
369
- } else if (date > minDate) {
370
- // If end date is after start date, show all times
371
- options = timeOptions;
372
- }
373
- }
374
-
375
- // Filter based on maxDate
376
- if (date && maxDate) {
377
- // Only restrict if the selected date is the same as maxDate
378
- if (
379
- date.getFullYear() === maxDate.getFullYear() &&
380
- date.getMonth() === maxDate.getMonth() &&
381
- date.getDate() === maxDate.getDate()
382
- ) {
383
- const maxTimeValue = `${format(maxDate, 'HH')}:${format(maxDate, 'mm')}`;
384
- options = options.filter((time) => time.value < maxTimeValue);
385
- }
386
- }
387
-
388
- // After filtering, ensure the selected time is always present in the dropdown
389
- if (date) {
390
- const hourMinute =
391
- tz !== null
392
- ? getDatePartsInTimezone(date, tz)
393
- : {
394
- hour: date.getHours(),
395
- minute: date.getMinutes(),
396
- };
397
- const customValue = `${hourMinute.hour.toString().padStart(2, '0')}:${hourMinute.minute.toString().padStart(2, '0')}`;
398
- if (!options.some((t) => t.value === customValue)) {
399
- const display =
400
- tz !== null
401
- ? formatInTimezone(
402
- date,
403
- tz,
404
- timeFormat === '24h' ? 'HH:mm' : 'h:mm A'
405
- )
406
- : format(date, timePattern);
407
- options = [...options, { value: customValue, display }];
408
- options.sort((a, b) => a.value.localeCompare(b.value));
409
- }
410
- }
411
- return options;
412
- }, [
413
- date,
414
- minDate,
415
- maxDate,
416
- minTime,
417
- timeOptions,
418
- timePattern,
419
- tz,
420
- timeFormat,
421
- ]);
422
-
423
348
  // If the filtered list is empty, show an error message
424
349
  const noValidTimes = filteredTimeOptions.length === 0;
425
350
  const selectedTimeValue = date
@@ -500,34 +425,7 @@ export function DateTimePicker({
500
425
  }
501
426
  };
502
427
 
503
- // Shared calendar disabled dates config
504
- const calendarDisabled =
505
- minDate || maxDate
506
- ? [
507
- ...(minDate
508
- ? [
509
- {
510
- before: new Date(
511
- minDate.getFullYear(),
512
- minDate.getMonth(),
513
- minDate.getDate()
514
- ),
515
- },
516
- ]
517
- : []),
518
- ...(maxDate
519
- ? [
520
- {
521
- after: new Date(
522
- maxDate.getFullYear(),
523
- maxDate.getMonth(),
524
- maxDate.getDate()
525
- ),
526
- },
527
- ]
528
- : []),
529
- ]
530
- : undefined;
428
+ const calendarDisabled = pickerCalendarBounds(minDate, maxDate, tz);
531
429
 
532
430
  const timeControl = showTimeSelect ? (
533
431
  <div className={DATE_TIME_PICKER_LAYOUT_CLASS_NAMES.time}>
@@ -545,7 +443,11 @@ export function DateTimePicker({
545
443
  <div className="flex items-center gap-2">
546
444
  <Input
547
445
  value={manualTimeInput}
548
- onChange={(e) => setManualTimeInput(e.target.value)}
446
+ onChange={(e) => {
447
+ setManualTimeInput(e.target.value);
448
+ setInvalidManualTime(false);
449
+ }}
450
+ aria-invalid={invalidManualTime}
549
451
  onKeyDown={handleManualTimeKeyDown}
550
452
  placeholder="HH:MM"
551
453
  className="h-9 flex-1"
@@ -650,7 +552,8 @@ export function DateTimePicker({
650
552
  <div className={DATE_TIME_PICKER_LAYOUT_CLASS_NAMES.calendar}>
651
553
  <Calendar
652
554
  mode="single"
653
- selected={date}
555
+ selected={date ? pickerCalendarDate(date, tz) : undefined}
556
+ today={pickerCalendarDate(new Date(), tz)}
654
557
  onSelect={handleSelect}
655
558
  onSubmit={(date) => {
656
559
  handleSelect(date);
@@ -4,24 +4,20 @@ import { CalendarX2, Clock, MapPin, Plus, Search, Sun } from '@tuturuuu/icons';
4
4
  import type { Workspace } from '@tuturuuu/types';
5
5
  import type { CalendarEvent } from '@tuturuuu/types/primitives/calendar-event';
6
6
  import { useCalendar } from '@tuturuuu/ui/hooks/use-calendar';
7
+ import { useCalendarClock } from '@tuturuuu/ui/hooks/use-calendar-clock';
7
8
  import { useCalendarPreferences } from '@tuturuuu/ui/hooks/use-calendar-preferences';
8
9
  import { useUserBooleanConfig } from '@tuturuuu/ui/hooks/use-user-config';
10
+ import { calendarEventStyle } from '@tuturuuu/utils/calendar-event-colors';
9
11
  import { isAllDayEvent } from '@tuturuuu/utils/calendar-utils';
10
12
  import { cn } from '@tuturuuu/utils/format';
11
13
  import { getTimeFormatPattern } from '@tuturuuu/utils/time-helper';
12
- import {
13
- addDays,
14
- format,
15
- isToday,
16
- isTomorrow,
17
- isYesterday,
18
- startOfDay,
19
- } from 'date-fns';
14
+ import { addDays, format, startOfDay } from 'date-fns';
20
15
  import dayjs from 'dayjs';
21
16
  import timezone from 'dayjs/plugin/timezone';
22
17
  import utc from 'dayjs/plugin/utc';
23
18
  import { useTranslations } from 'next-intl';
24
19
  import { useMemo, useState } from 'react';
20
+ import { calendarDayKey, calendarToday } from '../../../../lib/calendar-day';
25
21
  import {
26
22
  formatLunarDay,
27
23
  getLunarDate,
@@ -49,83 +45,17 @@ interface GroupedEvents {
49
45
  events: CalendarEvent[];
50
46
  }
51
47
 
52
- const COLOR_MAP: Record<
53
- string,
54
- { accent: string; bg: string; border: string }
55
- > = {
56
- blue: {
57
- accent: 'bg-dynamic-blue',
58
- bg: 'bg-dynamic-blue/5 hover:bg-dynamic-blue/10',
59
- border: 'border-l-dynamic-blue',
60
- },
61
- red: {
62
- accent: 'bg-dynamic-red',
63
- bg: 'bg-dynamic-red/5 hover:bg-dynamic-red/10',
64
- border: 'border-l-dynamic-red',
65
- },
66
- green: {
67
- accent: 'bg-dynamic-green',
68
- bg: 'bg-dynamic-green/5 hover:bg-dynamic-green/10',
69
- border: 'border-l-dynamic-green',
70
- },
71
- purple: {
72
- accent: 'bg-dynamic-purple',
73
- bg: 'bg-dynamic-purple/5 hover:bg-dynamic-purple/10',
74
- border: 'border-l-dynamic-purple',
75
- },
76
- yellow: {
77
- accent: 'bg-dynamic-yellow',
78
- bg: 'bg-dynamic-yellow/5 hover:bg-dynamic-yellow/10',
79
- border: 'border-l-dynamic-yellow',
80
- },
81
- orange: {
82
- accent: 'bg-dynamic-orange',
83
- bg: 'bg-dynamic-orange/5 hover:bg-dynamic-orange/10',
84
- border: 'border-l-dynamic-orange',
85
- },
86
- pink: {
87
- accent: 'bg-dynamic-pink',
88
- bg: 'bg-dynamic-pink/5 hover:bg-dynamic-pink/10',
89
- border: 'border-l-dynamic-pink',
90
- },
91
- cyan: {
92
- accent: 'bg-dynamic-cyan',
93
- bg: 'bg-dynamic-cyan/5 hover:bg-dynamic-cyan/10',
94
- border: 'border-l-dynamic-cyan',
95
- },
96
- indigo: {
97
- accent: 'bg-dynamic-indigo',
98
- bg: 'bg-dynamic-indigo/5 hover:bg-dynamic-indigo/10',
99
- border: 'border-l-dynamic-indigo',
100
- },
101
- gray: {
102
- accent: 'bg-dynamic-gray',
103
- bg: 'bg-dynamic-gray/5 hover:bg-dynamic-gray/10',
104
- border: 'border-l-dynamic-gray',
105
- },
106
- };
107
-
108
- function normalizeColor(color: string): string {
109
- if (!color) return 'blue';
110
- const normalized = color.trim().toLowerCase();
111
- if (normalized === '#6b7280' || normalized === 'grey') return 'gray';
112
- return normalized;
113
- }
114
-
115
- function getStyles(event: CalendarEvent) {
116
- const key = normalizeColor(event.color || 'blue');
117
- return COLOR_MAP[key] || COLOR_MAP.blue!;
118
- }
119
-
120
48
  function formatTimeWithMidnight(
121
49
  date: Date,
122
50
  timePattern: string,
123
- timeFormat: '12h' | '24h'
51
+ timeFormat: '12h' | '24h',
52
+ zone?: string
124
53
  ): string {
125
- if (date.getHours() === 23 && date.getMinutes() === 59) {
54
+ const zoned = zone && zone !== 'auto' ? dayjs(date).tz(zone) : dayjs(date);
55
+ if (zoned.hour() === 23 && zoned.minute() === 59) {
126
56
  return timeFormat === '24h' ? '00:00' : '12:00 am';
127
57
  }
128
- return format(date, timePattern);
58
+ return zoned.format(timePattern);
129
59
  }
130
60
 
131
61
  function EventCard({
@@ -141,40 +71,41 @@ function EventCard({
141
71
  t: (key: string) => string;
142
72
  onOpen: (id: string) => void;
143
73
  }) {
74
+ const { settings } = useCalendarSettings();
144
75
  const isAllDay = isAllDayEvent(event);
145
- const styles = getStyles(event);
146
76
 
147
77
  return (
148
78
  <button
149
79
  type="button"
150
80
  onClick={() => onOpen(event.id)}
81
+ style={calendarEventStyle(event)}
151
82
  className={cn(
152
- 'group flex w-full cursor-pointer items-start gap-3 rounded-lg border-l-[3px] px-3 py-2.5 text-left transition-colors',
153
- styles.border,
154
- styles.bg
83
+ 'group flex w-full cursor-pointer items-start gap-3 rounded-lg border-l-[3px] px-3 py-2.5 text-left transition-colors hover:ring-1 hover:ring-current'
155
84
  )}
156
85
  >
157
86
  {/* Time column */}
158
87
  <div className="flex w-16 shrink-0 flex-col pt-0.5 sm:w-20">
159
88
  {isAllDay ? (
160
- <span className="flex items-center gap-1 font-medium text-muted-foreground text-xs">
89
+ <span className="flex items-center gap-1 font-medium text-inherit text-xs">
161
90
  <Sun className="h-3 w-3" />
162
91
  {t('agenda_all_day')}
163
92
  </span>
164
93
  ) : (
165
94
  <>
166
- <span className="font-semibold text-foreground text-sm leading-tight">
95
+ <span className="font-semibold text-inherit text-sm leading-tight">
167
96
  {formatTimeWithMidnight(
168
97
  new Date(event.start_at),
169
98
  timePattern,
170
- timeFormat
99
+ timeFormat,
100
+ settings?.timezone?.timezone
171
101
  )}
172
102
  </span>
173
- <span className="text-muted-foreground text-xs">
103
+ <span className="text-inherit text-xs">
174
104
  {formatTimeWithMidnight(
175
105
  new Date(event.end_at),
176
106
  timePattern,
177
- timeFormat
107
+ timeFormat,
108
+ settings?.timezone?.timezone
178
109
  )}
179
110
  </span>
180
111
  </>
@@ -183,14 +114,14 @@ function EventCard({
183
114
 
184
115
  {/* Content */}
185
116
  <div className="min-w-0 flex-1">
186
- <span className="line-clamp-1 font-medium text-foreground text-sm leading-tight">
117
+ <span className="line-clamp-1 font-medium text-inherit text-sm leading-tight">
187
118
  {event.title || t('views.untitled_event')}
188
119
  </span>
189
120
 
190
121
  {(event.location || (!isAllDay && event.description)) && (
191
122
  <div className="mt-0.5 flex flex-wrap items-center gap-x-3 gap-y-0.5">
192
123
  {!isAllDay && (
193
- <span className="flex items-center gap-1 text-muted-foreground text-xs">
124
+ <span className="flex items-center gap-1 text-inherit text-xs">
194
125
  <Clock className="h-3 w-3 shrink-0" />
195
126
  {(() => {
196
127
  try {
@@ -209,7 +140,7 @@ function EventCard({
209
140
  </span>
210
141
  )}
211
142
  {event.location && (
212
- <span className="flex items-center gap-1 truncate text-muted-foreground text-xs">
143
+ <span className="flex items-center gap-1 truncate text-inherit text-xs">
213
144
  <MapPin className="h-3 w-3 shrink-0" />
214
145
  <span className="truncate">{event.location}</span>
215
146
  </span>
@@ -234,9 +165,13 @@ function DateHeader({
234
165
  showLunar: boolean;
235
166
  t: (key: string, values?: Record<string, number>) => string;
236
167
  }) {
237
- const today = isToday(date);
238
- const tomorrow = isTomorrow(date);
239
- const yesterday = isYesterday(date);
168
+ const { settings } = useCalendarSettings();
169
+ const now = useCalendarClock();
170
+ const todayDate = calendarToday(settings?.timezone?.timezone, now);
171
+ const dateKey = calendarDayKey(date);
172
+ const today = dateKey === calendarDayKey(todayDate);
173
+ const tomorrow = dateKey === calendarDayKey(addDays(todayDate, 1));
174
+ const yesterday = dateKey === calendarDayKey(addDays(todayDate, -1));
240
175
 
241
176
  const relativeLabel = today
242
177
  ? t('today')
@@ -0,0 +1,48 @@
1
+ import { render, screen } from '@testing-library/react';
2
+ import { expect, it, vi } from 'vitest';
3
+
4
+ vi.mock('@tuturuuu/ui/hooks/use-calendar', () => ({
5
+ useCalendar: () => ({
6
+ openModal: vi.fn(),
7
+ updateEvent: vi.fn(),
8
+ addEvent: vi.fn(),
9
+ deleteEvent: vi.fn(),
10
+ }),
11
+ }));
12
+ vi.mock('@tuturuuu/ui/hooks/use-calendar-sync', () => ({
13
+ useCalendarSync: () => ({
14
+ allDayEvents: [
15
+ {
16
+ id: 'cutoff',
17
+ title: 'Cut-off event',
18
+ start_at: '2026-09-06T00:00:00Z',
19
+ end_at: '2026-09-09T00:00:00Z',
20
+ color: 'BLUE',
21
+ scheduling_metadata: {
22
+ google_color: { version: 1, inherited: false, background: '#00ff88' },
23
+ },
24
+ },
25
+ ],
26
+ }),
27
+ }));
28
+ vi.mock('./settings/settings-context', () => ({
29
+ useCalendarSettings: () => ({
30
+ settings: {
31
+ appearance: { showWeekends: true },
32
+ timezone: { timezone: 'UTC' },
33
+ },
34
+ }),
35
+ }));
36
+ vi.mock('./location-timeline', () => ({ LocationTimeline: () => null }));
37
+
38
+ import { AllDayEventBar } from './all-day-event-bar';
39
+
40
+ it('keeps an opaque all-day fill and contrasting dashed cut-off edge', () => {
41
+ render(<AllDayEventBar dates={[new Date('2026-09-07T00:00:00Z')]} />);
42
+ const card = screen.getByText('Cut-off event').closest('.border-dashed');
43
+ expect(card).toHaveStyle({
44
+ backgroundColor: '#00ff88',
45
+ color: '#000000',
46
+ borderColor: '#000000',
47
+ });
48
+ });