@agent-native/core 0.98.8 → 0.98.9

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 (35) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +6 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/mcp/build-server.ts +99 -26
  5. package/corpus/templates/calendar/.agents/skills/event-management/SKILL.md +31 -2
  6. package/corpus/templates/calendar/AGENTS.md +11 -0
  7. package/corpus/templates/calendar/README.md +2 -1
  8. package/corpus/templates/calendar/actions/create-event.ts +7 -6
  9. package/corpus/templates/calendar/actions/event-action-helpers.ts +49 -0
  10. package/corpus/templates/calendar/actions/update-event.ts +225 -14
  11. package/corpus/templates/calendar/app/components/calendar/DayView.tsx +185 -70
  12. package/corpus/templates/calendar/app/components/calendar/EventCard.tsx +26 -4
  13. package/corpus/templates/calendar/app/components/calendar/EventDetailPanel.tsx +112 -69
  14. package/corpus/templates/calendar/app/components/calendar/EventDetailPopover.tsx +621 -524
  15. package/corpus/templates/calendar/app/components/calendar/WeekView.tsx +275 -164
  16. package/corpus/templates/calendar/app/components/calendar/WorkingLocationEditor.tsx +222 -0
  17. package/corpus/templates/calendar/app/hooks/use-events.ts +151 -79
  18. package/corpus/templates/calendar/app/i18n/zh-TW.ts +6 -0
  19. package/corpus/templates/calendar/app/i18n-data.ts +60 -0
  20. package/corpus/templates/calendar/app/lib/all-day-layout.ts +126 -0
  21. package/corpus/templates/calendar/app/lib/event-form-utils.ts +18 -1
  22. package/corpus/templates/calendar/app/lib/event-mutation-inputs.ts +1 -1
  23. package/corpus/templates/calendar/app/lib/working-location.ts +163 -0
  24. package/corpus/templates/calendar/app/pages/CalendarView.tsx +21 -2
  25. package/corpus/templates/calendar/changelog/2026-07-07-working-locations-from-google-calendar-now-appear-as-native-.md +6 -0
  26. package/corpus/templates/calendar/server/lib/calendar-availability.ts +2 -0
  27. package/corpus/templates/calendar/server/lib/google-api.ts +6 -1
  28. package/corpus/templates/calendar/server/lib/google-calendar.ts +49 -19
  29. package/corpus/templates/calendar/shared/api.ts +2 -0
  30. package/dist/mcp/build-server.d.ts.map +1 -1
  31. package/dist/mcp/build-server.js +82 -28
  32. package/dist/mcp/build-server.js.map +1 -1
  33. package/dist/observability/routes.d.ts +5 -5
  34. package/dist/secrets/routes.d.ts +9 -9
  35. package/package.json +1 -1
