@tuturuuu/ui 0.37.0 → 0.37.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.
- package/CHANGELOG.md +26 -0
- package/package.json +7 -5
- package/src/components/ui/custom/__tests__/workspace-picker-content.test.tsx +181 -0
- package/src/components/ui/custom/__tests__/workspace-select-helpers.test.ts +24 -38
- package/src/components/ui/custom/hidden-workspaces-settings.tsx +49 -0
- package/src/components/ui/custom/mobile-header-height.ts +16 -0
- package/src/components/ui/custom/structure.tsx +37 -5
- package/src/components/ui/custom/user-filters.tsx +2 -1
- package/src/components/ui/custom/visible-workspace-filter.tsx +41 -0
- package/src/components/ui/custom/workspace-picker-content.tsx +293 -0
- package/src/components/ui/custom/workspace-select-helpers.ts +11 -0
- package/src/components/ui/custom/workspace-select-invitations.tsx +3 -0
- package/src/components/ui/custom/workspace-select.tsx +87 -359
- package/src/components/ui/date-time-picker-timezone.test.tsx +258 -0
- package/src/components/ui/date-time-picker-values.ts +205 -0
- package/src/components/ui/date-time-picker.tsx +70 -167
- package/src/components/ui/legacy/calendar/agenda-view.tsx +29 -94
- package/src/components/ui/legacy/calendar/all-day-color.test.tsx +48 -0
- package/src/components/ui/legacy/calendar/all-day-event-bar.tsx +29 -41
- package/src/components/ui/legacy/calendar/calendar-cell-color.test.tsx +70 -0
- package/src/components/ui/legacy/calendar/calendar-cell.tsx +4 -5
- package/src/components/ui/legacy/calendar/calendar-content.test.tsx +82 -3
- package/src/components/ui/legacy/calendar/calendar-content.tsx +37 -34
- package/src/components/ui/legacy/calendar/calendar-day-bridge.test.tsx +94 -0
- package/src/components/ui/legacy/calendar/calendar-day-display.test.tsx +106 -0
- package/src/components/ui/legacy/calendar/calendar-header.tsx +14 -5
- package/src/components/ui/legacy/calendar/calendar-matrix.tsx +4 -9
- package/src/components/ui/legacy/calendar/calendar-views.test.tsx +120 -6
- package/src/components/ui/legacy/calendar/day-title.tsx +6 -6
- package/src/components/ui/legacy/calendar/dynamic-island.test.tsx +140 -0
- package/src/components/ui/legacy/calendar/dynamic-island.tsx +27 -24
- package/src/components/ui/legacy/calendar/event-card.test.tsx +206 -7
- package/src/components/ui/legacy/calendar/event-card.tsx +27 -49
- package/src/components/ui/legacy/calendar/event-modal.tsx +6 -26
- package/src/components/ui/legacy/calendar/event-picker-bounds.ts +16 -0
- package/src/components/ui/legacy/calendar/event-preview-popover.test.tsx +139 -0
- package/src/components/ui/legacy/calendar/event-preview-popover.tsx +13 -17
- package/src/components/ui/legacy/calendar/event-preview-time.ts +41 -0
- package/src/components/ui/legacy/calendar/location-timeline.tsx +9 -17
- package/src/components/ui/legacy/calendar/month-calendar.tsx +20 -10
- package/src/components/ui/legacy/calendar/settings/calendar-sync-dashboard.tsx +4 -23
- package/src/components/ui/legacy/calendar/settings/sync-logs-table.tsx +3 -6
- package/src/components/ui/legacy/calendar/time-indicator-line.tsx +3 -20
- package/src/components/ui/legacy/calendar/time-indicator.tsx +11 -3
- package/src/components/ui/legacy/calendar/weekday-bar.tsx +2 -8
- package/src/components/ui/legacy/calendar/year-calendar.tsx +27 -39
- package/src/components/ui/resizable.tsx +2 -1
- package/src/hooks/__tests__/use-calendar-sync.test.tsx +45 -0
- package/src/hooks/__tests__/use-visible-workspaces.test.tsx +76 -0
- package/src/hooks/__tests__/use-workspace-visibility.test.tsx +155 -0
- package/src/hooks/__tests__/workspace-actor-lifetime.test.tsx +103 -0
- package/src/hooks/calendar-event-lookup.test.ts +39 -0
- package/src/hooks/calendar-event-lookup.ts +22 -8
- package/src/hooks/calendar-sync-range.test.ts +92 -0
- package/src/hooks/calendar-sync-range.ts +40 -0
- package/src/hooks/use-calendar-clock.ts +49 -0
- package/src/hooks/use-calendar-day-zone.tsx +12 -0
- package/src/hooks/use-calendar-sync.tsx +42 -80
- package/src/hooks/use-calendar.tsx +3 -3
- package/src/hooks/use-visible-workspaces.ts +57 -0
- package/src/hooks/use-workspace-user.ts +15 -3
- package/src/hooks/use-workspace-visibility.ts +234 -0
- 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
|
|
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
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
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
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
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
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
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
|
-
|
|
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) =>
|
|
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
|
-
|
|
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(
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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
|
|
238
|
-
const
|
|
239
|
-
const
|
|
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
|
+
});
|