@agent-native/core 0.98.8 → 0.98.10

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 (52) hide show
  1. package/corpus/README.md +1 -1
  2. package/corpus/core/CHANGELOG.md +13 -0
  3. package/corpus/core/package.json +1 -1
  4. package/corpus/core/src/a2a/handlers.ts +39 -24
  5. package/corpus/core/src/agent/durable-background.ts +6 -0
  6. package/corpus/core/src/mcp/build-server.ts +104 -26
  7. package/corpus/core/src/server/agent-chat/plugin-options.ts +3 -1
  8. package/corpus/templates/analytics/server/plugins/agent-chat.ts +4 -0
  9. package/corpus/templates/calendar/.agents/skills/event-management/SKILL.md +31 -2
  10. package/corpus/templates/calendar/AGENTS.md +11 -0
  11. package/corpus/templates/calendar/README.md +2 -1
  12. package/corpus/templates/calendar/actions/create-event.ts +7 -6
  13. package/corpus/templates/calendar/actions/event-action-helpers.ts +49 -0
  14. package/corpus/templates/calendar/actions/update-event.ts +225 -14
  15. package/corpus/templates/calendar/app/components/calendar/DayView.tsx +185 -70
  16. package/corpus/templates/calendar/app/components/calendar/EventCard.tsx +26 -4
  17. package/corpus/templates/calendar/app/components/calendar/EventDetailPanel.tsx +112 -69
  18. package/corpus/templates/calendar/app/components/calendar/EventDetailPopover.tsx +621 -524
  19. package/corpus/templates/calendar/app/components/calendar/WeekView.tsx +275 -164
  20. package/corpus/templates/calendar/app/components/calendar/WorkingLocationEditor.tsx +222 -0
  21. package/corpus/templates/calendar/app/hooks/use-events.ts +151 -79
  22. package/corpus/templates/calendar/app/i18n/zh-TW.ts +6 -0
  23. package/corpus/templates/calendar/app/i18n-data.ts +60 -0
  24. package/corpus/templates/calendar/app/lib/all-day-layout.ts +126 -0
  25. package/corpus/templates/calendar/app/lib/event-form-utils.ts +18 -1
  26. package/corpus/templates/calendar/app/lib/event-mutation-inputs.ts +1 -1
  27. package/corpus/templates/calendar/app/lib/working-location.ts +163 -0
  28. package/corpus/templates/calendar/app/pages/CalendarView.tsx +21 -2
  29. package/corpus/templates/calendar/changelog/2026-07-07-working-locations-from-google-calendar-now-appear-as-native-.md +6 -0
  30. package/corpus/templates/calendar/server/lib/calendar-availability.ts +2 -0
  31. package/corpus/templates/calendar/server/lib/google-api.ts +6 -1
  32. package/corpus/templates/calendar/server/lib/google-calendar.ts +49 -19
  33. package/corpus/templates/calendar/shared/api.ts +2 -0
  34. package/dist/a2a/handlers.d.ts.map +1 -1
  35. package/dist/a2a/handlers.js +37 -24
  36. package/dist/a2a/handlers.js.map +1 -1
  37. package/dist/agent/durable-background.d.ts.map +1 -1
  38. package/dist/agent/durable-background.js +7 -0
  39. package/dist/agent/durable-background.js.map +1 -1
  40. package/dist/collab/routes.d.ts +1 -1
  41. package/dist/mcp/build-server.d.ts.map +1 -1
  42. package/dist/mcp/build-server.js +90 -28
  43. package/dist/mcp/build-server.js.map +1 -1
  44. package/dist/notifications/routes.d.ts +3 -3
  45. package/dist/observability/routes.d.ts +5 -5
  46. package/dist/secrets/routes.d.ts +9 -9
  47. package/dist/server/agent-chat/plugin-options.d.ts +3 -1
  48. package/dist/server/agent-chat/plugin-options.d.ts.map +1 -1
  49. package/dist/server/agent-chat/plugin-options.js.map +1 -1
  50. package/dist/server/agent-engine-api-key-route.d.ts +1 -1
  51. package/dist/server/transcribe-voice.d.ts +1 -1
  52. package/package.json +1 -1
