@agent-native/core 0.159.3 → 0.160.0

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 (90) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/templates/assets/server/lib/generation.ts +10 -6
  3. package/corpus/templates/calendar/.agents/skills/event-management/SKILL.md +18 -4
  4. package/corpus/templates/calendar/actions/create-event.ts +4 -2
  5. package/corpus/templates/calendar/actions/event-action-helpers.ts +56 -9
  6. package/corpus/templates/calendar/actions/update-event.ts +1 -0
  7. package/corpus/templates/calendar/agent-native.config.ts +1 -0
  8. package/corpus/templates/calendar/app/components/calendar/CreateEventDialog.tsx +8 -2
  9. package/corpus/templates/calendar/app/components/calendar/DayView.tsx +102 -68
  10. package/corpus/templates/calendar/app/components/calendar/EventDetailPopover.tsx +150 -49
  11. package/corpus/templates/calendar/app/components/calendar/MonthView.tsx +57 -23
  12. package/corpus/templates/calendar/app/components/calendar/OutOfOfficeEvent.tsx +4 -1
  13. package/corpus/templates/calendar/app/components/calendar/TimezoneSwitchDialog.tsx +70 -0
  14. package/corpus/templates/calendar/app/components/calendar/WeekView.tsx +127 -88
  15. package/corpus/templates/calendar/app/components/calendar/WorkingLocationEditor.tsx +59 -25
  16. package/corpus/templates/calendar/app/hooks/use-event-drag.ts +55 -18
  17. package/corpus/templates/calendar/app/i18n-data.ts +60 -0
  18. package/corpus/templates/calendar/app/lib/all-day-layout.ts +16 -8
  19. package/corpus/templates/calendar/app/lib/calendar-drafts.ts +56 -0
  20. package/corpus/templates/calendar/app/lib/calendar-timezone.ts +351 -0
  21. package/corpus/templates/calendar/app/lib/event-layout.ts +14 -8
  22. package/corpus/templates/calendar/app/lib/out-of-office.ts +29 -61
  23. package/corpus/templates/calendar/app/lib/working-location.ts +34 -2
  24. package/corpus/templates/calendar/app/pages/CalendarView.tsx +317 -139
  25. package/corpus/templates/calendar/changelog/2026-08-13-calendar-stays-pinned-to-the-saved-timezone-and-asks-before-.md +6 -0
  26. package/corpus/templates/calendar/changelog/2026-08-13-working-locations-can-be-added-from-calendar-days-with-a-ful.md +6 -0
  27. package/corpus/templates/calendar/changelog/2026-08-14-choosing-office-or-other-when-adding-a-working-location-creates.md +6 -0
  28. package/corpus/templates/calendar/changelog/2026-08-14-creating-an-other-working-location-now-keeps-the-custom-name.md +6 -0
  29. package/corpus/templates/calendar/changelog/2026-08-14-timed-working-locations-keep-home-office-or-custom-titles.md +6 -0
  30. package/corpus/templates/calendar/changelog/2026-08-14-timed-working-locations-now-keep-a-home-office-or-custom-title.md +6 -0
  31. package/corpus/templates/calendar/changelog/2026-08-14-turning-a-midnight-ending-working-location-back-to-all-day.md +6 -0
  32. package/corpus/templates/calendar/server/lib/google-calendar.ts +22 -1
  33. package/dist/agent/engine/builder-engine.d.ts +16 -2
  34. package/dist/agent/engine/builder-engine.js +178 -115
  35. package/dist/agent/engine/builtin.js +13 -4
  36. package/dist/agent/engine/credential-errors.d.ts +23 -0
  37. package/dist/agent/engine/credential-errors.js +19 -0
  38. package/dist/agent/engine/error-detail.d.ts +15 -0
  39. package/dist/agent/engine/error-detail.js +29 -0
  40. package/dist/agent/engine/registry.d.ts +14 -3
  41. package/dist/agent/engine/registry.js +106 -61
  42. package/dist/agent/engine/types.d.ts +16 -0
  43. package/dist/agent/engine/types.js +9 -0
  44. package/dist/agent/production-agent.d.ts +7 -0
  45. package/dist/agent/production-agent.js +58 -20
  46. package/dist/agent/run-manager.js +30 -0
  47. package/dist/agent/thread-data-builder.js +4 -0
  48. package/dist/agent/types.d.ts +9 -0
  49. package/dist/client/agent-chat-adapter.js +46 -15
  50. package/dist/client/chat-first/app-pane.js +1 -1
  51. package/dist/client/chat-first/copy.js +1 -0
  52. package/dist/client/error-format.js +25 -2
  53. package/dist/client/sse-event-processor.d.ts +2 -0
  54. package/dist/client/sse-event-processor.js +12 -1
  55. package/dist/feature-flags/index.d.ts +0 -2
  56. package/dist/feature-flags/index.js +3 -2
  57. package/dist/feature-flags/server.d.ts +2 -0
  58. package/dist/feature-flags/server.js +2 -0
  59. package/dist/index.d.ts +2 -1
  60. package/dist/index.js +2 -1
  61. package/dist/jobs/background-automation-runner.js +9 -15
  62. package/dist/mcp/screen-memory-stdio.d.ts +7 -7
  63. package/dist/notifications/routes.d.ts +3 -3
  64. package/dist/observability/routes.d.ts +2 -2
  65. package/dist/progress/routes.d.ts +1 -1
  66. package/dist/provider-api/actions/custom-provider-registration.d.ts +2 -2
  67. package/dist/resources/handlers.d.ts +1 -1
  68. package/dist/secrets/register-framework-secrets.js +25 -0
  69. package/dist/secrets/routes.d.ts +6 -6
  70. package/dist/server/agent-chat-plugin.js +9 -2
  71. package/dist/server/auth.js +10 -0
  72. package/dist/server/credential-provider.d.ts +80 -0
  73. package/dist/server/credential-provider.js +168 -1
  74. package/dist/server/google-realtime-session.d.ts +1 -1
  75. package/dist/server/google-realtime-session.js +6 -6
  76. package/dist/server/index.d.ts +2 -2
  77. package/dist/server/index.js +7 -2
  78. package/dist/server/onboarding-html.js +16 -257
  79. package/dist/server/realtime-token.d.ts +1 -1
  80. package/dist/server/realtime-voice.js +12 -6
  81. package/dist/server/transcribe-voice.d.ts +1 -1
  82. package/dist/server/transcribe-voice.js +18 -16
  83. package/dist/shared/auth-copy.d.ts +34 -0
  84. package/dist/shared/auth-copy.js +293 -0
  85. package/dist/shared/index.d.ts +1 -0
  86. package/dist/shared/index.js +1 -0
  87. package/dist/transcription/builder-transcription.js +8 -4
  88. package/docs/content/actions-advanced.mdx +1 -1
  89. package/docs/content/portal.mdx +21 -8
  90. package/package.json +3 -1
@@ -5,22 +5,16 @@ import {
5
5
  IconBuilding,
6
6
  IconHome,
7
7
  IconMapPin,
8
+ IconPlus,
8
9
  } from "@tabler/icons-react";
9
10
  import {
10
11
  startOfWeek,
11
12
  endOfWeek,
12
13
  eachDayOfInterval,
13
14
  eachHourOfInterval,
14
- isSameDay,
15
- isToday,
16
15
  format,
17
- parseISO,
18
- differenceInMinutes,
19
- startOfDay,
20
16
  set,
21
- addDays,
22
17
  addMinutes,
23
- min,
24
18
  } from "date-fns";
25
19
  import { useState, useEffect, useRef, useMemo, useCallback, memo } from "react";
26
20
 
@@ -42,6 +36,13 @@ import {
42
36
  layoutAllDayEvents,
43
37
  partitionAllDayEvents,
44
38
  } from "@/lib/all-day-layout";
39
+ import {
40
+ dateToCalendarDateKey,
41
+ getBrowserTimezone,
42
+ getDateKeyInTimezone,
43
+ getEventDateKey,
44
+ getEventSegmentForCalendarDay,
45
+ } from "@/lib/calendar-timezone";
45
46
  import { getEventDisplayColor, allOtherDeclined } from "@/lib/event-colors";
46
47
  import {
47
48
  computeTimedEventLayout,
@@ -72,6 +73,7 @@ import { shouldRenderWeekDragSegment } from "./week-drag-segment";
72
73
  interface WeekViewProps {
73
74
  events: CalendarEvent[];
74
75
  selectedDate: Date;
76
+ timezone?: string;
75
77
  onDateSelect: (date: Date) => void;
76
78
  onDeleteEvent: (eventId: string) => void;
77
79
  onEventTimeChange?: (eventId: string, newStart: Date, newEnd: Date) => void;
@@ -81,6 +83,7 @@ interface WeekViewProps {
81
83
  endTime: string,
82
84
  options?: { explicitDuration?: boolean },
83
85
  ) => void;
86
+ onCreateWorkingLocation?: (date: Date) => void;
84
87
  quickEditEventId?: string | null;
85
88
  onQuickEditSave?: (
86
89
  eventId: string,
@@ -148,7 +151,7 @@ function minutesToTimeString(totalMinutes: number): string {
148
151
  /** Convert minutes-from-START_HOUR on a given day into a Date, for ghost label formatting */
149
152
  function minutesToDate(day: Date, totalMinutes: number): Date {
150
153
  return addMinutes(
151
- set(startOfDay(day), { hours: START_HOUR, minutes: 0, seconds: 0 }),
154
+ set(day, { hours: START_HOUR, minutes: 0, seconds: 0 }),
152
155
  totalMinutes,
153
156
  );
154
157
  }
@@ -173,15 +176,10 @@ function formatEventTime(start: Date, end: Date): string {
173
176
  return `${startWithAmPm}\u2013${endStr}`;
174
177
  }
175
178
 
176
- function getSegmentStyle(event: CalendarEvent, day: Date) {
177
- const evStart = parseISO(event.start);
178
- const evEnd = parseISO(event.end);
179
- const dayBase = set(startOfDay(day), { hours: START_HOUR });
180
- const dayEnd = addDays(dayBase, 1);
181
- const segStart = evStart > dayBase ? evStart : dayBase;
182
- const segEnd = min([evEnd, dayEnd]);
183
- const topMinutes = Math.max(0, differenceInMinutes(segStart, dayBase));
184
- const durationMinutes = Math.max(15, differenceInMinutes(segEnd, segStart));
179
+ function getSegmentStyle(event: CalendarEvent, day: Date, timezone: string) {
180
+ const segment = getEventSegmentForCalendarDay(event, day, timezone);
181
+ const topMinutes = segment?.topMinutes ?? 0;
182
+ const durationMinutes = Math.max(15, segment?.durationMinutes ?? 15);
185
183
  return {
186
184
  top: `${(topMinutes / 60) * HOUR_HEIGHT}px`,
187
185
  height: `${(durationMinutes / 60) * HOUR_HEIGHT}px`,
@@ -192,6 +190,7 @@ interface WeekEventCardProps {
192
190
  event: CalendarEvent;
193
191
  day: Date;
194
192
  dayIndex: number;
193
+ timezone: string;
195
194
  layout: Map<string, TimedEventLayout>;
196
195
  now: Date;
197
196
  prefs: ViewPreferences;
@@ -246,6 +245,7 @@ const WeekEventCard = memo(function WeekEventCard({
246
245
  event,
247
246
  day,
248
247
  dayIndex,
248
+ timezone,
249
249
  layout,
250
250
  now,
251
251
  prefs,
@@ -272,6 +272,9 @@ const WeekEventCard = memo(function WeekEventCard({
272
272
  onPopoverOpenChange,
273
273
  }: WeekEventCardProps) {
274
274
  const t = useT();
275
+ const workingLocationLabels = createWorkingLocationDisplayLabels(t);
276
+ const title = getWorkingLocationChipLabel(event, workingLocationLabels);
277
+ const ariaTitle = getWorkingLocationTitle(event, workingLocationLabels);
275
278
  const li = layout.get(event.id) ?? {
276
279
  left: 0,
277
280
  width: 100,
@@ -284,12 +287,10 @@ const WeekEventCard = memo(function WeekEventCard({
284
287
  overrideTop !== null && overrideHeight !== null && overrideDayIndex !== null
285
288
  ? { top: overrideTop, height: overrideHeight, dayIndex: overrideDayIndex }
286
289
  : null;
287
- const start = parseISO(event.start);
288
- const end = parseISO(event.end);
289
- const dayBase = startOfDay(day);
290
- const segDayEnd = addDays(dayBase, 1);
291
- const isStart = isSameDay(start, day);
292
- const isEnd = end <= segDayEnd;
290
+ const segment = getEventSegmentForCalendarDay(event, day, timezone);
291
+ const isStart =
292
+ getEventDateKey(event, timezone) === dateToCalendarDateKey(day);
293
+ const isEnd = segment?.endsOnDay ?? true;
293
294
  const isDragPreviewSegment =
294
295
  isBeingDragged && overrides?.dayIndex === dayIndex;
295
296
  const segmentStartsHere = isStart || isDragPreviewSegment;
@@ -321,26 +322,19 @@ const WeekEventCard = memo(function WeekEventCard({
321
322
  top: `${overrides.top}px`,
322
323
  height: `${overrides.height}px`,
323
324
  }
324
- : getSegmentStyle(event, day);
325
+ : getSegmentStyle(event, day, timezone);
325
326
  const color = getEventDisplayColor(event, prefs);
326
- const segStart = isStart ? start : dayBase;
327
- const segEnd = min([end, segDayEnd]);
328
327
  const durationMin = overrides
329
328
  ? (overrides.height / HOUR_HEIGHT) * 60
330
- : differenceInMinutes(segEnd, segStart);
329
+ : (segment?.durationMinutes ?? 15);
331
330
  // Compute display times (use drag overrides if active)
332
331
  const displayStart = overrides
333
- ? addMinutes(
334
- set(startOfDay(day), {
335
- hours: START_HOUR,
336
- minutes: 0,
337
- seconds: 0,
338
- }),
339
- (overrides.top / HOUR_HEIGHT) * 60,
340
- )
341
- : start;
342
- const displayEnd = overrides ? addMinutes(displayStart, durationMin) : end;
343
- const isPast = end < now;
332
+ ? minutesToDate(day, START_HOUR * 60 + (overrides.top / HOUR_HEIGHT) * 60)
333
+ : minutesToDate(day, segment?.startMinutes ?? 0);
334
+ const displayEnd = overrides
335
+ ? addMinutes(displayStart, durationMin)
336
+ : minutesToDate(day, segment?.endMinutes ?? durationMin);
337
+ const isPast = new Date(event.end) < now;
344
338
  const isDeclined = event.responseStatus === "declined";
345
339
  const allOthersOut = allOtherDeclined(event);
346
340
 
@@ -367,10 +361,8 @@ const WeekEventCard = memo(function WeekEventCard({
367
361
  )}
368
362
  aria-label={
369
363
  event.ownerName || event.overlayEmail
370
- ? `${event.title}, ${
371
- event.ownerName || event.overlayEmail
372
- }'s calendar`
373
- : event.title
364
+ ? `${ariaTitle}, ${event.ownerName || event.overlayEmail}'s calendar`
365
+ : ariaTitle
374
366
  }
375
367
  style={{
376
368
  ...style,
@@ -429,7 +421,7 @@ const WeekEventCard = memo(function WeekEventCard({
429
421
  !isPast && !isDeclined && "font-semibold",
430
422
  )}
431
423
  >
432
- {event.title}
424
+ {title}
433
425
  </span>
434
426
  </div>
435
427
  ) : (
@@ -451,7 +443,7 @@ const WeekEventCard = memo(function WeekEventCard({
451
443
  />
452
444
  )}
453
445
  <EventStatusIcon event={event} className="shrink-0" />
454
- <span className="truncate">{event.title}</span>
446
+ <span className="truncate">{title}</span>
455
447
  </div>
456
448
  {segmentStartsHere && (
457
449
  <div
@@ -502,6 +494,7 @@ const WeekEventCard = memo(function WeekEventCard({
502
494
  return (
503
495
  <EventDetailPopover
504
496
  event={event}
497
+ timezone={timezone}
505
498
  onDelete={onDeleteEvent}
506
499
  isDraft={isDraft}
507
500
  defaultOpen={defaultOpen}
@@ -548,10 +541,12 @@ const WeekCreateGhost = memo(function WeekCreateGhost({
548
541
  export const WeekView = memo(function WeekView({
549
542
  events,
550
543
  selectedDate,
544
+ timezone = getBrowserTimezone(),
551
545
  onDateSelect,
552
546
  onDeleteEvent,
553
547
  onEventTimeChange,
554
548
  onClickTimeSlot,
549
+ onCreateWorkingLocation,
555
550
  quickEditEventId,
556
551
  onQuickEditSave,
557
552
  onQuickEditCancel,
@@ -664,8 +659,8 @@ export const WeekView = memo(function WeekView({
664
659
  [allDayEvents],
665
660
  );
666
661
  const workingLocationLayout = useMemo(
667
- () => layoutAllDayEvents(workingLocations, days),
668
- [days, workingLocations],
662
+ () => layoutAllDayEvents(workingLocations, days, timezone),
663
+ [days, timezone, workingLocations],
669
664
  );
670
665
  const workingLocationGroups = useMemo(
671
666
  () =>
@@ -684,29 +679,45 @@ export const WeekView = memo(function WeekView({
684
679
  [prefs, workingLocationLabels, workingLocationLayout.placements],
685
680
  );
686
681
  const regularAllDayLayout = useMemo(() => {
687
- return layoutAllDayEvents(regularEvents, days);
688
- }, [days, regularEvents]);
682
+ return layoutAllDayEvents(regularEvents, days, timezone);
683
+ }, [days, regularEvents, timezone]);
689
684
 
690
685
  // Pre-compute timed events per day with layout — include events spanning into this day
691
686
  const dayData = useMemo(() => {
692
687
  return days.map((day) => {
693
- const dayStart = startOfDay(day);
694
- const dayEnd = addDays(dayStart, 1);
695
- const dayEvents = timedEvents.filter((e) => {
696
- const evStart = parseISO(e.start);
697
- const evEnd = parseISO(e.end);
698
- return evStart < dayEnd && evEnd > dayStart;
699
- });
700
- const layout = computeTimedEventLayout(dayEvents, day);
688
+ const dayEvents = timedEvents.filter((event) =>
689
+ getEventSegmentForCalendarDay(event, day, timezone),
690
+ );
691
+ const layout = computeTimedEventLayout(dayEvents, day, timezone);
701
692
  return { day, events: dayEvents, layout };
702
693
  });
703
- }, [days, timedEvents]);
694
+ }, [days, timedEvents, timezone]);
704
695
 
705
696
  // Current time indicator
706
- const nowMinutes = (now.getHours() - START_HOUR) * 60 + now.getMinutes();
697
+ const nowParts = getDateKeyInTimezone(now, timezone)
698
+ ? new Intl.DateTimeFormat("en-US", {
699
+ timeZone: timezone,
700
+ hourCycle: "h23",
701
+ hour: "2-digit",
702
+ minute: "2-digit",
703
+ })
704
+ .formatToParts(now)
705
+ .reduce(
706
+ (parts, part) => {
707
+ if (part.type === "hour") parts.hour = Number(part.value);
708
+ if (part.type === "minute") parts.minute = Number(part.value);
709
+ return parts;
710
+ },
711
+ { hour: 0, minute: 0 },
712
+ )
713
+ : null;
714
+ const nowMinutes = nowParts
715
+ ? (nowParts.hour - START_HOUR) * 60 + nowParts.minute
716
+ : -1;
707
717
  const nowTop = (nowMinutes / 60) * HOUR_HEIGHT;
708
718
  const showNowIndicator =
709
719
  nowMinutes >= 0 && nowMinutes <= (END_HOUR - START_HOUR) * 60;
720
+ const currentDateKey = getDateKeyInTimezone(now, timezone);
710
721
 
711
722
  const hasWorkingLocations = workingLocationLayout.rowCount > 0;
712
723
  const hasRegularAllDayEvents = regularAllDayLayout.rowCount > 0;
@@ -774,7 +785,10 @@ export const WeekView = memo(function WeekView({
774
785
  function nameForToken(token: "shortGeneric" | "longGeneric" | "short") {
775
786
  try {
776
787
  return (
777
- new Intl.DateTimeFormat("en-US", { timeZoneName: token })
788
+ new Intl.DateTimeFormat("en-US", {
789
+ timeZone: timezone,
790
+ timeZoneName: token,
791
+ })
778
792
  .formatToParts(now)
779
793
  .find((p) => p.type === "timeZoneName")?.value ?? ""
780
794
  );
@@ -783,10 +797,7 @@ export const WeekView = memo(function WeekView({
783
797
  }
784
798
  }
785
799
 
786
- let iana = "";
787
- try {
788
- iana = Intl.DateTimeFormat().resolvedOptions().timeZone ?? "";
789
- } catch {}
800
+ const iana = timezone;
790
801
 
791
802
  const longGeneric = nameForToken("longGeneric");
792
803
  let shortGeneric = nameForToken("shortGeneric");
@@ -804,7 +815,7 @@ export const WeekView = memo(function WeekView({
804
815
  tzLong: longGeneric || iana,
805
816
  tzIana: iana,
806
817
  };
807
- }, []);
818
+ }, [now, timezone]);
808
819
 
809
820
  // Drag-to-move and drag-to-resize
810
821
  const handleEventTimeChange = useCallback(
@@ -827,6 +838,7 @@ export const WeekView = memo(function WeekView({
827
838
  days,
828
839
  onEventTimeChange: handleEventTimeChange,
829
840
  events,
841
+ timezone,
830
842
  });
831
843
 
832
844
  const canDrag = !!onEventTimeChange;
@@ -934,30 +946,53 @@ export const WeekView = memo(function WeekView({
934
946
  </div>
935
947
 
936
948
  {/* Day columns */}
937
- {days.map((day) => (
938
- <div
939
- key={day.toISOString()}
940
- onClick={() => onDateSelect(day)}
941
- className={cn(
942
- "flex flex-1 cursor-pointer flex-col items-center justify-center gap-0.5 border-r border-border py-1.5 sm:flex-row sm:gap-1.5 sm:py-2.5 last:border-r-0",
943
- isToday(day) ? "bg-primary/5" : "hover:bg-accent/40",
944
- )}
945
- >
946
- <span className="text-[10px] font-medium text-muted-foreground sm:text-xs">
947
- {isMobile ? format(day, "EEEEE") : format(day, "EEE")}
948
- </span>
949
- <span
949
+ {days.map((day) => {
950
+ const isCurrentDay = dateToCalendarDateKey(day) === currentDateKey;
951
+ return (
952
+ <div
953
+ key={day.toISOString()}
954
+ onClick={() => onDateSelect(day)}
950
955
  className={cn(
951
- "flex h-6 w-6 items-center justify-center rounded-full text-xs font-semibold sm:h-7 sm:w-7 sm:text-sm",
952
- isToday(day)
953
- ? "bg-foreground text-background"
954
- : "text-foreground",
956
+ "group/day relative flex flex-1 cursor-pointer flex-col items-center justify-center gap-0.5 border-r border-border py-1.5 sm:flex-row sm:gap-1.5 sm:py-2.5 last:border-r-0",
957
+ isCurrentDay ? "bg-primary/5" : "hover:bg-accent/40",
955
958
  )}
956
959
  >
957
- {format(day, "d")}
958
- </span>
959
- </div>
960
- ))}
960
+ <span className="text-[10px] font-medium text-muted-foreground sm:text-xs">
961
+ {isMobile ? format(day, "EEEEE") : format(day, "EEE")}
962
+ </span>
963
+ <span
964
+ className={cn(
965
+ "flex h-6 w-6 items-center justify-center rounded-full text-xs font-semibold sm:h-7 sm:w-7 sm:text-sm",
966
+ isCurrentDay
967
+ ? "bg-foreground text-background"
968
+ : "text-foreground",
969
+ )}
970
+ >
971
+ {format(day, "d")}
972
+ </span>
973
+ {onCreateWorkingLocation && (
974
+ <Tooltip>
975
+ <TooltipTrigger asChild>
976
+ <button
977
+ type="button"
978
+ aria-label={t("calendarView.addWorkingLocation")}
979
+ className="absolute right-1 top-1 flex size-5 items-center justify-center rounded text-muted-foreground opacity-0 transition-opacity hover:bg-accent hover:text-foreground focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring group-hover/day:opacity-100"
980
+ onClick={(event) => {
981
+ event.stopPropagation();
982
+ onCreateWorkingLocation(day);
983
+ }}
984
+ >
985
+ <IconPlus aria-hidden="true" className="size-3.5" />
986
+ </button>
987
+ </TooltipTrigger>
988
+ <TooltipContent side="bottom">
989
+ {t("calendarView.addWorkingLocation")}
990
+ </TooltipContent>
991
+ </Tooltip>
992
+ )}
993
+ </div>
994
+ );
995
+ })}
961
996
  {timeGridScrollbarWidth > 0 && (
962
997
  <div
963
998
  aria-hidden="true"
@@ -1065,6 +1100,7 @@ export const WeekView = memo(function WeekView({
1065
1100
  <EventDetailPopover
1066
1101
  key={`${event.overlayEmail ?? event.accountEmail ?? "primary"}:${event.id}`}
1067
1102
  event={event}
1103
+ timezone={timezone}
1068
1104
  onDelete={onDeleteEvent}
1069
1105
  isDraft={draftEventIds.includes(event.id)}
1070
1106
  defaultOpen={quickEditEventId === event.id}
@@ -1145,6 +1181,7 @@ export const WeekView = memo(function WeekView({
1145
1181
  <EventDetailPopover
1146
1182
  key={`${event.overlayEmail ?? event.accountEmail ?? "primary"}:${event.id}`}
1147
1183
  event={event}
1184
+ timezone={timezone}
1148
1185
  onDelete={onDeleteEvent}
1149
1186
  isDraft={draftEventIds.includes(event.id)}
1150
1187
  defaultOpen={quickEditEventId === event.id}
@@ -1245,7 +1282,7 @@ export const WeekView = memo(function WeekView({
1245
1282
 
1246
1283
  {/* Day columns */}
1247
1284
  {dayData.map(({ day, events: dayEvents, layout }, dayIndex) => {
1248
- const isCurrentDay = isToday(day);
1285
+ const isCurrentDay = dateToCalendarDateKey(day) === currentDateKey;
1249
1286
 
1250
1287
  // Collect events that were dragged into this column from another day
1251
1288
  const draggedInEvents: CalendarEvent[] = [];
@@ -1265,7 +1302,7 @@ export const WeekView = memo(function WeekView({
1265
1302
  (event) => {
1266
1303
  const overrides = getDragOverrides(event.id);
1267
1304
  return (
1268
- getOutOfOfficeSegment(event, day) !== null ||
1305
+ getOutOfOfficeSegment(event, day, timezone) !== null ||
1269
1306
  (dragEventId === event.id && overrides?.dayIndex === dayIndex)
1270
1307
  );
1271
1308
  },
@@ -1353,12 +1390,13 @@ export const WeekView = memo(function WeekView({
1353
1390
  const isBeingDragged = dragEventId === event.id;
1354
1391
  const overrides = getDragOverrides(event.id);
1355
1392
  const canonicalDayIndex =
1356
- getFirstVisibleOutOfOfficeDayIndex(event, days);
1393
+ getFirstVisibleOutOfOfficeDayIndex(event, days, timezone);
1357
1394
  return (
1358
1395
  <OutOfOfficeEvent
1359
1396
  key={`${event._tempId ?? event.id}:${day.toISOString()}`}
1360
1397
  event={event}
1361
1398
  day={day}
1399
+ timezone={timezone}
1362
1400
  hourHeight={HOUR_HEIGHT}
1363
1401
  color={
1364
1402
  getEventDisplayColor(event, prefs) ??
@@ -1451,6 +1489,7 @@ export const WeekView = memo(function WeekView({
1451
1489
  event={event}
1452
1490
  day={day}
1453
1491
  dayIndex={dayIndex}
1492
+ timezone={timezone}
1454
1493
  layout={layout}
1455
1494
  now={now}
1456
1495
  prefs={prefs}
@@ -21,6 +21,7 @@ import {
21
21
  interface WorkingLocationEditorProps {
22
22
  event: CalendarEvent;
23
23
  isRecurring: boolean;
24
+ isDraft?: boolean;
24
25
  readOnly?: boolean;
25
26
  disabled?: boolean;
26
27
  onSave: (selection: WorkingLocationSelection) => void;
@@ -40,6 +41,7 @@ function initialLabels(event: CalendarEvent) {
40
41
  export function WorkingLocationEditor({
41
42
  event,
42
43
  isRecurring,
44
+ isDraft = false,
43
45
  readOnly = false,
44
46
  disabled = false,
45
47
  onSave,
@@ -54,18 +56,42 @@ export function WorkingLocationEditor({
54
56
  const detail = getWorkingLocationDetail(event, workingLocationLabels);
55
57
 
56
58
  useEffect(() => {
57
- setType(getWorkingLocationType(event));
58
- setLabels(initialLabels(event));
59
- setScope("single");
60
- }, [currentLabel, currentType, event.id]);
59
+ setType(currentType);
60
+ setLabels((current) => {
61
+ const incoming = initialLabels(event);
62
+ if (!isDraft) return incoming;
63
+ return {
64
+ officeLocation:
65
+ currentType === "officeLocation"
66
+ ? incoming.officeLocation
67
+ : current.officeLocation,
68
+ customLocation:
69
+ currentType === "customLocation"
70
+ ? incoming.customLocation
71
+ : current.customLocation,
72
+ };
73
+ });
74
+ if (!isDraft) setScope("single");
75
+ }, [currentLabel, currentType, event.id, isDraft]);
61
76
 
62
77
  const label = type === "homeOffice" ? "" : labels[type];
63
- const isValid = type === "homeOffice" || label.trim().length > 0;
78
+ const isValid = type !== "customLocation" || label.trim().length > 0;
64
79
  const isDirty = useMemo(
65
80
  () => type !== currentType || label.trim() !== currentLabel.trim(),
66
81
  [currentLabel, currentType, label, type],
67
82
  );
68
83
 
84
+ const commitDraft = (
85
+ nextType: WorkingLocationKind,
86
+ nextLabels: Record<LabeledWorkingLocation, string>,
87
+ ) => {
88
+ if (!isDraft) return;
89
+ onSave({
90
+ type: nextType,
91
+ label: nextType === "homeOffice" ? "" : nextLabels[nextType],
92
+ });
93
+ };
94
+
69
95
  if (readOnly) {
70
96
  return (
71
97
  <div className="flex items-start gap-3 py-1.5">
@@ -108,7 +134,11 @@ export function WorkingLocationEditor({
108
134
  </div>
109
135
  <RadioGroup
110
136
  value={type}
111
- onValueChange={(value) => setType(value as WorkingLocationKind)}
137
+ onValueChange={(value) => {
138
+ const nextType = value as WorkingLocationKind;
139
+ setType(nextType);
140
+ commitDraft(nextType, labels);
141
+ }}
112
142
  className="grid grid-cols-3 gap-1.5"
113
143
  aria-label={t("eventForm.workingLocation")}
114
144
  disabled={disabled}
@@ -143,12 +173,14 @@ export function WorkingLocationEditor({
143
173
  <div className="mt-2">
144
174
  <Input
145
175
  value={labels[type]}
146
- onChange={(event) =>
147
- setLabels((current) => ({
148
- ...current,
149
- [type]: event.target.value,
150
- }))
151
- }
176
+ onChange={(inputEvent) => {
177
+ const nextLabels = {
178
+ ...labels,
179
+ [type]: inputEvent.target.value,
180
+ };
181
+ setLabels(nextLabels);
182
+ commitDraft(type, nextLabels);
183
+ }}
152
184
  aria-label={
153
185
  type === "officeLocation"
154
186
  ? t("eventForm.office")
@@ -166,7 +198,7 @@ export function WorkingLocationEditor({
166
198
  </div>
167
199
  )}
168
200
 
169
- {isRecurring && (
201
+ {isRecurring && !isDraft && (
170
202
  <div className="mt-2">
171
203
  <p className="mb-1 text-[10px] font-medium text-muted-foreground">
172
204
  {t("eventForm.applyTo")}
@@ -205,18 +237,20 @@ export function WorkingLocationEditor({
205
237
  </div>
206
238
  )}
207
239
 
208
- <div className="mt-2 flex justify-end">
209
- <Button
210
- size="sm"
211
- className="h-7 text-xs"
212
- disabled={disabled || !isValid || !isDirty}
213
- onClick={() =>
214
- onSave({ type, label, scope: isRecurring ? scope : undefined })
215
- }
216
- >
217
- {t("eventForm.save")}
218
- </Button>
219
- </div>
240
+ {!isDraft && (
241
+ <div className="mt-2 flex justify-end">
242
+ <Button
243
+ size="sm"
244
+ className="h-7 text-xs"
245
+ disabled={disabled || !isValid || !isDirty}
246
+ onClick={() =>
247
+ onSave({ type, label, scope: isRecurring ? scope : undefined })
248
+ }
249
+ >
250
+ {t("eventForm.save")}
251
+ </Button>
252
+ </div>
253
+ )}
220
254
  </div>
221
255
  );
222
256
  }