Skip to content

feat(editor): add calendar invite slash command plugin#3490

Draft
christina-de-martinez wants to merge 4 commits into
canaryfrom
feat/calendar-invite
Draft

feat(editor): add calendar invite slash command plugin#3490
christina-de-martinez wants to merge 4 commits into
canaryfrom
feat/calendar-invite

Conversation

@christina-de-martinez
Copy link
Copy Markdown
Collaborator

@christina-de-martinez christina-de-martinez commented May 12, 2026

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-invite slash 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 via EmailEditor props, with fixes for all‑day/multi‑day date math, editor scoping, and smarter defaults.

  • New Features

    • Slash command with modal: date picker, start time, duration (incl. “All day”), timezone, location, notes.
    • Inserts a calendar invite card with provider buttons; Apple uses a data:text/calendar URI (no file hosting).
    • Multiple invites per email; draggable atom node with an in-editor preview card.
    • New CalendarInvite node with React Email rendering and URL/ICS generation.
    • Enabled by default; customize via calendarInvite prop (defaultTimezone, cardBg, accentColor, borderColor).
    • Event bus: 'calendar-invite:open' for launching the modal.
    • Exports: CalendarInvite, CalendarInvitePlugin, calendarInviteSlashCommand, detectCalendarTimezone(), CALENDAR_TIMEZONES, and types.
  • Bug Fixes

    • Correct all-day next-day and multi-day end dates using UTC arithmetic to avoid timezone shifts in iCal.
    • Scope 'calendar-invite:open' to the originating editor instance to prevent cross-triggering.
    • Register the slash command and plugin only when CalendarInvite is present to avoid crashes with custom extensions.
    • Compute the default start time via epoch-based rounding to handle day rollover; validate defaultTimezone with Intl.DateTimeFormat and include unknown but valid zones in the select.
    • Generate ICS UID with crypto.randomBytes to avoid insecure randomness.

Written for commit e14bfae. Summary will update on new commits.

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]>
@vercel
Copy link
Copy Markdown
Contributor

vercel Bot commented May 12, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
react-email Ready Ready Preview, Comment May 12, 2026 8:11pm
react-email-demo Ready Ready Preview, Comment May 12, 2026 8:11pm

Request Review

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 12, 2026

⚠️ No Changeset found

Latest commit: e14bfae

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented May 12, 2026

Open in StackBlitz

npm i https://pkg.pr.new/@react-email/editor@3490

commit: e14bfae

Comment thread packages/editor/src/plugins/calendar-invite/ical-generator.ts Fixed
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.ts where 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 global window event for CalendarInvitePlugin can cause one editor instance to open modals in other editors, and always exposing the slash command can crash when CalendarInvite is omitted from custom extensions.
  • Additional correctness issues in packages/editor/src/plugins/calendar-invite/types.ts and packages/editor/src/plugins/calendar-invite/modal.tsx can 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.

Comment thread packages/editor/src/plugins/calendar-invite/ical-generator.ts Outdated
Comment thread packages/editor/src/plugins/calendar-invite/ical-generator.ts Outdated
<BubbleMenu.ImageDefault />
<SlashCommandRoot />
<SlashCommandRoot items={slashCommands} />
<CalendarInvitePlugin {...(calendarInvite ?? {})} />
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 = [
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>

Comment thread packages/editor/src/email-editor/email-editor.tsx
Comment thread packages/editor/src/plugins/calendar-invite/modal.tsx Outdated
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

0 issues found across 1 file (changes from recent commits).

Requires human review: Auto-approval blocked by 7 unresolved issues from previous reviews.

- 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]>
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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')}`;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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>
Suggested change
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';

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants