@agent-native/core 0.91.1 → 0.91.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/blueprints/action/crud.md +1 -1
- package/blueprints/channel/discord.md +1 -1
- package/blueprints/provider/stripe.md +1 -1
- package/blueprints/sandbox/docker.md +1 -1
- package/corpus/README.md +1 -1
- package/corpus/core/CHANGELOG.md +14 -0
- package/corpus/core/blueprints/action/crud.md +1 -1
- package/corpus/core/blueprints/channel/discord.md +1 -1
- package/corpus/core/blueprints/provider/stripe.md +1 -1
- package/corpus/core/blueprints/sandbox/docker.md +1 -1
- package/corpus/core/package.json +6 -5
- package/corpus/core/src/agent/run-manager.ts +23 -2
- package/corpus/core/src/agent/run-store.ts +2 -2
- package/corpus/core/src/cli/add.ts +1 -1
- package/corpus/core/src/cli/create.ts +8 -3
- package/corpus/core/src/cli/index.ts +5 -4
- package/corpus/core/src/client/extensions/ExtensionSlot.tsx +7 -0
- package/corpus/core/src/client/session-replay.ts +97 -3
- package/corpus/core/src/jobs/scheduler.ts +30 -1
- package/corpus/core/src/localization/default-messages.ts +21 -0
- package/corpus/core/src/templates/default/app/i18n/ar-SA.ts +21 -0
- package/corpus/core/src/templates/default/app/i18n/de-DE.ts +21 -0
- package/corpus/core/src/templates/default/app/i18n/en-US.ts +21 -0
- package/corpus/core/src/templates/default/app/i18n/es-ES.ts +21 -0
- package/corpus/core/src/templates/default/app/i18n/fr-FR.ts +21 -0
- package/corpus/core/src/templates/default/app/i18n/hi-IN.ts +20 -0
- package/corpus/core/src/templates/default/app/i18n/ja-JP.ts +21 -0
- package/corpus/core/src/templates/default/app/i18n/ko-KR.ts +20 -0
- package/corpus/core/src/templates/default/app/i18n/pt-BR.ts +21 -0
- package/corpus/core/src/templates/default/app/i18n/zh-CN.ts +20 -0
- package/corpus/core/src/templates/default/app/i18n/zh-TW.ts +19 -0
- package/corpus/core/src/templates/default/package.json +2 -2
- package/corpus/core/src/templates/headless/package.json +3 -3
- package/corpus/core/src/templates/workspace-core/package.json +6 -6
- package/corpus/core/src/templates/workspace-root/package.json +2 -2
- package/corpus/core/src/triggers/dispatcher.ts +30 -2
- package/corpus/core/src/vite/client.ts +96 -1
- package/corpus/templates/analytics/.agents/skills/session-replay/SKILL.md +21 -2
- package/corpus/templates/analytics/app/global.css +26 -10
- package/corpus/templates/analytics/app/i18n/zh-TW.ts +2 -0
- package/corpus/templates/analytics/app/i18n-data.ts +25 -0
- package/corpus/templates/analytics/app/pages/sessions/SessionDetailPage.tsx +548 -237
- package/corpus/templates/analytics/app/pages/sessions/SessionDevToolsPanel.tsx +285 -294
- package/corpus/templates/analytics/changelog/2026-07-08-console-details-expand-inline-under-each-row-and-the-event-t.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-08-session-replay-clamps-ultra-wide-viewports-while-keeping-the.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-08-session-replay-keeps-a-usable-stage-when-dev-tools-is-open-a.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-08-session-replay-resumes-after-scrubbing-and-expands-console-r.md +6 -0
- package/corpus/templates/analytics/changelog/2026-07-08-session-replays-wait-for-full-load-keep-a-normal-viewport-an.md +6 -0
- package/corpus/templates/analytics/package.json +2 -2
- package/corpus/templates/assets/package.json +2 -2
- package/corpus/templates/brain/package.json +2 -2
- package/corpus/templates/calendar/AGENTS.md +10 -0
- package/corpus/templates/calendar/actions/create-event.ts +5 -1
- package/corpus/templates/calendar/actions/event-action-helpers.ts +41 -0
- package/corpus/templates/calendar/actions/get-attendee-timezones.ts +28 -0
- package/corpus/templates/calendar/actions/set-attendee-timezone.ts +68 -0
- package/corpus/templates/calendar/app/components/calendar/EventAttendeesSection.tsx +150 -11
- package/corpus/templates/calendar/app/components/calendar/EventDetailPanel.tsx +36 -1
- package/corpus/templates/calendar/app/components/calendar/EventDetailPopover.tsx +32 -0
- package/corpus/templates/calendar/app/hooks/use-attendee-timezones.ts +24 -0
- package/corpus/templates/calendar/app/i18n/zh-TW.ts +2 -0
- package/corpus/templates/calendar/app/i18n-data.ts +20 -0
- package/corpus/templates/calendar/app/lib/attendee-local-time.ts +122 -0
- package/corpus/templates/calendar/changelog/2026-07-08-event-details-now-support-extension-widgets-and-you-can-see-.md +6 -0
- package/corpus/templates/calendar/changelog/2026-07-08-when-you-invite-guests-to-a-new-event-you-now-appear-in.md +6 -0
- package/corpus/templates/calendar/package.json +2 -2
- package/corpus/templates/calendar/server/handlers/events.ts +5 -1
- package/corpus/templates/calendar/shared/api.ts +6 -0
- package/corpus/templates/chat/package.json +2 -2
- package/corpus/templates/clips/.agents/skills/meetings/SKILL.md +6 -2
- package/corpus/templates/clips/actions/create-meeting.ts +8 -3
- package/corpus/templates/clips/actions/list-meetings.ts +31 -3
- package/corpus/templates/clips/changelog/2026-07-08-after-finishing-a-desktop-recording-reopening-clips-restor.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-08-clips-now-pops-up-to-take-notes-when-you-join-an-adhoc-zoom-.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-08-desktop-clip-recordings-are-louder-after-capture-mic-only.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-08-meeting-reminders-now-appear-1-minute-before-start-stay-unti.md +6 -0
- package/corpus/templates/clips/changelog/2026-07-08-the-desktop-meetings-panel-has-room-for-its-drop-shadow-and-.md +6 -0
- package/corpus/templates/clips/chrome-extension/package.json +2 -2
- package/corpus/templates/clips/desktop/package.json +2 -2
- package/corpus/templates/clips/desktop/src/app.tsx +91 -34
- package/corpus/templates/clips/desktop/src/hooks/useMeetingTranscription.ts +15 -5
- package/corpus/templates/clips/desktop/src/lib/meeting-notification-timing.ts +83 -0
- package/corpus/templates/clips/desktop/src/lib/native-recording-warm.ts +21 -0
- package/corpus/templates/clips/desktop/src/lib/recorder.ts +98 -20
- package/corpus/templates/clips/desktop/src/overlays/meeting-notification.tsx +174 -50
- package/corpus/templates/clips/desktop/src/styles.css +128 -53
- package/corpus/templates/clips/desktop/src-tauri/src/adhoc_meetings_watcher.rs +364 -0
- package/corpus/templates/clips/desktop/src-tauri/src/clips/mod.rs +1 -1
- package/corpus/templates/clips/desktop/src-tauri/src/lib.rs +5 -0
- package/corpus/templates/clips/desktop/src-tauri/src/meetings_watcher.rs +91 -11
- package/corpus/templates/clips/desktop/src-tauri/src/native_screen.rs +69 -19
- package/corpus/templates/clips/desktop/src-tauri/src/native_speech.rs +36 -1
- package/corpus/templates/clips/desktop/src-tauri/src/notifications.rs +62 -10
- package/corpus/templates/clips/desktop/src-tauri/src/silence_detector.rs +1 -24
- package/corpus/templates/clips/desktop/src-tauri/src/util.rs +36 -1
- package/corpus/templates/clips/package.json +2 -2
- package/corpus/templates/content/actions/_content-database-personal-view.ts +80 -0
- package/corpus/templates/content/actions/get-content-database-personal-view.ts +27 -0
- package/corpus/templates/content/actions/update-content-database-personal-view.ts +31 -0
- package/corpus/templates/content/actions/update-content-database-view.ts +3 -0
- package/corpus/templates/content/app/components/editor/DocumentProperties.tsx +1 -1
- package/corpus/templates/content/app/components/editor/database/DatabaseView.tsx +2584 -546
- package/corpus/templates/content/app/components/editor/database/filter-sort.ts +220 -18
- package/corpus/templates/content/app/hooks/use-content-database.ts +32 -0
- package/corpus/templates/content/app/i18n/zh-TW.ts +16 -0
- package/corpus/templates/content/app/i18n-data.ts +154 -0
- package/corpus/templates/content/changelog/2026-07-08-database-filters-now-support-notion-style-reset-save-for-eve.md +6 -0
- package/corpus/templates/content/changelog/2026-07-08-date-filters-can-now-target-a-between-range.md +6 -0
- package/corpus/templates/content/package.json +2 -2
- package/corpus/templates/content/parity/matrix.md +1 -1
- package/corpus/templates/content/parity/matrix.ts +2 -0
- package/corpus/templates/content/shared/api.ts +24 -0
- package/corpus/templates/design/package.json +2 -2
- package/corpus/templates/dispatch/app/i18n/zh-TW.ts +34 -1
- package/corpus/templates/dispatch/app/i18n-data.ts +387 -10
- package/corpus/templates/dispatch/app/routes/automations.tsx +1 -0
- package/corpus/templates/dispatch/changelog/2026-07-08-added-an-automations-page-to-review-pause-and-create.md +6 -0
- package/corpus/templates/dispatch/changelog/2026-07-08-destinations-now-shows-outbound-delivery-queue-health.md +6 -0
- package/corpus/templates/dispatch/package.json +2 -2
- package/corpus/templates/forms/package.json +2 -2
- package/corpus/templates/macros/package.json +2 -2
- package/corpus/templates/mail/app/hooks/use-emails.ts +56 -44
- package/corpus/templates/mail/app/lib/gmail-mutation-queue.ts +325 -0
- package/corpus/templates/mail/changelog/2026-07-08-rapid-archive-and-mark-read-now-batch-gmail-updates-so-the-i.md +6 -0
- package/corpus/templates/mail/package.json +2 -2
- package/corpus/templates/plan/package.json +2 -2
- package/corpus/templates/slides/app/components/editor/ImageDropPromptPopover.tsx +46 -29
- package/corpus/templates/slides/app/context/DeckContext.tsx +45 -18
- package/corpus/templates/slides/app/i18n/ar-SA.ts +4 -0
- package/corpus/templates/slides/app/i18n/de-DE.ts +4 -0
- package/corpus/templates/slides/app/i18n/en-US.ts +4 -0
- package/corpus/templates/slides/app/i18n/es-ES.ts +4 -0
- package/corpus/templates/slides/app/i18n/fr-FR.ts +4 -0
- package/corpus/templates/slides/app/i18n/hi-IN.ts +4 -0
- package/corpus/templates/slides/app/i18n/ja-JP.ts +4 -0
- package/corpus/templates/slides/app/i18n/ko-KR.ts +4 -0
- package/corpus/templates/slides/app/i18n/pt-BR.ts +4 -0
- package/corpus/templates/slides/app/i18n/zh-CN.ts +4 -0
- package/corpus/templates/slides/app/i18n/zh-TW.ts +4 -0
- package/corpus/templates/slides/app/lib/image-drop-to-agent.ts +115 -0
- package/corpus/templates/slides/app/pages/DeckEditor.tsx +22 -13
- package/corpus/templates/slides/changelog/2026-07-08-dropping-an-image-onto-a-slide-and-sending-it-to-the-agent-w.md +6 -0
- package/corpus/templates/slides/changelog/2026-07-08-undo-works-after-the-agent-edits-a-slide-so-you-can-reverse.md +6 -0
- package/corpus/templates/slides/package.json +2 -2
- package/dist/agent/run-manager.d.ts.map +1 -1
- package/dist/agent/run-manager.js +23 -2
- package/dist/agent/run-manager.js.map +1 -1
- package/dist/agent/run-store.js +2 -2
- package/dist/agent/run-store.js.map +1 -1
- package/dist/catalog.json +2 -2
- package/dist/cli/add.js +1 -1
- package/dist/cli/add.js.map +1 -1
- package/dist/cli/create.d.ts.map +1 -1
- package/dist/cli/create.js +8 -3
- package/dist/cli/create.js.map +1 -1
- package/dist/cli/index.js +5 -4
- package/dist/cli/index.js.map +1 -1
- package/dist/client/extensions/ExtensionSlot.js +6 -0
- package/dist/client/extensions/ExtensionSlot.js.map +1 -1
- package/dist/client/session-replay.d.ts +8 -0
- package/dist/client/session-replay.d.ts.map +1 -1
- package/dist/client/session-replay.js +80 -3
- package/dist/client/session-replay.js.map +1 -1
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/file-upload/actions/upload-image.d.ts +1 -1
- package/dist/jobs/scheduler.js +26 -1
- package/dist/jobs/scheduler.js.map +1 -1
- package/dist/localization/default-messages.d.ts +19 -0
- package/dist/localization/default-messages.d.ts.map +1 -1
- package/dist/localization/default-messages.js +19 -0
- package/dist/localization/default-messages.js.map +1 -1
- package/dist/provider-api/corpus-jobs.d.ts +2 -2
- package/dist/resources/handlers.d.ts +2 -2
- package/dist/templates/default/app/i18n/ar-SA.ts +21 -0
- package/dist/templates/default/app/i18n/de-DE.ts +21 -0
- package/dist/templates/default/app/i18n/en-US.ts +21 -0
- package/dist/templates/default/app/i18n/es-ES.ts +21 -0
- package/dist/templates/default/app/i18n/fr-FR.ts +21 -0
- package/dist/templates/default/app/i18n/hi-IN.ts +20 -0
- package/dist/templates/default/app/i18n/ja-JP.ts +21 -0
- package/dist/templates/default/app/i18n/ko-KR.ts +20 -0
- package/dist/templates/default/app/i18n/pt-BR.ts +21 -0
- package/dist/templates/default/app/i18n/zh-CN.ts +20 -0
- package/dist/templates/default/app/i18n/zh-TW.ts +19 -0
- package/dist/templates/default/package.json +2 -2
- package/dist/templates/headless/package.json +3 -3
- package/dist/templates/workspace-core/package.json +6 -6
- package/dist/templates/workspace-root/package.json +2 -2
- package/dist/triggers/dispatcher.js +27 -2
- package/dist/triggers/dispatcher.js.map +1 -1
- package/dist/vite/client.d.ts.map +1 -1
- package/dist/vite/client.js +70 -1
- package/dist/vite/client.js.map +1 -1
- package/package.json +7 -6
- package/src/templates/default/app/i18n/ar-SA.ts +21 -0
- package/src/templates/default/app/i18n/de-DE.ts +21 -0
- package/src/templates/default/app/i18n/en-US.ts +21 -0
- package/src/templates/default/app/i18n/es-ES.ts +21 -0
- package/src/templates/default/app/i18n/fr-FR.ts +21 -0
- package/src/templates/default/app/i18n/hi-IN.ts +20 -0
- package/src/templates/default/app/i18n/ja-JP.ts +21 -0
- package/src/templates/default/app/i18n/ko-KR.ts +20 -0
- package/src/templates/default/app/i18n/pt-BR.ts +21 -0
- package/src/templates/default/app/i18n/zh-CN.ts +20 -0
- package/src/templates/default/app/i18n/zh-TW.ts +19 -0
- package/src/templates/default/package.json +2 -2
- package/src/templates/headless/package.json +3 -3
- package/src/templates/workspace-core/package.json +6 -6
- package/src/templates/workspace-root/package.json +2 -2
|
@@ -73,7 +73,6 @@
|
|
|
73
73
|
"@types/react": "catalog:",
|
|
74
74
|
"@types/react-dom": "catalog:",
|
|
75
75
|
"@types/three": "^0.176.0",
|
|
76
|
-
"@typescript/native-preview": "catalog:",
|
|
77
76
|
"@vitejs/plugin-react-swc": "^4.0.0",
|
|
78
77
|
"chart.js": "^4.5.1",
|
|
79
78
|
"chartjs-node-canvas": "^5.0.0",
|
|
@@ -101,7 +100,8 @@
|
|
|
101
100
|
"tailwindcss": "catalog:",
|
|
102
101
|
"three": "^0.176.0",
|
|
103
102
|
"tsx": "^4.20.3",
|
|
104
|
-
"typescript": "
|
|
103
|
+
"typescript": "catalog:",
|
|
104
|
+
"typescript-7": "catalog:",
|
|
105
105
|
"vaul": "^1.1.2",
|
|
106
106
|
"vite": "catalog:",
|
|
107
107
|
"vitest": "catalog:"
|
|
@@ -55,7 +55,6 @@
|
|
|
55
55
|
"@types/node": "^24.2.1",
|
|
56
56
|
"@types/react": "catalog:",
|
|
57
57
|
"@types/react-dom": "catalog:",
|
|
58
|
-
"@typescript/native-preview": "catalog:",
|
|
59
58
|
"class-variance-authority": "^0.7.1",
|
|
60
59
|
"clsx": "^2.1.1",
|
|
61
60
|
"next-themes": "^0.4.6",
|
|
@@ -67,7 +66,8 @@
|
|
|
67
66
|
"tailwind-merge": "^3.5.0",
|
|
68
67
|
"tailwindcss": "catalog:",
|
|
69
68
|
"tsx": "^4.20.3",
|
|
70
|
-
"typescript": "
|
|
69
|
+
"typescript": "catalog:",
|
|
70
|
+
"typescript-7": "catalog:",
|
|
71
71
|
"vite": "catalog:",
|
|
72
72
|
"vitest": "catalog:"
|
|
73
73
|
}
|
|
@@ -48,7 +48,6 @@
|
|
|
48
48
|
"@types/node": "^25.8.0",
|
|
49
49
|
"@types/react": "catalog:",
|
|
50
50
|
"@types/react-dom": "catalog:",
|
|
51
|
-
"@typescript/native-preview": "catalog:",
|
|
52
51
|
"@vitejs/plugin-react-swc": "^4.3.1",
|
|
53
52
|
"class-variance-authority": "^0.7.1",
|
|
54
53
|
"clsx": "^2.1.1",
|
|
@@ -58,7 +57,8 @@
|
|
|
58
57
|
"sonner": "^2.0.7",
|
|
59
58
|
"tailwind-merge": "^3.6.0",
|
|
60
59
|
"tailwindcss": "catalog:",
|
|
61
|
-
"typescript": "
|
|
60
|
+
"typescript": "catalog:",
|
|
61
|
+
"typescript-7": "catalog:",
|
|
62
62
|
"vite": "catalog:",
|
|
63
63
|
"vitest": "catalog:"
|
|
64
64
|
}
|
|
@@ -43,6 +43,16 @@ Detailed event, availability, booking, storage, and UI rules live in
|
|
|
43
43
|
co-hosts to the created Google Calendar event.
|
|
44
44
|
- Keep scheduling answers concrete: exact dates, time zones, conflicts, and
|
|
45
45
|
assumptions.
|
|
46
|
+
- Event detail (panel and popover) exposes `calendar.event-detail.bottom` as an
|
|
47
|
+
`ExtensionSlot`. Extensions render as widgets there with `slotContext`
|
|
48
|
+
(eventId, title, start/end, timezones, location, attendees, accountEmail).
|
|
49
|
+
For inline adornments next to each guest email (e.g. local times), prefer the
|
|
50
|
+
first-party attendee timezone UI / `set-attendee-timezone` settings, or a
|
|
51
|
+
source edit — do not claim the slot can inject per-row UI.
|
|
52
|
+
- Use `get-attendee-timezones` / `set-attendee-timezone` to read or save
|
|
53
|
+
per-guest IANA timezone overrides (`attendee-timezones` user setting). The UI
|
|
54
|
+
shows each guest's local event-start time when a timezone is known (self from
|
|
55
|
+
the event/browser zone; others from `attendee.timeZone` or the override map).
|
|
46
56
|
- Use `rsvp-event` for invitation responses. Pass `note` when the user wants a
|
|
47
57
|
visible RSVP comment on a declined or tentative response; pass an empty note to
|
|
48
58
|
clear an existing RSVP comment.
|
|
@@ -19,6 +19,7 @@ import {
|
|
|
19
19
|
cliBoolean,
|
|
20
20
|
eventTypeInput,
|
|
21
21
|
googleColorIdInput,
|
|
22
|
+
ensureOrganizerInAttendees,
|
|
22
23
|
normalizeAttendees,
|
|
23
24
|
reminderMethodInput,
|
|
24
25
|
reminderMinutesInput,
|
|
@@ -140,7 +141,10 @@ export default defineAction({
|
|
|
140
141
|
acctEmail = args.accountEmail;
|
|
141
142
|
}
|
|
142
143
|
|
|
143
|
-
const attendees =
|
|
144
|
+
const attendees = ensureOrganizerInAttendees(
|
|
145
|
+
normalizeAttendees(args.attendees),
|
|
146
|
+
acctEmail,
|
|
147
|
+
);
|
|
144
148
|
const reminderFields = buildReminderOverrides({
|
|
145
149
|
reminders: args.reminders,
|
|
146
150
|
reminderMinutes: args.reminderMinutes,
|
|
@@ -112,6 +112,47 @@ export function normalizeAttendees(
|
|
|
112
112
|
}));
|
|
113
113
|
}
|
|
114
114
|
|
|
115
|
+
/**
|
|
116
|
+
* Google Calendar's UI always lists the organizer in Guests when inviting
|
|
117
|
+
* others. The insert API does not — unless we include the organizer/self
|
|
118
|
+
* email in `attendees`. Call this when creating/publishing an event that
|
|
119
|
+
* already has guests so AN matches GCal.
|
|
120
|
+
*/
|
|
121
|
+
export function ensureOrganizerInAttendees(
|
|
122
|
+
attendees: NormalizedAttendee[] | undefined,
|
|
123
|
+
organizerEmail: string,
|
|
124
|
+
): NormalizedAttendee[] | undefined {
|
|
125
|
+
if (!attendees || attendees.length === 0) return attendees;
|
|
126
|
+
const organizer = organizerEmail.trim().toLowerCase();
|
|
127
|
+
if (!organizer.includes("@")) return attendees;
|
|
128
|
+
|
|
129
|
+
const existing = attendees.find(
|
|
130
|
+
(attendee) => attendee.email.trim().toLowerCase() === organizer,
|
|
131
|
+
);
|
|
132
|
+
if (existing) {
|
|
133
|
+
return attendees.map((attendee) =>
|
|
134
|
+
attendee.email.trim().toLowerCase() === organizer
|
|
135
|
+
? {
|
|
136
|
+
...attendee,
|
|
137
|
+
organizer: true,
|
|
138
|
+
self: true,
|
|
139
|
+
responseStatus: attendee.responseStatus ?? "accepted",
|
|
140
|
+
}
|
|
141
|
+
: attendee,
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
return [
|
|
146
|
+
{
|
|
147
|
+
email: organizerEmail.trim(),
|
|
148
|
+
organizer: true,
|
|
149
|
+
self: true,
|
|
150
|
+
responseStatus: "accepted",
|
|
151
|
+
},
|
|
152
|
+
...attendees,
|
|
153
|
+
];
|
|
154
|
+
}
|
|
155
|
+
|
|
115
156
|
export function requireActionUserEmail(): string {
|
|
116
157
|
const email = getRequestUserEmail();
|
|
117
158
|
if (!email) throw new Error("no authenticated user");
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { defineAction } from "@agent-native/core";
|
|
2
|
+
import { getRequestUserEmail } from "@agent-native/core/server";
|
|
3
|
+
import { getUserSetting } from "@agent-native/core/settings";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
|
|
6
|
+
export type AttendeeTimezones = Record<string, string>;
|
|
7
|
+
|
|
8
|
+
export default defineAction({
|
|
9
|
+
description:
|
|
10
|
+
"Get saved per-attendee IANA timezone overrides (email → timezone). Used to show each guest's local time on event details.",
|
|
11
|
+
schema: z.object({}),
|
|
12
|
+
http: { method: "GET" },
|
|
13
|
+
run: async () => {
|
|
14
|
+
const email = getRequestUserEmail();
|
|
15
|
+
if (!email) throw new Error("no authenticated user");
|
|
16
|
+
const data = await getUserSetting(email, "attendee-timezones");
|
|
17
|
+
if (!data || typeof data !== "object" || Array.isArray(data)) {
|
|
18
|
+
return {} as AttendeeTimezones;
|
|
19
|
+
}
|
|
20
|
+
const result: AttendeeTimezones = {};
|
|
21
|
+
for (const [key, value] of Object.entries(data)) {
|
|
22
|
+
if (typeof value === "string" && value.trim()) {
|
|
23
|
+
result[key.trim().toLowerCase()] = value.trim();
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
return result;
|
|
27
|
+
},
|
|
28
|
+
});
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { defineAction } from "@agent-native/core";
|
|
2
|
+
import { getRequestUserEmail } from "@agent-native/core/server";
|
|
3
|
+
import { getUserSetting, putUserSetting } from "@agent-native/core/settings";
|
|
4
|
+
import { z } from "zod";
|
|
5
|
+
|
|
6
|
+
import type { AttendeeTimezones } from "./get-attendee-timezones.js";
|
|
7
|
+
|
|
8
|
+
function isValidIanaTimeZone(timeZone: string): boolean {
|
|
9
|
+
try {
|
|
10
|
+
new Intl.DateTimeFormat("en-US", { timeZone }).format();
|
|
11
|
+
return true;
|
|
12
|
+
} catch {
|
|
13
|
+
return false;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export default defineAction({
|
|
18
|
+
description:
|
|
19
|
+
"Save or clear a per-attendee IANA timezone override so event details can show that guest's local time. Pass timeZone to set; omit or pass empty to clear.",
|
|
20
|
+
schema: z.object({
|
|
21
|
+
email: z
|
|
22
|
+
.string()
|
|
23
|
+
.email()
|
|
24
|
+
.describe("Attendee email whose timezone override to set"),
|
|
25
|
+
timeZone: z
|
|
26
|
+
.string()
|
|
27
|
+
.optional()
|
|
28
|
+
.describe(
|
|
29
|
+
"IANA timezone (e.g. America/New_York). Omit or pass empty string to clear the override.",
|
|
30
|
+
),
|
|
31
|
+
}),
|
|
32
|
+
http: { method: "PUT" },
|
|
33
|
+
run: async (args) => {
|
|
34
|
+
const ownerEmail = getRequestUserEmail();
|
|
35
|
+
if (!ownerEmail) throw new Error("no authenticated user");
|
|
36
|
+
|
|
37
|
+
const attendeeEmail = args.email.trim().toLowerCase();
|
|
38
|
+
const nextZone = args.timeZone?.trim() || "";
|
|
39
|
+
if (nextZone && !isValidIanaTimeZone(nextZone)) {
|
|
40
|
+
throw new Error(`Invalid IANA timezone: ${nextZone}`);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
const existing =
|
|
44
|
+
((await getUserSetting(
|
|
45
|
+
ownerEmail,
|
|
46
|
+
"attendee-timezones",
|
|
47
|
+
)) as AttendeeTimezones | null) ?? {};
|
|
48
|
+
const updated: AttendeeTimezones = {};
|
|
49
|
+
for (const [key, value] of Object.entries(existing)) {
|
|
50
|
+
if (typeof value === "string" && value.trim()) {
|
|
51
|
+
updated[key.trim().toLowerCase()] = value.trim();
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
if (nextZone) {
|
|
56
|
+
updated[attendeeEmail] = nextZone;
|
|
57
|
+
} else {
|
|
58
|
+
delete updated[attendeeEmail];
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
await putUserSetting(
|
|
62
|
+
ownerEmail,
|
|
63
|
+
"attendee-timezones",
|
|
64
|
+
updated as unknown as Record<string, unknown>,
|
|
65
|
+
);
|
|
66
|
+
return updated;
|
|
67
|
+
},
|
|
68
|
+
});
|
|
@@ -4,6 +4,7 @@ import { IconDots, IconMessageCircle, IconUser } from "@tabler/icons-react";
|
|
|
4
4
|
import { useEffect, useId, useMemo, useState } from "react";
|
|
5
5
|
|
|
6
6
|
import { AttendeeApolloPopover } from "@/components/calendar/ApolloPanel";
|
|
7
|
+
import { TimezoneCombobox } from "@/components/TimezoneCombobox";
|
|
7
8
|
import { Button } from "@/components/ui/button";
|
|
8
9
|
import {
|
|
9
10
|
DropdownMenu,
|
|
@@ -21,7 +22,16 @@ import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
|
|
|
21
22
|
import { Separator } from "@/components/ui/separator";
|
|
22
23
|
import { Textarea } from "@/components/ui/textarea";
|
|
23
24
|
import { useAttendeePhotos } from "@/hooks/use-attendee-photos";
|
|
25
|
+
import {
|
|
26
|
+
useAttendeeTimezones,
|
|
27
|
+
useSetAttendeeTimezone,
|
|
28
|
+
} from "@/hooks/use-attendee-timezones";
|
|
24
29
|
import { useRsvpEvent } from "@/hooks/use-events";
|
|
30
|
+
import {
|
|
31
|
+
getAttendeeLocalTimeLabel,
|
|
32
|
+
resolveAttendeeTimeZone,
|
|
33
|
+
} from "@/lib/attendee-local-time";
|
|
34
|
+
import { getLocalTimezone } from "@/lib/event-form-utils";
|
|
25
35
|
import {
|
|
26
36
|
canInlineRsvp,
|
|
27
37
|
RsvpStatusIcon,
|
|
@@ -351,9 +361,14 @@ function AttendeeRow({
|
|
|
351
361
|
isRecurring,
|
|
352
362
|
canEditOptional,
|
|
353
363
|
onToggleOptional,
|
|
364
|
+
timezoneOverrides,
|
|
365
|
+
onSetTimezone,
|
|
354
366
|
}: {
|
|
355
367
|
attendee: Attendee;
|
|
356
|
-
event: Pick<
|
|
368
|
+
event: Pick<
|
|
369
|
+
CalendarEvent,
|
|
370
|
+
"id" | "accountEmail" | "start" | "startTimeZone" | "allDay"
|
|
371
|
+
>;
|
|
357
372
|
photoUrl?: string;
|
|
358
373
|
inlineRsvp?: boolean;
|
|
359
374
|
currentStatus?: RsvpStatus;
|
|
@@ -362,14 +377,42 @@ function AttendeeRow({
|
|
|
362
377
|
isRecurring?: boolean;
|
|
363
378
|
canEditOptional?: boolean;
|
|
364
379
|
onToggleOptional?: (email: string, optional: boolean) => void;
|
|
380
|
+
timezoneOverrides?: Record<string, string>;
|
|
381
|
+
onSetTimezone?: (email: string, timeZone: string | undefined) => void;
|
|
365
382
|
}) {
|
|
366
383
|
const t = useT();
|
|
384
|
+
const [timezonePickerOpen, setTimezonePickerOpen] = useState(false);
|
|
385
|
+
const resolvedZone =
|
|
386
|
+
resolveAttendeeTimeZone({
|
|
387
|
+
attendee,
|
|
388
|
+
accountEmail: event.accountEmail,
|
|
389
|
+
eventStartTimeZone: event.startTimeZone,
|
|
390
|
+
overrides: timezoneOverrides,
|
|
391
|
+
}) ?? getLocalTimezone();
|
|
392
|
+
const [draftTimezone, setDraftTimezone] = useState(resolvedZone);
|
|
367
393
|
const displayStatus = inlineRsvp ? currentStatus : attendee.responseStatus;
|
|
368
394
|
const statusLabel =
|
|
369
395
|
getLocalizedRsvpStatusLabel(t, displayStatus) ?? t("eventForm.awaiting");
|
|
370
396
|
const comment = (inlineRsvp ? currentNote : attendee.comment)?.trim();
|
|
371
397
|
const showOptionalMenu =
|
|
372
398
|
canEditOptional && onToggleOptional && !attendee.organizer;
|
|
399
|
+
const showMenu = showOptionalMenu || !!onSetTimezone;
|
|
400
|
+
const localTimeLabel =
|
|
401
|
+
!event.allDay && event.start
|
|
402
|
+
? getAttendeeLocalTimeLabel({
|
|
403
|
+
attendee,
|
|
404
|
+
accountEmail: event.accountEmail,
|
|
405
|
+
eventStartTimeZone: event.startTimeZone,
|
|
406
|
+
overrides: timezoneOverrides,
|
|
407
|
+
startIso: event.start,
|
|
408
|
+
})
|
|
409
|
+
: null;
|
|
410
|
+
|
|
411
|
+
useEffect(() => {
|
|
412
|
+
if (timezonePickerOpen) {
|
|
413
|
+
setDraftTimezone(resolvedZone);
|
|
414
|
+
}
|
|
415
|
+
}, [timezonePickerOpen, resolvedZone]);
|
|
373
416
|
|
|
374
417
|
return (
|
|
375
418
|
<div className="group rounded-xl px-1 py-1 transition-colors hover:bg-muted/40">
|
|
@@ -400,15 +443,25 @@ function AttendeeRow({
|
|
|
400
443
|
{attendee.displayName && (
|
|
401
444
|
<div className="truncate text-[11px] text-muted-foreground/60">
|
|
402
445
|
{attendee.email}
|
|
446
|
+
{localTimeLabel ? (
|
|
447
|
+
<span className="ms-1.5 text-muted-foreground/50">
|
|
448
|
+
· {localTimeLabel}
|
|
449
|
+
</span>
|
|
450
|
+
) : null}
|
|
403
451
|
</div>
|
|
404
452
|
)}
|
|
453
|
+
{!attendee.displayName && localTimeLabel ? (
|
|
454
|
+
<div className="truncate text-[11px] text-muted-foreground/50">
|
|
455
|
+
{localTimeLabel}
|
|
456
|
+
</div>
|
|
457
|
+
) : null}
|
|
405
458
|
<div className="mt-0.5 text-[11px] text-muted-foreground/70">
|
|
406
459
|
{inlineRsvp
|
|
407
460
|
? t("eventForm.yourResponse", { status: statusLabel })
|
|
408
461
|
: statusLabel}
|
|
409
462
|
</div>
|
|
410
463
|
</div>
|
|
411
|
-
{
|
|
464
|
+
{showMenu && (
|
|
412
465
|
<DropdownMenu>
|
|
413
466
|
<DropdownMenuTrigger asChild>
|
|
414
467
|
<Button
|
|
@@ -428,15 +481,27 @@ function AttendeeRow({
|
|
|
428
481
|
align="end"
|
|
429
482
|
onClick={(event) => event.stopPropagation()}
|
|
430
483
|
>
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
484
|
+
{showOptionalMenu && (
|
|
485
|
+
<DropdownMenuItem
|
|
486
|
+
onSelect={() =>
|
|
487
|
+
onToggleOptional(attendee.email, !attendee.optional)
|
|
488
|
+
}
|
|
489
|
+
>
|
|
490
|
+
{attendee.optional
|
|
491
|
+
? t("attendees.markRequired")
|
|
492
|
+
: t("attendees.markOptional")}
|
|
493
|
+
</DropdownMenuItem>
|
|
494
|
+
)}
|
|
495
|
+
{onSetTimezone && (
|
|
496
|
+
<DropdownMenuItem
|
|
497
|
+
onSelect={(event) => {
|
|
498
|
+
event.preventDefault();
|
|
499
|
+
setTimezonePickerOpen(true);
|
|
500
|
+
}}
|
|
501
|
+
>
|
|
502
|
+
{t("attendees.setTimezone")}
|
|
503
|
+
</DropdownMenuItem>
|
|
504
|
+
)}
|
|
440
505
|
</DropdownMenuContent>
|
|
441
506
|
</DropdownMenu>
|
|
442
507
|
)}
|
|
@@ -448,6 +513,67 @@ function AttendeeRow({
|
|
|
448
513
|
</div>
|
|
449
514
|
)}
|
|
450
515
|
</AttendeeApolloPopover>
|
|
516
|
+
{onSetTimezone && (
|
|
517
|
+
<Popover open={timezonePickerOpen} onOpenChange={setTimezonePickerOpen}>
|
|
518
|
+
<PopoverTrigger asChild>
|
|
519
|
+
<button type="button" className="sr-only" tabIndex={-1} />
|
|
520
|
+
</PopoverTrigger>
|
|
521
|
+
<PopoverContent
|
|
522
|
+
align="end"
|
|
523
|
+
side="left"
|
|
524
|
+
className="w-72 p-3"
|
|
525
|
+
onClick={(e) => e.stopPropagation()}
|
|
526
|
+
onPointerDownCapture={(e) => e.stopPropagation()}
|
|
527
|
+
>
|
|
528
|
+
<div className="space-y-2">
|
|
529
|
+
<p className="text-sm font-medium">
|
|
530
|
+
{t("attendees.setTimezone")}
|
|
531
|
+
</p>
|
|
532
|
+
<p className="text-[11px] text-muted-foreground">
|
|
533
|
+
{attendee.displayName || attendee.email}
|
|
534
|
+
</p>
|
|
535
|
+
<TimezoneCombobox
|
|
536
|
+
id={`attendee-tz-${attendee.email}`}
|
|
537
|
+
value={draftTimezone}
|
|
538
|
+
onChange={setDraftTimezone}
|
|
539
|
+
/>
|
|
540
|
+
<div className="flex justify-end gap-1.5 pt-1">
|
|
541
|
+
{timezoneOverrides?.[attendee.email.trim().toLowerCase()] ? (
|
|
542
|
+
<Button
|
|
543
|
+
variant="ghost"
|
|
544
|
+
size="sm"
|
|
545
|
+
className="h-7 text-xs me-auto"
|
|
546
|
+
onClick={() => {
|
|
547
|
+
onSetTimezone(attendee.email, undefined);
|
|
548
|
+
setTimezonePickerOpen(false);
|
|
549
|
+
}}
|
|
550
|
+
>
|
|
551
|
+
{t("attendees.clearTimezone")}
|
|
552
|
+
</Button>
|
|
553
|
+
) : null}
|
|
554
|
+
<Button
|
|
555
|
+
variant="ghost"
|
|
556
|
+
size="sm"
|
|
557
|
+
className="h-7 text-xs"
|
|
558
|
+
onClick={() => setTimezonePickerOpen(false)}
|
|
559
|
+
>
|
|
560
|
+
{t("eventForm.cancel")}
|
|
561
|
+
</Button>
|
|
562
|
+
<Button
|
|
563
|
+
size="sm"
|
|
564
|
+
className="h-7 text-xs"
|
|
565
|
+
onClick={() => {
|
|
566
|
+
onSetTimezone(attendee.email, draftTimezone);
|
|
567
|
+
setTimezonePickerOpen(false);
|
|
568
|
+
}}
|
|
569
|
+
>
|
|
570
|
+
{t("eventForm.save")}
|
|
571
|
+
</Button>
|
|
572
|
+
</div>
|
|
573
|
+
</div>
|
|
574
|
+
</PopoverContent>
|
|
575
|
+
</Popover>
|
|
576
|
+
)}
|
|
451
577
|
{inlineRsvp && currentStatus && onResponseChange && (
|
|
452
578
|
<RsvpControls
|
|
453
579
|
eventId={event.id}
|
|
@@ -486,6 +612,9 @@ export function EventAttendeesSection({
|
|
|
486
612
|
| "responseStatus"
|
|
487
613
|
| "source"
|
|
488
614
|
| "recurringEventId"
|
|
615
|
+
| "start"
|
|
616
|
+
| "startTimeZone"
|
|
617
|
+
| "allDay"
|
|
489
618
|
>;
|
|
490
619
|
canEditOptional?: boolean;
|
|
491
620
|
onToggleOptional?: (email: string, optional: boolean) => void;
|
|
@@ -501,6 +630,8 @@ export function EventAttendeesSection({
|
|
|
501
630
|
);
|
|
502
631
|
const emails = attendees.map((attendee) => attendee.email);
|
|
503
632
|
const { data: photos } = useAttendeePhotos(emails);
|
|
633
|
+
const { data: timezoneOverrides } = useAttendeeTimezones();
|
|
634
|
+
const setAttendeeTimezone = useSetAttendeeTimezone();
|
|
504
635
|
|
|
505
636
|
const sorted = useMemo(() => sortAttendees(attendees), [attendees]);
|
|
506
637
|
const canRsvpInline = canInlineRsvp(event);
|
|
@@ -522,6 +653,10 @@ export function EventAttendeesSection({
|
|
|
522
653
|
setSelfNote(note);
|
|
523
654
|
};
|
|
524
655
|
|
|
656
|
+
const handleSetTimezone = (email: string, timeZone: string | undefined) => {
|
|
657
|
+
setAttendeeTimezone.mutate({ email, timeZone });
|
|
658
|
+
};
|
|
659
|
+
|
|
525
660
|
const shouldTruncate = attendees.length > ATTENDEE_TRUNCATE_THRESHOLD;
|
|
526
661
|
const visibleOthers =
|
|
527
662
|
shouldTruncate && !expanded
|
|
@@ -578,6 +713,8 @@ export function EventAttendeesSection({
|
|
|
578
713
|
photoUrl={photos?.[attendee.email.toLowerCase()]}
|
|
579
714
|
canEditOptional={canEditOptional}
|
|
580
715
|
onToggleOptional={onToggleOptional}
|
|
716
|
+
timezoneOverrides={timezoneOverrides}
|
|
717
|
+
onSetTimezone={handleSetTimezone}
|
|
581
718
|
/>
|
|
582
719
|
))}
|
|
583
720
|
|
|
@@ -614,6 +751,8 @@ export function EventAttendeesSection({
|
|
|
614
751
|
isRecurring={!!event.recurringEventId}
|
|
615
752
|
canEditOptional={canEditOptional}
|
|
616
753
|
onToggleOptional={onToggleOptional}
|
|
754
|
+
timezoneOverrides={timezoneOverrides}
|
|
755
|
+
onSetTimezone={handleSetTimezone}
|
|
617
756
|
/>
|
|
618
757
|
</>
|
|
619
758
|
)}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { useT } from "@agent-native/core/client";
|
|
2
|
+
import { ExtensionSlot } from "@agent-native/core/client/extensions";
|
|
2
3
|
import type { CalendarEvent } from "@shared/api";
|
|
3
4
|
import {
|
|
4
5
|
IconX,
|
|
@@ -12,7 +13,7 @@ import {
|
|
|
12
13
|
IconVideo,
|
|
13
14
|
} from "@tabler/icons-react";
|
|
14
15
|
import { format, parseISO, differenceInMinutes } from "date-fns";
|
|
15
|
-
import { useState, useRef, useEffect, useCallback } from "react";
|
|
16
|
+
import { useState, useRef, useEffect, useCallback, useMemo } from "react";
|
|
16
17
|
import { toast } from "sonner";
|
|
17
18
|
|
|
18
19
|
import { ResearchMeetingButton } from "@/components/calendar/ApolloPanel";
|
|
@@ -34,6 +35,28 @@ import { useUpdateEvent } from "@/hooks/use-events";
|
|
|
34
35
|
import { useViewPreferences } from "@/hooks/use-view-preferences";
|
|
35
36
|
import { cn } from "@/lib/utils";
|
|
36
37
|
|
|
38
|
+
function buildEventDetailSlotContext(event: CalendarEvent) {
|
|
39
|
+
return {
|
|
40
|
+
eventId: event.id,
|
|
41
|
+
title: event.title,
|
|
42
|
+
start: event.start,
|
|
43
|
+
end: event.end,
|
|
44
|
+
startTimeZone: event.startTimeZone,
|
|
45
|
+
endTimeZone: event.endTimeZone,
|
|
46
|
+
location: event.location,
|
|
47
|
+
accountEmail: event.accountEmail,
|
|
48
|
+
attendees: (event.attendees ?? []).map((attendee) => ({
|
|
49
|
+
email: attendee.email,
|
|
50
|
+
displayName: attendee.displayName,
|
|
51
|
+
responseStatus: attendee.responseStatus,
|
|
52
|
+
organizer: attendee.organizer,
|
|
53
|
+
optional: attendee.optional,
|
|
54
|
+
timeZone: attendee.timeZone,
|
|
55
|
+
self: attendee.self,
|
|
56
|
+
})),
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
37
60
|
interface EventDetailPanelProps {
|
|
38
61
|
event: CalendarEvent | null;
|
|
39
62
|
onClose: () => void;
|
|
@@ -105,6 +128,10 @@ export function EventDetailPanel({
|
|
|
105
128
|
const lastSavedDescriptionRef = useRef(event?.description || "");
|
|
106
129
|
const meetingLink = event ? extractMeetingLink(event) : null;
|
|
107
130
|
const ownerLabel = event?.ownerName || event?.overlayEmail;
|
|
131
|
+
const eventDetailSlotContext = useMemo(
|
|
132
|
+
() => (event ? buildEventDetailSlotContext(event) : null),
|
|
133
|
+
[event],
|
|
134
|
+
);
|
|
108
135
|
|
|
109
136
|
// Reset editing state when event changes
|
|
110
137
|
useEffect(() => {
|
|
@@ -460,6 +487,14 @@ export function EventDetailPanel({
|
|
|
460
487
|
{event.attendees && event.attendees.length > 0 && (
|
|
461
488
|
<ResearchMeetingButton event={event} />
|
|
462
489
|
)}
|
|
490
|
+
|
|
491
|
+
{eventDetailSlotContext && (
|
|
492
|
+
<ExtensionSlot
|
|
493
|
+
id="calendar.event-detail.bottom"
|
|
494
|
+
context={eventDetailSlotContext}
|
|
495
|
+
showEmptyAffordance
|
|
496
|
+
/>
|
|
497
|
+
)}
|
|
463
498
|
</div>
|
|
464
499
|
|
|
465
500
|
{/* Actions */}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { sendToAgentChat, useT } from "@agent-native/core/client";
|
|
2
|
+
import { ExtensionSlot } from "@agent-native/core/client/extensions";
|
|
2
3
|
import type {
|
|
3
4
|
CalendarEvent,
|
|
4
5
|
FindTimeSlot,
|
|
@@ -96,6 +97,28 @@ import { shortcutModifierLabel } from "@/lib/utils";
|
|
|
96
97
|
const ZOOM_AFTER_CONNECT_EVENT_ID_KEY = "calendar.zoomAfterConnectEventId";
|
|
97
98
|
const ZOOM_AFTER_CONNECT_MAX_AGE_MS = 10 * 60 * 1000;
|
|
98
99
|
|
|
100
|
+
function buildEventDetailSlotContext(event: CalendarEvent) {
|
|
101
|
+
return {
|
|
102
|
+
eventId: event.id,
|
|
103
|
+
title: event.title,
|
|
104
|
+
start: event.start,
|
|
105
|
+
end: event.end,
|
|
106
|
+
startTimeZone: event.startTimeZone,
|
|
107
|
+
endTimeZone: event.endTimeZone,
|
|
108
|
+
location: event.location,
|
|
109
|
+
accountEmail: event.accountEmail,
|
|
110
|
+
attendees: (event.attendees ?? []).map((attendee) => ({
|
|
111
|
+
email: attendee.email,
|
|
112
|
+
displayName: attendee.displayName,
|
|
113
|
+
responseStatus: attendee.responseStatus,
|
|
114
|
+
organizer: attendee.organizer,
|
|
115
|
+
optional: attendee.optional,
|
|
116
|
+
timeZone: attendee.timeZone,
|
|
117
|
+
self: attendee.self,
|
|
118
|
+
})),
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
99
122
|
function getStoredZoomAfterConnectEventId(): string | null {
|
|
100
123
|
if (typeof window === "undefined") return null;
|
|
101
124
|
try {
|
|
@@ -1681,6 +1704,15 @@ export function EventDetailPopover({
|
|
|
1681
1704
|
</>
|
|
1682
1705
|
)}
|
|
1683
1706
|
|
|
1707
|
+
<div className="mx-4 my-2 border-t border-border/50" />
|
|
1708
|
+
<div className="px-4 py-1">
|
|
1709
|
+
<ExtensionSlot
|
|
1710
|
+
id="calendar.event-detail.bottom"
|
|
1711
|
+
context={buildEventDetailSlotContext(event)}
|
|
1712
|
+
showEmptyAffordance
|
|
1713
|
+
/>
|
|
1714
|
+
</div>
|
|
1715
|
+
|
|
1684
1716
|
{/* Meeting link */}
|
|
1685
1717
|
{meetingLink ? (
|
|
1686
1718
|
<>
|