feat(editor): add calendar invite slash command plugin#3490
feat(editor): add calendar invite slash command plugin#3490christina-de-martinez wants to merge 4 commits into
Conversation
Adds a /calendar-invite slash command that opens a modal for configuring a calendar event (date picker, time/duration pills, timezone with GMT offset, location, notes). Inserts a card with provider buttons (Google, Outlook, Apple, Yahoo) that link directly to add-to-calendar URLs; Apple Calendar uses a data:text/calendar URI so no file hosting is needed. Supports multiple invites per email, custom card styling via CalendarPluginOptions, and renders correctly to React Email HTML output. Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
commit: |
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
There was a problem hiding this comment.
7 issues found across 14 files
Confidence score: 3/5
- There is moderate merge risk because several high-confidence issues are user-facing (severity 6–7), especially date handling in
packages/editor/src/plugins/calendar-invite/ical-generator.tswhere UTC ISO conversion can shift all-day/end dates to the wrong day in some timezones. - A key behavioral risk is in
packages/editor/src/email-editor/email-editor.tsx: using a globalwindowevent forCalendarInvitePlugincan cause one editor instance to open modals in other editors, and always exposing the slash command can crash whenCalendarInviteis omitted from custom extensions. - Additional correctness issues in
packages/editor/src/plugins/calendar-invite/types.tsandpackages/editor/src/plugins/calendar-invite/modal.tsxcan force valid timezones to UTC and initialize default start times in the past around day rollover, which may produce invalid event defaults. - Pay close attention to
packages/editor/src/plugins/calendar-invite/ical-generator.ts,packages/editor/src/email-editor/email-editor.tsx,packages/editor/src/plugins/calendar-invite/types.ts,packages/editor/src/plugins/calendar-invite/modal.tsx,packages/editor/src/plugins/calendar-invite/slash-command.tsx- timezone/date math and cross-instance command/modal wiring are the main regression hotspots.
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="packages/editor/src/plugins/calendar-invite/ical-generator.ts">
<violation number="1" location="packages/editor/src/plugins/calendar-invite/ical-generator.ts:33">
P1: Deriving `endDate` with `toISOString()` can shift the date by timezone and produce the wrong day in some locales. Format from local date parts instead of UTC ISO.</violation>
<violation number="2" location="packages/editor/src/plugins/calendar-invite/ical-generator.ts:67">
P1: All-day `nextDay` calculation uses UTC ISO conversion, which can produce an incorrect date in some timezones.</violation>
</file>
<file name="packages/editor/src/plugins/calendar-invite/slash-command.tsx">
<violation number="1" location="packages/editor/src/plugins/calendar-invite/slash-command.tsx:11">
P2: Delete the slash-command range before opening the calendar invite modal so canceling the modal does not leave `/calendar-invite` in the document.</violation>
</file>
<file name="packages/editor/src/plugins/calendar-invite/types.ts">
<violation number="1" location="packages/editor/src/plugins/calendar-invite/types.ts:22">
P2: The timezone allowlist is too narrow and forces valid timezones to UTC when they are not listed.</violation>
</file>
<file name="packages/editor/src/email-editor/email-editor.tsx">
<violation number="1" location="packages/editor/src/email-editor/email-editor.tsx:205">
P2: The calendar slash-command is always exposed even when custom `extensions` omit `CalendarInvite`, which can crash when the command is selected.</violation>
<violation number="2" location="packages/editor/src/email-editor/email-editor.tsx:206">
P1: Mounting `CalendarInvitePlugin` per editor currently wires all instances to a global `window` event, so one editor’s calendar action can trigger modals in other editors.</violation>
</file>
<file name="packages/editor/src/plugins/calendar-invite/modal.tsx">
<violation number="1" location="packages/editor/src/plugins/calendar-invite/modal.tsx:99">
P2: Default start time can be initialized to a past time around day rollover, producing an invalid default event time.</violation>
</file>
Reply with feedback, questions, or to request a fix. Tag @cubic-dev-ai to re-run a review.
| <BubbleMenu.ImageDefault /> | ||
| <SlashCommandRoot /> | ||
| <SlashCommandRoot items={slashCommands} /> | ||
| <CalendarInvitePlugin {...(calendarInvite ?? {})} /> |
There was a problem hiding this comment.
P1: Mounting CalendarInvitePlugin per editor currently wires all instances to a global window event, so one editor’s calendar action can trigger modals in other editors.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/editor/src/email-editor/email-editor.tsx, line 206:
<comment>Mounting `CalendarInvitePlugin` per editor currently wires all instances to a global `window` event, so one editor’s calendar action can trigger modals in other editors.</comment>
<file context>
@@ -194,7 +202,8 @@ export const EmailEditor = forwardRef<EmailEditorRef, EmailEditorProps>(
<BubbleMenu.ImageDefault />
- <SlashCommandRoot />
+ <SlashCommandRoot items={slashCommands} />
+ <CalendarInvitePlugin {...(calendarInvite ?? {})} />
{children}
</EditorProvider>
</file context>
| category: 'Utility', | ||
| searchTerms: ['calendar', 'invite', 'event', 'meeting', 'schedule', 'ical', 'ics'], | ||
| command: ({ editor, range }) => { | ||
| editor.commands.openCalendarInviteModal(range); |
There was a problem hiding this comment.
P2: Delete the slash-command range before opening the calendar invite modal so canceling the modal does not leave /calendar-invite in the document.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/editor/src/plugins/calendar-invite/slash-command.tsx, line 11:
<comment>Delete the slash-command range before opening the calendar invite modal so canceling the modal does not leave `/calendar-invite` in the document.</comment>
<file context>
@@ -0,0 +1,13 @@
+ category: 'Utility',
+ searchTerms: ['calendar', 'invite', 'event', 'meeting', 'schedule', 'ical', 'ics'],
+ command: ({ editor, range }) => {
+ editor.commands.openCalendarInviteModal(range);
+ },
+};
</file context>
There was a problem hiding this comment.
I believe the editor uses this when inserting the card once it's created (bc it loses focus and needs to find its way back), so we shouldn't destroy it until success/failure
We should verify this though
There was a problem hiding this comment.
Got it—keeping the range until the modal completes may be necessary for restoring focus/insert; I’ll verify this behavior before recommending early deletion.
| borderColor?: string; // Card border color | ||
| } | ||
|
|
||
| export const CALENDAR_TIMEZONES = [ |
There was a problem hiding this comment.
P2: The timezone allowlist is too narrow and forces valid timezones to UTC when they are not listed.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/editor/src/plugins/calendar-invite/types.ts, line 22:
<comment>The timezone allowlist is too narrow and forces valid timezones to UTC when they are not listed.</comment>
<file context>
@@ -0,0 +1,40 @@
+ borderColor?: string; // Card border color
+}
+
+export const CALENDAR_TIMEZONES = [
+ { label: 'UTC', value: 'UTC' },
+ { label: 'Eastern Time (ET)', value: 'America/New_York' },
</file context>
- ical-generator: fix all-day nextDay and multi-day event endDate using Date.UTC arithmetic to avoid local-timezone shifts from toISOString() - event-bus: scope calendar-invite:open events to the originating editor instance via editorRef so multiple mounted editors don't cross-trigger - email-editor: gate calendarInviteSlashCommand and CalendarInvitePlugin on CalendarInvite being present in the active extension list to prevent crashes when consumers pass custom extensions - modal: fix default start time day-rollover by computing the next 30-min slot in epoch ms instead of using setHours() which wraps at midnight - modal: validate defaultTimezone with Intl.DateTimeFormat instead of checking the curated list, so valid IANA timezones outside the list are no longer silently coerced to UTC; inject them into the select options Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
There was a problem hiding this comment.
2 issues found across 6 files (changes from recent commits).
Prompt for AI agents (unresolved issues)
Check if these issues are valid — if so, understand the root cause of each and fix them. If appropriate, use sub-agents to investigate and fix each issue separately.
<file name="packages/editor/src/plugins/calendar-invite/modal.tsx">
<violation number="1" location="packages/editor/src/plugins/calendar-invite/modal.tsx:107">
P2: Default start time can be initialized to `23:30`, but that value is not in `TIME_SLOTS`, causing an inconsistent/invalid initial selection near late evening.</violation>
</file>
<file name="packages/editor/src/plugins/calendar-invite/plugin.tsx">
<violation number="1" location="packages/editor/src/plugins/calendar-invite/plugin.tsx:19">
P1: This guard captures the initial editor value, so the listener never starts accepting events after the editor initializes.</violation>
</file>
Tip: Review your code locally with the cubic CLI to iterate faster.
|
|
||
| useEffect(() => { | ||
| const sub = editorEventBus.on('calendar-invite:open', (payload) => { | ||
| if (payload.editorRef !== editor) return; |
There was a problem hiding this comment.
P1: This guard captures the initial editor value, so the listener never starts accepting events after the editor initializes.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/editor/src/plugins/calendar-invite/plugin.tsx, line 19:
<comment>This guard captures the initial editor value, so the listener never starts accepting events after the editor initializes.</comment>
<file context>
@@ -16,6 +16,7 @@ export function CalendarInvitePlugin({
useEffect(() => {
const sub = editorEventBus.on('calendar-invite:open', (payload) => {
+ if (payload.editorRef !== editor) return;
setPendingRange(payload.range);
setIsOpen(true);
</file context>
Tip: Review your code locally with the cubic CLI to iterate faster.
| const m = snapped.getMinutes(); | ||
| // If still on today and within TIME_SLOTS range (06:00–23:00), use it | ||
| if (snapped.getDate() === now.getDate() && h >= 6 && h <= 23) { | ||
| return `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}`; |
There was a problem hiding this comment.
P2: Default start time can be initialized to 23:30, but that value is not in TIME_SLOTS, causing an inconsistent/invalid initial selection near late evening.
Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At packages/editor/src/plugins/calendar-invite/modal.tsx, line 107:
<comment>Default start time can be initialized to `23:30`, but that value is not in `TIME_SLOTS`, causing an inconsistent/invalid initial selection near late evening.</comment>
<file context>
@@ -96,10 +96,17 @@ function buildCalendarGrid(month: number, year: number): (number | null)[][] {
+ const m = snapped.getMinutes();
+ // If still on today and within TIME_SLOTS range (06:00–23:00), use it
+ if (snapped.getDate() === now.getDate() && h >= 6 && h <= 23) {
+ return `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}`;
+ }
+ return '09:00';
</file context>
| return `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}`; | |
| return TIME_SLOTS.includes(`${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}`) | |
| ? `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}` | |
| : '09:00'; |
I spoke to a few people at an event recently that asked for the ability to send calendar invites via Resend. I thought that would be a cool add to the open source editor, too. This is my vibe coded proof-of-concept version of it.
Adds a /calendar-invite slash command that opens a modal for configuring a calendar event.
Inserts a card with provider buttons (Google, Outlook, Apple, Yahoo) that link directly to add-to-calendar URLs. Apple Calendar uses a data:text/calendar URI so no file hosting is needed.
Supports multiple invites per email, custom card styling via CalendarPluginOptions, and renders correctly to React Email HTML output.
Testing
Run the app, then visit http://localhost:3000/editor/standalone-editor
Type /calendar and select the calendar invite option
Set the details for the event and it should insert an invite
(I haven't verified this yet, but Claude assures me that the calendar invite buttons do work in the generated HTML of the email)
Summary by cubic
Adds a
/calendar-inviteslash command that opens a modal to create an event and inserts an “Add to calendar” card with Google, Outlook, Apple, and Yahoo links. The card renders in React Email and can be customized viaEmailEditorprops, with fixes for all‑day/multi‑day date math, editor scoping, and smarter defaults.New Features
data:text/calendarURI (no file hosting).CalendarInvitenode with React Email rendering and URL/ICS generation.calendarInviteprop (defaultTimezone,cardBg,accentColor,borderColor).'calendar-invite:open'for launching the modal.CalendarInvite,CalendarInvitePlugin,calendarInviteSlashCommand,detectCalendarTimezone(),CALENDAR_TIMEZONES, and types.Bug Fixes
'calendar-invite:open'to the originating editor instance to prevent cross-triggering.CalendarInviteis present to avoid crashes with custom extensions.defaultTimezonewithIntl.DateTimeFormatand include unknown but valid zones in the select.UIDwithcrypto.randomBytesto avoid insecure randomness.Written for commit e14bfae. Summary will update on new commits.