@@ -13,6 +13,7 @@ import {
13
13
  attachmentsInput,
14
14
  attendeesInput,
15
15
  buildReminderOverrides,
16
+ buildStatusEventFields,
16
17
  cliBoolean,
17
18
  googleColorIdInput,
18
19
  normalizeAttendees,
@@ -23,7 +24,9 @@ import {
23
24
  remindersInput,
24
25
  requireActionUserEmail,
25
26
  resolveOwnedAccountEmail,
27
+ validateStatusEventTiming,
26
28
  visibilityInput,
29
+ workingLocationTypeInput,
27
30
  } from "./event-action-helpers.js";
28
31
 
29
32
  function mergeAttendees(
@@ -64,6 +67,16 @@ function mergeAttendees(
64
67
  return Array.from(merged.values());
65
68
  }
66
69
 
70
+ function workingLocationTitle(
71
+ properties: NonNullable<CalendarEvent["workingLocationProperties"]>,
72
+ ): string {
73
+ if (properties.type === "homeOffice") return "Home";
74
+ if (properties.type === "officeLocation") {
75
+ return properties.officeLocation?.label || "Office";
76
+ }
77
+ return properties.customLocation?.label || "Working location";
78
+ }
79
+
67
80
  export default defineAction({
68
81
  description:
69
82
  "Update a Google Calendar event. Supports title, description, location, time, event color, attachments, reminders, and recurrence rules such as RRULE:FREQ=DAILY;BYDAY=MO,TU,WE,TH,FR.",
@@ -80,6 +93,15 @@ export default defineAction({
80
93
  title: z.string().optional().describe("New event title"),
81
94
  description: z.string().optional().describe("New event description"),
82
95
  location: z.string().optional().describe("New event location"),
96
+ workingLocationType: workingLocationTypeInput.describe(
97
+ "For existing working-location events: homeOffice, officeLocation, or customLocation. Google Calendar event types cannot be changed after creation.",
98
+ ),
99
+ workingLocationLabel: z
100
+ .string()
101
+ .optional()
102
+ .describe(
103
+ "For existing working-location events: label shown in Google Calendar.",
104
+ ),
83
105
  start: z.string().optional().describe("New start time/date as ISO string"),
84
106
  end: z.string().optional().describe("New end time/date as ISO string"),
85
107
  startTimeZone: z
@@ -92,10 +114,10 @@ export default defineAction({
92
114
  .describe("IANA timezone for the event end, e.g. America/New_York"),
93
115
  allDay: cliBoolean.optional().describe("Whether the event is all-day"),
94
116
  transparency: availabilityInput.describe(
95
- "Google Calendar availability: opaque blocks time (Busy), transparent does not block time (Free).",
117
+ "Google Calendar availability: opaque blocks time (Busy), transparent does not block time (Free). Existing working-location events are always sent to Google as transparent.",
96
118
  ),
97
119
  visibility: visibilityInput.describe(
98
- "Google Calendar visibility: default, public, private, or confidential.",
120
+ "Google Calendar visibility: default, public, private, or confidential. Existing working-location events are always sent to Google as public.",
99
121
  ),
100
122
  status: z
101
123
  .enum(["confirmed", "tentative", "cancelled"])
@@ -193,6 +215,13 @@ export default defineAction({
193
215
  reminderMethod: args.reminderMethod,
194
216
  useDefaultReminders: args.remindersUseDefault,
195
217
  });
218
+ const hasWorkingLocationPatch =
219
+ args.workingLocationType !== undefined ||
220
+ args.workingLocationLabel !== undefined;
221
+ const hasTimePatch =
222
+ args.start !== undefined ||
223
+ args.end !== undefined ||
224
+ args.allDay !== undefined;
196
225
 
197
226
  const attendeesToAdd = normalizeAttendees(args.addAttendees);
198
227
  let attendees = normalizeAttendees(args.attendees);
@@ -216,7 +245,8 @@ export default defineAction({
216
245
  attendeesToAdd !== undefined ||
217
246
  Object.keys(reminderFields).length > 0 ||
218
247
  args.addGoogleMeet === true ||
219
- args.addZoom === true;
248
+ args.addZoom === true ||
249
+ hasWorkingLocationPatch;
220
250
 
221
251
  if (!hasPatch) {
222
252
  throw new Error("No event updates provided.");
@@ -253,6 +283,91 @@ export default defineAction({
253
283
  return existingEvent;
254
284
  };
255
285
 
286
+ if (args.location !== undefined && !hasWorkingLocationPatch) {
287
+ const existingEvent = await loadExistingEvent();
288
+ if (existingEvent.eventType === "workingLocation") {
289
+ throw new Error(
290
+ "Working-location events do not support a generic location. Use workingLocationType and workingLocationLabel instead.",
291
+ );
292
+ }
293
+ }
294
+
295
+ if (hasTimePatch) {
296
+ const existingEvent = await loadExistingEvent();
297
+ const existingStatusEventType =
298
+ existingEvent.eventType === "outOfOffice" ||
299
+ existingEvent.eventType === "focusTime" ||
300
+ existingEvent.eventType === "workingLocation"
301
+ ? existingEvent.eventType
302
+ : "default";
303
+ validateStatusEventTiming({
304
+ eventType: existingStatusEventType,
305
+ allDay: args.allDay ?? existingEvent.allDay,
306
+ start: args.start ?? existingEvent.start,
307
+ end: args.end ?? existingEvent.end,
308
+ });
309
+ if (
310
+ existingEvent.eventType === "workingLocation" &&
311
+ existingEvent.workingLocationProperties
312
+ ) {
313
+ Object.assign(updates, {
314
+ eventType: "workingLocation" as const,
315
+ transparency: "transparent" as const,
316
+ visibility: "public" as const,
317
+ workingLocationProperties: existingEvent.workingLocationProperties,
318
+ });
319
+ }
320
+ }
321
+
322
+ if (hasWorkingLocationPatch) {
323
+ const existingEvent = await loadExistingEvent();
324
+ if (existingEvent.eventType !== "workingLocation") {
325
+ throw new Error(
326
+ "Working location details can only be updated on existing working-location events. Google Calendar event types cannot be changed after creation.",
327
+ );
328
+ }
329
+ const nextWorkingLocationType =
330
+ args.workingLocationType ??
331
+ existingEvent.workingLocationProperties?.type ??
332
+ "customLocation";
333
+ const existingWorkingLocationLabel =
334
+ existingEvent.workingLocationProperties?.type === "officeLocation"
335
+ ? existingEvent.workingLocationProperties.officeLocation?.label
336
+ : existingEvent.workingLocationProperties?.type === "customLocation"
337
+ ? existingEvent.workingLocationProperties.customLocation?.label
338
+ : undefined;
339
+ const nextWorkingLocationLabel =
340
+ args.workingLocationLabel ??
341
+ args.location ??
342
+ existingWorkingLocationLabel ??
343
+ existingEvent.title;
344
+ const workingLocationFields = buildStatusEventFields({
345
+ eventType: "workingLocation",
346
+ title: args.title ?? existingEvent.title,
347
+ workingLocationType: nextWorkingLocationType,
348
+ workingLocationLabel: nextWorkingLocationLabel,
349
+ });
350
+ const nextWorkingLocationProperties =
351
+ nextWorkingLocationType === "officeLocation"
352
+ ? {
353
+ type: "officeLocation" as const,
354
+ officeLocation: {
355
+ ...(existingEvent.workingLocationProperties?.type ===
356
+ "officeLocation"
357
+ ? existingEvent.workingLocationProperties.officeLocation
358
+ : {}),
359
+ label: nextWorkingLocationLabel,
360
+ },
361
+ }
362
+ : workingLocationFields.workingLocationProperties;
363
+ Object.assign(updates, {
364
+ transparency: "transparent",
365
+ visibility: "public",
366
+ workingLocationProperties: nextWorkingLocationProperties,
367
+ });
368
+ delete updates.location;
369
+ }
370
+
256
371
  if (attendeesToAdd !== undefined) {
257
372
  const existingEvent = await loadExistingEvent();
258
373
  attendees = mergeAttendees(existingEvent.attendees, attendeesToAdd);
@@ -296,16 +411,109 @@ export default defineAction({
296
411
  };
297
412
  }
298
413
 
299
- const result = await googleCalendar.updateEvent(googleEventId, updates, {
300
- account: { ownerEmail, accountEmail },
301
- sendUpdates:
302
- args.sendUpdates ??
303
- (guestNotificationMessage || (attendeesToAdd?.length ?? 0) > 0
304
- ? "all"
305
- : undefined),
306
- addGoogleMeet: args.addGoogleMeet,
307
- scope: args.scope,
308
- });
414
+ let result: Awaited<ReturnType<typeof googleCalendar.updateEvent>>;
415
+ let returnedGoogleEventId = googleEventId;
416
+ const replaceRecurringWorkingLocation =
417
+ hasWorkingLocationPatch &&
418
+ (args.scope ?? "single") === "single" &&
419
+ existingEvent?.recurringEventId &&
420
+ updates.workingLocationProperties;
421
+
422
+ if (replaceRecurringWorkingLocation) {
423
+ const hasUnsupportedReplacementPatch =
424
+ args.title !== undefined ||
425
+ args.description !== undefined ||
426
+ args.start !== undefined ||
427
+ args.end !== undefined ||
428
+ args.startTimeZone !== undefined ||
429
+ args.endTimeZone !== undefined ||
430
+ args.allDay !== undefined ||
431
+ args.status !== undefined ||
432
+ args.colorId !== undefined ||
433
+ args.attachments !== undefined ||
434
+ args.reminders !== undefined ||
435
+ args.reminderMinutes !== undefined ||
436
+ args.remindersUseDefault !== undefined ||
437
+ args.addGoogleMeet === true ||
438
+ args.addZoom === true ||
439
+ args.recurrence !== undefined ||
440
+ args.attendees !== undefined ||
441
+ args.addAttendees !== undefined ||
442
+ args.notificationMessage !== undefined;
443
+ if (hasUnsupportedReplacementPatch) {
444
+ throw new Error(
445
+ "Change the working location separately from other event fields for a single recurring occurrence.",
446
+ );
447
+ }
448
+ const now = new Date().toISOString();
449
+ const replacement = await googleCalendar.createEvent(
450
+ {
451
+ id: "",
452
+ title: workingLocationTitle(updates.workingLocationProperties!),
453
+ description: "",
454
+ start: existingEvent!.start,
455
+ end: existingEvent!.end,
456
+ startTimeZone: existingEvent!.startTimeZone,
457
+ endTimeZone: existingEvent!.endTimeZone,
458
+ location: "",
459
+ allDay: existingEvent!.allDay,
460
+ source: "google",
461
+ accountEmail,
462
+ colorId: existingEvent!.colorId,
463
+ transparency: "transparent",
464
+ visibility: "public",
465
+ status: "confirmed",
466
+ eventType: "workingLocation",
467
+ workingLocationProperties: updates.workingLocationProperties,
468
+ createdAt: now,
469
+ updatedAt: now,
470
+ },
471
+ { account: { ownerEmail, accountEmail } },
472
+ );
473
+ if (!replacement.id) {
474
+ throw new Error(
475
+ "Google did not return an id for the working location.",
476
+ );
477
+ }
478
+ try {
479
+ await googleCalendar.deleteEvent(
480
+ googleEventId,
481
+ { ownerEmail, accountEmail },
482
+ { scope: "single" },
483
+ );
484
+ } catch (error) {
485
+ try {
486
+ await googleCalendar.deleteEvent(
487
+ replacement.id,
488
+ { ownerEmail, accountEmail },
489
+ { scope: "single" },
490
+ );
491
+ } catch (cleanupError) {
492
+ console.error(
493
+ "Failed to clean up a working-location replacement after the original occurrence could not be cancelled.",
494
+ cleanupError,
495
+ );
496
+ }
497
+ throw error;
498
+ }
499
+ returnedGoogleEventId = replacement.id;
500
+ result = {
501
+ htmlLink: replacement.htmlLink,
502
+ meetLink: replacement.meetLink,
503
+ conferenceData: replacement.conferenceData,
504
+ };
505
+ } else {
506
+ result = await googleCalendar.updateEvent(googleEventId, updates, {
507
+ account: { ownerEmail, accountEmail },
508
+ sendUpdates:
509
+ args.sendUpdates ??
510
+ (guestNotificationMessage || (attendeesToAdd?.length ?? 0) > 0
511
+ ? "all"
512
+ : undefined),
513
+ addGoogleMeet: args.addGoogleMeet,
514
+ scope: args.scope,
515
+ });
516
+ }
309
517
 
310
518
  const returnedPatch: Partial<CalendarEvent> = {};
311
519
  if (result.htmlLink) returnedPatch.htmlLink = result.htmlLink;
@@ -340,7 +548,10 @@ export default defineAction({
340
548
 
341
549
  return {
342
550
  success: true,
343
- id: `google-${googleEventId}`,
551
+ id: `google-${returnedGoogleEventId}`,
552
+ ...(returnedGoogleEventId !== googleEventId
553
+ ? { replacedId: `google-${googleEventId}` }
554
+ : {}),
344
555
  accountEmail,
345
556
  updated: updatedKeys,
346
557
  htmlLink: result.htmlLink,
@@ -1,6 +1,6 @@
1
1
  import { useT } from "@agent-native/core/client";
2
2
  import type { CalendarEvent } from "@shared/api";
3
- import { IconAlertTriangleFilled } from "@tabler/icons-react";
3
+ import { IconAlertTriangleFilled, IconMapPin } from "@tabler/icons-react";
4
4
  import {
5
5
  eachHourOfInterval,
6
6
  format,
@@ -28,6 +28,7 @@ import {
28
28
  useViewPreferences,
29
29
  type ViewPreferences,
30
30
  } from "@/hooks/use-view-preferences";
31
+ import { partitionAllDayEvents } from "@/lib/all-day-layout";
31
32
  import { getEventDisplayColor, allOtherDeclined } from "@/lib/event-colors";
32
33
  import {
33
34
  shouldSuppressAfterPopoverClose,
@@ -35,6 +36,12 @@ import {
35
36
  } from "@/lib/popover-click-guard";
36
37
  import { EventStatusIcon } from "@/lib/rsvp-status";
37
38
  import { cn } from "@/lib/utils";
39
+ import {
40
+ createWorkingLocationDisplayLabels,
41
+ getWorkingLocationChipLabel,
42
+ getWorkingLocationTitle,
43
+ isWorkingLocationEvent,
44
+ } from "@/lib/working-location";
38
45
 
39
46
  import { EventDetailPopover } from "./EventDetailPopover";
40
47
 
@@ -256,6 +263,8 @@ const DayEventCard = memo(function DayEventCard({
256
263
  onDraftCreate,
257
264
  onDraftDiscard,
258
265
  }: DayEventCardProps) {
266
+ const t = useT();
267
+ const workingLocationLabels = createWorkingLocationDisplayLabels(t);
259
268
  const li = layout.get(event.id) ?? {
260
269
  left: 0,
261
270
  width: 100,
@@ -323,10 +332,10 @@ const DayEventCard = memo(function DayEventCard({
323
332
  )}
324
333
  aria-label={
325
334
  event.ownerName || event.overlayEmail
326
- ? `${event.title}, ${
335
+ ? `${getWorkingLocationTitle(event, workingLocationLabels)}, ${
327
336
  event.ownerName || event.overlayEmail
328
337
  }'s calendar`
329
- : event.title
338
+ : getWorkingLocationTitle(event, workingLocationLabels)
330
339
  }
331
340
  style={{
332
341
  ...posStyle,
@@ -381,8 +390,13 @@ const DayEventCard = memo(function DayEventCard({
381
390
  !isPast && !isDeclined && "font-semibold",
382
391
  )}
383
392
  >
384
- {event.title}
393
+ {getWorkingLocationChipLabel(event, workingLocationLabels)}
385
394
  </span>
395
+ {isWorkingLocationEvent(event) && (
396
+ <span className="shrink-0 text-[10px] font-normal text-foreground/55">
397
+ {t("eventForm.workingLocation")}
398
+ </span>
399
+ )}
386
400
  </div>
387
401
  ) : (
388
402
  <>
@@ -403,8 +417,15 @@ const DayEventCard = memo(function DayEventCard({
403
417
  />
404
418
  )}
405
419
  <EventStatusIcon event={event} className="shrink-0" />
406
- <span className="truncate">{event.title}</span>
420
+ <span className="truncate">
421
+ {getWorkingLocationChipLabel(event, workingLocationLabels)}
422
+ </span>
407
423
  </div>
424
+ {isWorkingLocationEvent(event) && isStart && (
425
+ <div className="mt-0.5 truncate text-[10px] leading-tight text-foreground/60">
426
+ {t("eventForm.workingLocation")}
427
+ </div>
428
+ )}
408
429
  {isStart && (
409
430
  <div
410
431
  className={cn(
@@ -517,6 +538,7 @@ export const DayView = memo(function DayView({
517
538
  isLoading = false,
518
539
  }: DayViewProps) {
519
540
  const t = useT();
541
+ const workingLocationLabels = createWorkingLocationDisplayLabels(t);
520
542
  const { setFocusedEvent } = useCalendarSetters();
521
543
  const { prefs } = useViewPreferences();
522
544
  const [now, setNow] = useState(new Date());
@@ -568,6 +590,10 @@ export const DayView = memo(function DayView({
568
590
  );
569
591
 
570
592
  const allDayEvents = useMemo(() => events.filter((e) => e.allDay), [events]);
593
+ const { workingLocations, regularEvents: regularAllDayEvents } = useMemo(
594
+ () => partitionAllDayEvents(allDayEvents),
595
+ [allDayEvents],
596
+ );
571
597
  const timedEvents = useMemo(() => events.filter((e) => !e.allDay), [events]);
572
598
  const layout = useMemo(() => computeLayout(timedEvents), [timedEvents]);
573
599
 
@@ -726,72 +752,161 @@ export const DayView = memo(function DayView({
726
752
  </Tooltip>
727
753
  </div>
728
754
 
729
- {/* All-day events */}
755
+ {/* Working locations and ordinary all-day events */}
730
756
  {allDayEvents.length > 0 && (
731
- <div className="border-b border-border bg-card/50 px-4 py-2">
732
- <p className="mb-1.5 text-[11px] font-medium uppercase tracking-wider text-muted-foreground">
733
- {t("eventForm.allDay")}
734
- </p>
735
- <div className="space-y-1">
736
- {allDayEvents.map((event) => {
737
- const color = getEventDisplayColor(event, prefs);
738
- return (
739
- <EventDetailPopover
740
- key={event.id}
741
- event={event}
742
- onDelete={onDeleteEvent}
743
- isDraft={draftEventIds.includes(event.id)}
744
- defaultOpen={quickEditEventId === event.id}
745
- onTitleSave={onQuickEditSave}
746
- onDismissNew={onQuickEditCancel}
747
- onDraftUpdate={onDraftUpdate}
748
- onDraftCreate={onDraftCreate}
749
- onDraftDiscard={onDraftDiscard}
750
- >
751
- <button
752
- className={cn(
753
- "relative flex w-full items-center gap-1.5 rounded-md px-3 py-1.5 text-left text-sm font-medium text-foreground transition-all hover:brightness-110",
754
- event.ownerColor && "pr-5",
755
- )}
756
- aria-label={
757
- event.ownerName || event.overlayEmail
758
- ? `${event.title}, ${
759
- event.ownerName || event.overlayEmail
760
- }'s calendar`
761
- : event.title
762
- }
763
- style={
764
- color
765
- ? {
766
- backgroundColor: `${color}30`,
767
- borderLeft: `3px solid ${color}`,
768
- }
769
- : {
770
- backgroundColor: "hsl(var(--primary) / 0.15)",
771
- borderLeft: "3px solid hsl(var(--primary))",
772
- }
773
- }
774
- >
775
- {allOtherDeclined(event) && (
776
- <IconAlertTriangleFilled
777
- size={14}
778
- className="shrink-0 text-current opacity-70"
779
- />
780
- )}
781
- <EventStatusIcon event={event} className="shrink-0" />
782
- <span className="truncate">{event.title}</span>
783
- {event.ownerColor && (
784
- <span
785
- aria-hidden="true"
786
- className="absolute right-2 top-1/2 size-1.5 -translate-y-1/2 rounded-full ring-1 ring-background/70"
787
- style={{ backgroundColor: event.ownerColor }}
788
- />
789
- )}
790
- </button>
791
- </EventDetailPopover>
792
- );
793
- })}
794
- </div>
757
+ <div className="border-b border-border bg-card/50">
758
+ {workingLocations.length > 0 && (
759
+ <div data-working-location-lane className="px-4 py-1.5">
760
+ <p className="mb-1 flex items-center gap-1 text-[10px] font-medium uppercase text-muted-foreground">
761
+ <IconMapPin aria-hidden="true" className="size-3" />
762
+ {t("eventForm.workingLocation")}
763
+ </p>
764
+ <div className="grid gap-1 sm:grid-cols-2">
765
+ {workingLocations.map((event) => {
766
+ const color = getEventDisplayColor(event, prefs);
767
+ return (
768
+ <EventDetailPopover
769
+ key={`${event.overlayEmail ?? event.accountEmail ?? "primary"}:${event.id}`}
770
+ event={event}
771
+ onDelete={onDeleteEvent}
772
+ isDraft={draftEventIds.includes(event.id)}
773
+ defaultOpen={quickEditEventId === event.id}
774
+ onTitleSave={onQuickEditSave}
775
+ onDismissNew={onQuickEditCancel}
776
+ onDraftUpdate={onDraftUpdate}
777
+ onDraftCreate={onDraftCreate}
778
+ onDraftDiscard={onDraftDiscard}
779
+ >
780
+ <button
781
+ className={cn(
782
+ "relative flex h-6 w-full items-center gap-1.5 truncate rounded-sm px-2 text-left text-xs font-medium text-foreground transition-opacity hover:opacity-80",
783
+ event.ownerColor && "pr-5",
784
+ )}
785
+ aria-label={
786
+ event.ownerName || event.overlayEmail
787
+ ? `${getWorkingLocationTitle(event, workingLocationLabels)}, ${
788
+ event.ownerName || event.overlayEmail
789
+ }'s calendar`
790
+ : getWorkingLocationTitle(
791
+ event,
792
+ workingLocationLabels,
793
+ )
794
+ }
795
+ style={{
796
+ backgroundColor: color
797
+ ? `${color}1f`
798
+ : "hsl(var(--muted))",
799
+ borderLeft: `2px solid ${
800
+ color ?? "hsl(var(--muted-foreground))"
801
+ }`,
802
+ }}
803
+ >
804
+ <IconMapPin
805
+ aria-hidden="true"
806
+ className="size-3 shrink-0 opacity-70"
807
+ />
808
+ <span className="truncate">
809
+ {getWorkingLocationChipLabel(
810
+ event,
811
+ workingLocationLabels,
812
+ )}
813
+ </span>
814
+ {event.ownerColor && (
815
+ <span
816
+ aria-hidden="true"
817
+ className="absolute right-2 top-1/2 size-1.5 -translate-y-1/2 rounded-full ring-1 ring-background/70"
818
+ style={{ backgroundColor: event.ownerColor }}
819
+ />
820
+ )}
821
+ </button>
822
+ </EventDetailPopover>
823
+ );
824
+ })}
825
+ </div>
826
+ </div>
827
+ )}
828
+
829
+ {regularAllDayEvents.length > 0 && (
830
+ <div
831
+ data-all-day-event-lane
832
+ className={cn(
833
+ "px-4 py-2",
834
+ workingLocations.length > 0 && "border-t border-border/60",
835
+ )}
836
+ >
837
+ <p className="mb-1.5 text-[11px] font-medium uppercase text-muted-foreground">
838
+ {t("eventForm.allDay")}
839
+ </p>
840
+ <div className="flex flex-col gap-1">
841
+ {regularAllDayEvents.map((event) => {
842
+ const color = getEventDisplayColor(event, prefs);
843
+ return (
844
+ <EventDetailPopover
845
+ key={`${event.overlayEmail ?? event.accountEmail ?? "primary"}:${event.id}`}
846
+ event={event}
847
+ onDelete={onDeleteEvent}
848
+ isDraft={draftEventIds.includes(event.id)}
849
+ defaultOpen={quickEditEventId === event.id}
850
+ onTitleSave={onQuickEditSave}
851
+ onDismissNew={onQuickEditCancel}
852
+ onDraftUpdate={onDraftUpdate}
853
+ onDraftCreate={onDraftCreate}
854
+ onDraftDiscard={onDraftDiscard}
855
+ >
856
+ <button
857
+ className={cn(
858
+ "relative flex w-full items-center gap-1.5 rounded-md px-3 py-1.5 text-left text-sm font-medium text-foreground transition-all hover:brightness-110",
859
+ event.ownerColor && "pr-5",
860
+ )}
861
+ aria-label={
862
+ event.ownerName || event.overlayEmail
863
+ ? `${getWorkingLocationTitle(event, workingLocationLabels)}, ${
864
+ event.ownerName || event.overlayEmail
865
+ }'s calendar`
866
+ : getWorkingLocationTitle(
867
+ event,
868
+ workingLocationLabels,
869
+ )
870
+ }
871
+ style={
872
+ color
873
+ ? {
874
+ backgroundColor: `${color}30`,
875
+ borderLeft: `3px solid ${color}`,
876
+ }
877
+ : {
878
+ backgroundColor: "hsl(var(--primary) / 0.15)",
879
+ borderLeft: "3px solid hsl(var(--primary))",
880
+ }
881
+ }
882
+ >
883
+ {allOtherDeclined(event) && (
884
+ <IconAlertTriangleFilled
885
+ size={14}
886
+ className="shrink-0 text-current opacity-70"
887
+ />
888
+ )}
889
+ <EventStatusIcon event={event} className="shrink-0" />
890
+ <span className="truncate">
891
+ {getWorkingLocationChipLabel(
892
+ event,
893
+ workingLocationLabels,
894
+ )}
895
+ </span>
896
+ {event.ownerColor && (
897
+ <span
898
+ aria-hidden="true"
899
+ className="absolute right-2 top-1/2 size-1.5 -translate-y-1/2 rounded-full ring-1 ring-background/70"
900
+ style={{ backgroundColor: event.ownerColor }}
901
+ />
902
+ )}
903
+ </button>
904
+ </EventDetailPopover>
905
+ );
906
+ })}
907
+ </div>
908
+ </div>
909
+ )}
795
910
  </div>
796
911
  )}
797
912