@@ -712,6 +712,7 @@ const enUS = {
712
712
  addingMeetingLink: "Adding {{provider}} link",
713
713
  alerts: "Alerts",
714
714
  allDay: "All day",
715
+ allDays: "All days",
715
716
  allEvents: "All events",
716
717
  applyChangesTo: "Apply changes to",
717
718
  applyTo: "Apply to",
@@ -796,6 +797,7 @@ const enUS = {
796
797
  notifyGuestsQuestion: "Notify guests?",
797
798
  none: "None",
798
799
  office: "Office",
800
+ other: "Other",
799
801
  openInSidebar: "Open in sidebar",
800
802
  openInGoogleCalendar: "Open in Google Calendar",
801
803
  optionalDescription: "Optional description",
@@ -849,6 +851,7 @@ const enUS = {
849
851
  start: "Start",
850
852
  startDate: "Start date",
851
853
  subscribedFeeds: "Subscribed feeds",
854
+ thisDayOnly: "This day only",
852
855
  thisEvent: "This event",
853
856
  timeSelected: "Time selected",
854
857
  timezone: "Timezone",
@@ -872,6 +875,9 @@ const enUS = {
872
875
  weekly: "Weekly",
873
876
  workingFrom: "Working from",
874
877
  workingLocation: "Working location",
878
+ workingLocationDesk: "Desk {{desk}}",
879
+ workingLocationFloor: "Floor {{floor}}",
880
+ workingLocationTitle: "Working location: {{location}}",
875
881
  yearly: "Yearly",
876
882
  zoom: "Zoom",
877
883
  zoomAdded: "Zoom added",
@@ -1142,6 +1148,7 @@ const calendarSurfaceTranslations = {
1142
1148
  addingMeetingLink: "添加 {{provider}} 链接",
1143
1149
  alerts: "警报",
1144
1150
  allDay: "全天",
1151
+ allDays: "所有日期",
1145
1152
  allEvents: "所有活动",
1146
1153
  applyTo: "适用于",
1147
1154
  askAi: "询问人工智能",
@@ -1204,6 +1211,7 @@ const calendarSurfaceTranslations = {
1204
1211
  noVideo: "没有视频",
1205
1212
  none: "无",
1206
1213
  office: "办公室",
1214
+ other: "其他",
1207
1215
  openInSidebar: "在侧边栏中打开",
1208
1216
  optionalDescription: "可选描述",
1209
1217
  optionalLocation: "可选位置",
@@ -1222,6 +1230,7 @@ const calendarSurfaceTranslations = {
1222
1230
  showAs: "显示为",
1223
1231
  start: "开始",
1224
1232
  startDate: "开始日期",
1233
+ thisDayOnly: "仅当天",
1225
1234
  thisEvent: "本次活动",
1226
1235
  timeSelected: "选定时间",
1227
1236
  timezone: "时区",
@@ -1235,6 +1244,9 @@ const calendarSurfaceTranslations = {
1235
1244
  weekly: "每周",
1236
1245
  workingFrom: "工作自",
1237
1246
  workingLocation: "工作地点",
1247
+ workingLocationDesk: "工位 {{desk}}",
1248
+ workingLocationFloor: "楼层 {{floor}}",
1249
+ workingLocationTitle: "工作地点:{{location}}",
1238
1250
  yearly: "每年",
1239
1251
  zoom: "Zoom",
1240
1252
  zoomAdded: "添加Zoom",
@@ -1435,6 +1447,7 @@ const calendarSurfaceTranslations = {
1435
1447
  addingMeetingLink: "Añadiendo enlace {{provider}}",
1436
1448
  alerts: "Alertas",
1437
1449
  allDay: "Todo el día",
1450
+ allDays: "Todos los días",
1438
1451
  allEvents: "Todos los eventos",
1439
1452
  applyTo: "Referirse a",
1440
1453
  askAi: "Pregúntale a la IA",
@@ -1500,6 +1513,7 @@ const calendarSurfaceTranslations = {
1500
1513
  noVideo: "Sin vídeo",
1501
1514
  none: "Ninguno",
1502
1515
  office: "Oficina",
1516
+ other: "Otro",
1503
1517
  openInSidebar: "Abrir en la barra lateral",
1504
1518
  optionalDescription: "Descripción opcional",
1505
1519
  optionalLocation: "Ubicación opcional",
@@ -1519,6 +1533,7 @@ const calendarSurfaceTranslations = {
1519
1533
  showAs: "Mostrar como",
1520
1534
  start: "Comenzar",
1521
1535
  startDate: "Fecha de inicio",
1536
+ thisDayOnly: "Solo este día",
1522
1537
  thisEvent: "este evento",
1523
1538
  timeSelected: "Hora seleccionada",
1524
1539
  timezone: "Zona horaria",
@@ -1532,6 +1547,9 @@ const calendarSurfaceTranslations = {
1532
1547
  weekly: "Semanalmente",
1533
1548
  workingFrom: "Trabajando desde",
1534
1549
  workingLocation: "Lugar de trabajo",
1550
+ workingLocationDesk: "Escritorio {{desk}}",
1551
+ workingLocationFloor: "Piso {{floor}}",
1552
+ workingLocationTitle: "Lugar de trabajo: {{location}}",
1535
1553
  yearly: "Anual",
1536
1554
  zoom: "Zoom",
1537
1555
  zoomAdded: "Zoom añadido",
@@ -1738,6 +1756,7 @@ const calendarSurfaceTranslations = {
1738
1756
  addingMeetingLink: "Ajout du lien {{provider}}",
1739
1757
  alerts: "Alertes",
1740
1758
  allDay: "Toute la journée",
1759
+ allDays: "Tous les jours",
1741
1760
  allEvents: "Tous les événements",
1742
1761
  applyTo: "Postuler à",
1743
1762
  askAi: "Demandez à l'IA",
@@ -1804,6 +1823,7 @@ const calendarSurfaceTranslations = {
1804
1823
  noVideo: "Pas de vidéo",
1805
1824
  none: "Aucun",
1806
1825
  office: "Bureau",
1826
+ other: "Autre",
1807
1827
  openInSidebar: "Ouvrir dans la barre latérale",
1808
1828
  optionalDescription: "Description facultative",
1809
1829
  optionalLocation: "Emplacement facultatif",
@@ -1823,6 +1843,7 @@ const calendarSurfaceTranslations = {
1823
1843
  showAs: "Afficher comme",
1824
1844
  start: "Commencer",
1825
1845
  startDate: "Date de début",
1846
+ thisDayOnly: "Ce jour uniquement",
1826
1847
  thisEvent: "Cet événement",
1827
1848
  timeSelected: "Heure sélectionnée",
1828
1849
  timezone: "Fuseau horaire",
@@ -1836,6 +1857,9 @@ const calendarSurfaceTranslations = {
1836
1857
  weekly: "Hebdomadaire",
1837
1858
  workingFrom: "Travailler à partir de",
1838
1859
  workingLocation: "Lieu de travail",
1860
+ workingLocationDesk: "Bureau {{desk}}",
1861
+ workingLocationFloor: "Étage {{floor}}",
1862
+ workingLocationTitle: "Lieu de travail : {{location}}",
1839
1863
  yearly: "Annuel",
1840
1864
  zoom: "Zoom",
1841
1865
  zoomAdded: "Zoom ajouté",
@@ -2041,6 +2065,7 @@ const calendarSurfaceTranslations = {
2041
2065
  addingMeetingLink: "{{provider}}-Link hinzufügen",
2042
2066
  alerts: "Warnungen",
2043
2067
  allDay: "Den ganzen Tag",
2068
+ allDays: "Alle Tage",
2044
2069
  allEvents: "Alle Veranstaltungen",
2045
2070
  applyTo: "Bewerben Sie sich bei",
2046
2071
  askAi: "Fragen Sie die KI",
@@ -2108,6 +2133,7 @@ const calendarSurfaceTranslations = {
2108
2133
  noVideo: "Kein Video",
2109
2134
  none: "Keine",
2110
2135
  office: "Büro",
2136
+ other: "Andere",
2111
2137
  openInSidebar: "In der Seitenleiste öffnen",
2112
2138
  optionalDescription: "Optionale Beschreibung",
2113
2139
  optionalLocation: "Optionaler Standort",
@@ -2127,6 +2153,7 @@ const calendarSurfaceTranslations = {
2127
2153
  showAs: "Anzeigen als",
2128
2154
  start: "Starten",
2129
2155
  startDate: "Startdatum",
2156
+ thisDayOnly: "Nur dieser Tag",
2130
2157
  thisEvent: "Dieses Ereignis",
2131
2158
  timeSelected: "Zeit ausgewählt",
2132
2159
  timezone: "Zeitzone",
@@ -2140,6 +2167,9 @@ const calendarSurfaceTranslations = {
2140
2167
  weekly: "Wöchentlich",
2141
2168
  workingFrom: "Arbeiten von",
2142
2169
  workingLocation: "Arbeitsort",
2170
+ workingLocationDesk: "Schreibtisch {{desk}}",
2171
+ workingLocationFloor: "Etage {{floor}}",
2172
+ workingLocationTitle: "Arbeitsort: {{location}}",
2143
2173
  yearly: "Jährlich",
2144
2174
  zoom: "Zoom",
2145
2175
  zoomAdded: "Zoom hinzugefügt",
@@ -2339,6 +2369,7 @@ const calendarSurfaceTranslations = {
2339
2369
  addingMeetingLink: "{{provider}}リンクの追加",
2340
2370
  alerts: "アラート",
2341
2371
  allDay: "一日中",
2372
+ allDays: "すべての日",
2342
2373
  allEvents: "すべてのイベント",
2343
2374
  applyTo: "申請先",
2344
2375
  askAi: "AIに聞く",
@@ -2405,6 +2436,7 @@ const calendarSurfaceTranslations = {
2405
2436
  noVideo: "ビデオなし",
2406
2437
  none: "なし",
2407
2438
  office: "オフィス",
2439
+ other: "その他",
2408
2440
  openInSidebar: "サイドバーで開く",
2409
2441
  optionalDescription: "オプションの説明",
2410
2442
  optionalLocation: "オプションの場所",
@@ -2424,6 +2456,7 @@ const calendarSurfaceTranslations = {
2424
2456
  showAs: "として表示",
2425
2457
  start: "スタート",
2426
2458
  startDate: "開始日",
2459
+ thisDayOnly: "この日のみ",
2427
2460
  thisEvent: "このイベント",
2428
2461
  timeSelected: "選択された時間",
2429
2462
  timezone: "タイムゾーン",
@@ -2437,6 +2470,9 @@ const calendarSurfaceTranslations = {
2437
2470
  weekly: "毎週",
2438
2471
  workingFrom: "から働く",
2439
2472
  workingLocation: "勤務地",
2473
+ workingLocationDesk: "デスク {{desk}}",
2474
+ workingLocationFloor: "{{floor}}階",
2475
+ workingLocationTitle: "勤務地: {{location}}",
2440
2476
  yearly: "毎年",
2441
2477
  zoom: "Zoom",
2442
2478
  zoomAdded: "Zoom追加",
@@ -2636,6 +2672,7 @@ const calendarSurfaceTranslations = {
2636
2672
  addingMeetingLink: "{{provider}} 링크 추가",
2637
2673
  alerts: "경고",
2638
2674
  allDay: "하루 종일",
2675
+ allDays: "모든 날",
2639
2676
  allEvents: "모든 이벤트",
2640
2677
  applyTo: "신청 대상",
2641
2678
  askAi: "AI에게 물어보세요",
@@ -2699,6 +2736,7 @@ const calendarSurfaceTranslations = {
2699
2736
  noVideo: "영상 없음",
2700
2737
  none: "없음",
2701
2738
  office: "사무실",
2739
+ other: "기타",
2702
2740
  openInSidebar: "사이드바에서 열기",
2703
2741
  optionalDescription: "선택적 설명",
2704
2742
  optionalLocation: "선택적 위치",
@@ -2718,6 +2756,7 @@ const calendarSurfaceTranslations = {
2718
2756
  showAs: "다음으로 표시",
2719
2757
  start: "시작",
2720
2758
  startDate: "시작일",
2759
+ thisDayOnly: "이 날만",
2721
2760
  thisEvent: "이번 이벤트",
2722
2761
  timeSelected: "선택한 시간",
2723
2762
  timezone: "시간대",
@@ -2731,6 +2770,9 @@ const calendarSurfaceTranslations = {
2731
2770
  weekly: "주간",
2732
2771
  workingFrom: "근무처:",
2733
2772
  workingLocation: "근무 위치",
2773
+ workingLocationDesk: "좌석 {{desk}}",
2774
+ workingLocationFloor: "{{floor}}층",
2775
+ workingLocationTitle: "근무 위치: {{location}}",
2734
2776
  yearly: "매년",
2735
2777
  zoom: "Zoom",
2736
2778
  zoomAdded: "Zoom 추가됨",
@@ -2933,6 +2975,7 @@ const calendarSurfaceTranslations = {
2933
2975
  addingMeetingLink: "Adicionando link {{provider}}",
2934
2976
  alerts: "Alertas",
2935
2977
  allDay: "O dia todo",
2978
+ allDays: "Todos os dias",
2936
2979
  allEvents: "Todos os eventos",
2937
2980
  applyTo: "Candidatar-se a",
2938
2981
  askAi: "Pergunte à IA",
@@ -2998,6 +3041,7 @@ const calendarSurfaceTranslations = {
2998
3041
  noVideo: "Nenhum vídeo",
2999
3042
  none: "Nenhum",
3000
3043
  office: "Escritório",
3044
+ other: "Outro",
3001
3045
  openInSidebar: "Abrir na barra lateral",
3002
3046
  optionalDescription: "Descrição opcional",
3003
3047
  optionalLocation: "Localização opcional",
@@ -3017,6 +3061,7 @@ const calendarSurfaceTranslations = {
3017
3061
  showAs: "Mostrar como",
3018
3062
  start: "Começar",
3019
3063
  startDate: "Data de início",
3064
+ thisDayOnly: "Somente este dia",
3020
3065
  thisEvent: "Este evento",
3021
3066
  timeSelected: "Hora selecionada",
3022
3067
  timezone: "Fuso horário",
@@ -3030,6 +3075,9 @@ const calendarSurfaceTranslations = {
3030
3075
  weekly: "Semanalmente",
3031
3076
  workingFrom: "Trabalhando de",
3032
3077
  workingLocation: "Local de trabalho",
3078
+ workingLocationDesk: "Mesa {{desk}}",
3079
+ workingLocationFloor: "Andar {{floor}}",
3080
+ workingLocationTitle: "Local de trabalho: {{location}}",
3033
3081
  yearly: "Anualmente",
3034
3082
  zoom: "Zoom",
3035
3083
  zoomAdded: "Zoom adicionado",
@@ -3227,6 +3275,7 @@ const calendarSurfaceTranslations = {
3227
3275
  addingMeetingLink: "{{provider}} लिंक जोड़ा जा रहा है",
3228
3276
  alerts: "अलर्ट",
3229
3277
  allDay: "सारा दिन",
3278
+ allDays: "सभी दिन",
3230
3279
  allEvents: "सभी घटनाएँ",
3231
3280
  applyTo: "के लिए आवेदन करें",
3232
3281
  askAi: "एआई से पूछें",
@@ -3290,6 +3339,7 @@ const calendarSurfaceTranslations = {
3290
3339
  noVideo: "कोई वीडियो नहीं",
3291
3340
  none: "कोई नहीं",
3292
3341
  office: "कार्यालय",
3342
+ other: "अन्य",
3293
3343
  openInSidebar: "साइडबार में खोलें",
3294
3344
  optionalDescription: "वैकल्पिक विवरण",
3295
3345
  optionalLocation: "वैकल्पिक स्थान",
@@ -3309,6 +3359,7 @@ const calendarSurfaceTranslations = {
3309
3359
  showAs: "के रूप में दिखाएँ",
3310
3360
  start: "प्रारंभ करें",
3311
3361
  startDate: "आरंभ तिथि",
3362
+ thisDayOnly: "केवल यह दिन",
3312
3363
  thisEvent: "यह घटना",
3313
3364
  timeSelected: "समय चयनित",
3314
3365
  timezone: "समयक्षेत्र",
@@ -3322,6 +3373,9 @@ const calendarSurfaceTranslations = {
3322
3373
  weekly: "साप्ताहिक",
3323
3374
  workingFrom: "से काम कर रहे हैं",
3324
3375
  workingLocation: "कार्य स्थान",
3376
+ workingLocationDesk: "डेस्क {{desk}}",
3377
+ workingLocationFloor: "मंज़िल {{floor}}",
3378
+ workingLocationTitle: "कार्य स्थान: {{location}}",
3325
3379
  yearly: "वार्षिक",
3326
3380
  zoom: "Zoom",
3327
3381
  zoomAdded: "Zoom जोड़ा गया",
@@ -3519,6 +3573,7 @@ const calendarSurfaceTranslations = {
3519
3573
  addingMeetingLink: "إضافة رابط {{provider}}",
3520
3574
  alerts: "التنبيهات",
3521
3575
  allDay: "طوال اليوم",
3576
+ allDays: "كل الأيام",
3522
3577
  allEvents: "جميع الأحداث",
3523
3578
  applyTo: "تنطبق على",
3524
3579
  askAi: "اسأل الذكاء الاصطناعي",
@@ -3583,6 +3638,7 @@ const calendarSurfaceTranslations = {
3583
3638
  noVideo: "لا يوجد فيديو",
3584
3639
  none: "لا شيء",
3585
3640
  office: "مكتب",
3641
+ other: "أخرى",
3586
3642
  openInSidebar: "فتح في الشريط الجانبي",
3587
3643
  optionalDescription: "وصف اختياري",
3588
3644
  optionalLocation: "الموقع الاختياري",
@@ -3602,6 +3658,7 @@ const calendarSurfaceTranslations = {
3602
3658
  showAs: "إظهار كـ",
3603
3659
  start: "ابدأ",
3604
3660
  startDate: "تاريخ البدء",
3661
+ thisDayOnly: "هذا اليوم فقط",
3605
3662
  thisEvent: "هذا الحدث",
3606
3663
  timeSelected: "الوقت المحدد",
3607
3664
  timezone: "المنطقة الزمنية",
@@ -3615,6 +3672,9 @@ const calendarSurfaceTranslations = {
3615
3672
  weekly: "أسبوعيا",
3616
3673
  workingFrom: "العمل من",
3617
3674
  workingLocation: "موقع العمل",
3675
+ workingLocationDesk: "المكتب {{desk}}",
3676
+ workingLocationFloor: "الطابق {{floor}}",
3677
+ workingLocationTitle: "موقع العمل: {{location}}",
3618
3678
  yearly: "سنويا",
3619
3679
  zoom: "Zoom",
3620
3680
  zoomAdded: "تمت إضافة Zoom",
@@ -0,0 +1,126 @@
1
+ import type { CalendarEvent } from "@shared/api";
2
+ import { addDays, parseISO, startOfDay } from "date-fns";
3
+
4
+ import { isWorkingLocationEvent } from "@/lib/working-location";
5
+
6
+ export interface AllDaySpan {
7
+ event: CalendarEvent;
8
+ startCol: number;
9
+ endCol: number;
10
+ }
11
+
12
+ export interface AllDayPlacement extends AllDaySpan {
13
+ row: number;
14
+ }
15
+
16
+ export interface AllDayLayout {
17
+ placements: AllDayPlacement[];
18
+ rowCount: number;
19
+ }
20
+
21
+ export type AllDayPlacementGroup = AllDayPlacement[];
22
+
23
+ export function partitionAllDayEvents(events: CalendarEvent[]) {
24
+ const workingLocations: CalendarEvent[] = [];
25
+ const regularEvents: CalendarEvent[] = [];
26
+
27
+ for (const event of events) {
28
+ if (isWorkingLocationEvent(event)) workingLocations.push(event);
29
+ else regularEvents.push(event);
30
+ }
31
+
32
+ return { workingLocations, regularEvents };
33
+ }
34
+
35
+ /** Determine which visible day columns an all-day event overlaps. */
36
+ export function getAllDaySpan(
37
+ event: CalendarEvent,
38
+ days: Date[],
39
+ ): Omit<AllDaySpan, "event"> | null {
40
+ const eventStart = parseISO(event.start);
41
+ const eventEnd = event.end ? parseISO(event.end) : addDays(eventStart, 1);
42
+
43
+ let startCol = -1;
44
+ let endCol = -1;
45
+
46
+ for (let index = 0; index < days.length; index++) {
47
+ const dayStart = startOfDay(days[index]);
48
+ const dayEnd = addDays(dayStart, 1);
49
+ if (eventStart < dayEnd && eventEnd > dayStart) {
50
+ if (startCol === -1) startCol = index;
51
+ endCol = index;
52
+ }
53
+ }
54
+
55
+ return startCol === -1 ? null : { startCol, endCol };
56
+ }
57
+
58
+ function compareSpans(a: AllDaySpan, b: AllDaySpan): number {
59
+ if (a.startCol !== b.startCol) return a.startCol - b.startCol;
60
+ if (a.endCol !== b.endCol) return b.endCol - a.endCol;
61
+ return a.event.id.localeCompare(b.event.id);
62
+ }
63
+
64
+ export function layoutAllDayEvents(
65
+ events: CalendarEvent[],
66
+ days: Date[],
67
+ ): AllDayLayout {
68
+ const spans = events
69
+ .map((event) => {
70
+ const span = getAllDaySpan(event, days);
71
+ return span ? { event, ...span } : null;
72
+ })
73
+ .filter((span): span is AllDaySpan => span !== null)
74
+ .sort(compareSpans);
75
+ const rows: AllDaySpan[][] = [];
76
+ const placements: AllDayPlacement[] = [];
77
+
78
+ for (const span of spans) {
79
+ let row = rows.findIndex((candidate) =>
80
+ candidate.every(
81
+ (existing) =>
82
+ span.endCol < existing.startCol || span.startCol > existing.endCol,
83
+ ),
84
+ );
85
+
86
+ if (row === -1) {
87
+ row = rows.length;
88
+ rows.push([]);
89
+ }
90
+
91
+ rows[row].push(span);
92
+ placements.push({ ...span, row });
93
+ }
94
+
95
+ return { placements, rowCount: rows.length };
96
+ }
97
+
98
+ /** Group visually adjacent placements while preserving each event's click target. */
99
+ export function groupAdjacentAllDayPlacements(
100
+ placements: AllDayPlacement[],
101
+ getGroupKey: (placement: AllDayPlacement) => string,
102
+ ): AllDayPlacementGroup[] {
103
+ const sorted = [...placements].sort(
104
+ (a, b) => a.row - b.row || a.startCol - b.startCol,
105
+ );
106
+ const groups: AllDayPlacementGroup[] = [];
107
+
108
+ for (const placement of sorted) {
109
+ const previousGroup = groups[groups.length - 1];
110
+ const previousPlacement = previousGroup?.[previousGroup.length - 1];
111
+ const isAdjacent =
112
+ previousPlacement?.row === placement.row &&
113
+ previousPlacement.endCol + 1 === placement.startCol;
114
+ const isSameGroup =
115
+ previousPlacement !== undefined &&
116
+ getGroupKey(previousPlacement) === getGroupKey(placement);
117
+
118
+ if (previousGroup && isAdjacent && isSameGroup) {
119
+ previousGroup.push(placement);
120
+ } else {
121
+ groups.push([placement]);
122
+ }
123
+ }
124
+
125
+ return groups;
126
+ }
@@ -1,4 +1,4 @@
1
- import type { CalendarEvent } from "@shared/api";
1
+ import type { CalendarEvent, UpdateEventScope } from "@shared/api";
2
2
 
3
3
  export type ReminderMethod = "popup" | "email";
4
4
  export type ReminderMode = "default" | "none" | "custom";
@@ -195,6 +195,23 @@ export function getEventEndValidationMessage({
195
195
  return "End date and time must be after start date and time.";
196
196
  }
197
197
 
198
+ export function normalizeAllDayEditEndDate(
199
+ singleDay: boolean,
200
+ startDate: string,
201
+ endDate: string,
202
+ ): string {
203
+ return singleDay ? startDate : endDate;
204
+ }
205
+
206
+ export function resolveTimeEditScope(
207
+ isRecurring: boolean,
208
+ isSingleDayWorkingLocation: boolean,
209
+ requestedScope: UpdateEventScope,
210
+ ): UpdateEventScope {
211
+ if (!isRecurring || isSingleDayWorkingLocation) return "single";
212
+ return requestedScope;
213
+ }
214
+
198
215
  export function getLocalTimezone() {
199
216
  return Intl.DateTimeFormat().resolvedOptions().timeZone || "UTC";
200
217
  }
@@ -11,8 +11,8 @@ export function buildDeleteEventMutationInput(
11
11
  options: DeleteEventOptions = {},
12
12
  ): DeleteEventMutationInput {
13
13
  return {
14
+ ...options,
14
15
  id: event.id,
15
16
  accountEmail: event.accountEmail,
16
- ...options,
17
17
  };
18
18
  }
@@ -0,0 +1,163 @@
1
+ import type { CalendarEvent, UpdateEventScope } from "@shared/api";
2
+
3
+ export type WorkingLocationKind =
4
+ | "homeOffice"
5
+ | "officeLocation"
6
+ | "customLocation";
7
+
8
+ export interface WorkingLocationSelection {
9
+ type: WorkingLocationKind;
10
+ label: string;
11
+ scope?: UpdateEventScope;
12
+ }
13
+
14
+ export interface WorkingLocationDisplayLabels {
15
+ home: string;
16
+ office: string;
17
+ workingLocation: string;
18
+ title: (location: string) => string;
19
+ floor: (floor: string) => string;
20
+ desk: (desk: string) => string;
21
+ }
22
+
23
+ type WorkingLocationTranslate = (
24
+ key: string,
25
+ values?: Record<string, string>,
26
+ ) => string;
27
+
28
+ export function createWorkingLocationDisplayLabels(
29
+ t: WorkingLocationTranslate,
30
+ ): WorkingLocationDisplayLabels {
31
+ return {
32
+ home: t("eventForm.home"),
33
+ office: t("eventForm.office"),
34
+ workingLocation: t("eventForm.workingLocation"),
35
+ title: (location) => t("eventForm.workingLocationTitle", { location }),
36
+ floor: (floor) => t("eventForm.workingLocationFloor", { floor }),
37
+ desk: (desk) => t("eventForm.workingLocationDesk", { desk }),
38
+ };
39
+ }
40
+
41
+ export function isWorkingLocationEvent(
42
+ event: Pick<CalendarEvent, "eventType">,
43
+ ) {
44
+ return event.eventType === "workingLocation";
45
+ }
46
+
47
+ export function getWorkingLocationType(
48
+ event: Pick<CalendarEvent, "workingLocationProperties">,
49
+ ): WorkingLocationKind {
50
+ return event.workingLocationProperties?.type ?? "customLocation";
51
+ }
52
+
53
+ export function getWorkingLocationLabel(
54
+ event: Pick<
55
+ CalendarEvent,
56
+ "location" | "title" | "workingLocationProperties"
57
+ >,
58
+ labels: WorkingLocationDisplayLabels,
59
+ ): string {
60
+ const properties = event.workingLocationProperties;
61
+ if (properties?.type === "homeOffice") return labels.home;
62
+ if (properties?.type === "officeLocation") {
63
+ return (
64
+ properties.officeLocation?.label ||
65
+ properties.officeLocation?.buildingId ||
66
+ event.location ||
67
+ event.title ||
68
+ labels.office
69
+ );
70
+ }
71
+ return (
72
+ properties?.customLocation?.label ||
73
+ event.location ||
74
+ event.title ||
75
+ labels.workingLocation
76
+ );
77
+ }
78
+
79
+ export function getWorkingLocationEditableLabel(
80
+ event: Pick<CalendarEvent, "location" | "workingLocationProperties">,
81
+ ): string {
82
+ const properties = event.workingLocationProperties;
83
+ if (properties?.type === "officeLocation") {
84
+ return properties.officeLocation?.label || event.location || "";
85
+ }
86
+ if (properties?.type === "customLocation") {
87
+ return properties.customLocation?.label || event.location || "";
88
+ }
89
+ return "";
90
+ }
91
+
92
+ export function buildWorkingLocationUpdate(
93
+ event: Pick<CalendarEvent, "id" | "accountEmail">,
94
+ selection: WorkingLocationSelection,
95
+ ) {
96
+ const label = selection.type === "homeOffice" ? "" : selection.label.trim();
97
+
98
+ return {
99
+ id: event.id,
100
+ accountEmail: event.accountEmail,
101
+ workingLocationType: selection.type,
102
+ workingLocationLabel: label,
103
+ scope: selection.scope,
104
+ };
105
+ }
106
+
107
+ export function buildWorkingLocationProperties(
108
+ event: Pick<CalendarEvent, "workingLocationProperties">,
109
+ selection: Pick<WorkingLocationSelection, "type" | "label">,
110
+ ): NonNullable<CalendarEvent["workingLocationProperties"]> {
111
+ const label = selection.label.trim();
112
+ if (selection.type === "homeOffice") {
113
+ return { type: "homeOffice", homeOffice: {} };
114
+ }
115
+ if (selection.type === "officeLocation") {
116
+ return {
117
+ type: "officeLocation",
118
+ officeLocation: {
119
+ ...(event.workingLocationProperties?.type === "officeLocation"
120
+ ? event.workingLocationProperties.officeLocation
121
+ : {}),
122
+ label,
123
+ },
124
+ };
125
+ }
126
+ return {
127
+ type: "customLocation",
128
+ customLocation: { label },
129
+ };
130
+ }
131
+
132
+ export function getWorkingLocationChipLabel(
133
+ event: CalendarEvent,
134
+ labels: WorkingLocationDisplayLabels,
135
+ ): string {
136
+ return isWorkingLocationEvent(event)
137
+ ? getWorkingLocationLabel(event, labels)
138
+ : event.title;
139
+ }
140
+
141
+ export function getWorkingLocationTitle(
142
+ event: CalendarEvent,
143
+ labels: WorkingLocationDisplayLabels,
144
+ ): string {
145
+ return isWorkingLocationEvent(event)
146
+ ? labels.title(getWorkingLocationLabel(event, labels))
147
+ : event.title;
148
+ }
149
+
150
+ export function getWorkingLocationDetail(
151
+ event: Pick<CalendarEvent, "workingLocationProperties">,
152
+ labels: WorkingLocationDisplayLabels,
153
+ ): string | undefined {
154
+ const office = event.workingLocationProperties?.officeLocation;
155
+ if (!office) return undefined;
156
+ const parts = [
157
+ office.buildingId,
158
+ office.floorId ? labels.floor(office.floorId) : undefined,
159
+ office.floorSectionId,
160
+ office.deskId ? labels.desk(office.deskId) : undefined,
161
+ ].filter(Boolean);
162
+ return parts.length > 0 ? parts.join(" / ") : undefined;
163
+ }