From fae2c4fc24d1779125b21d4e786ff416eefb8991 Mon Sep 17 00:00:00 2001 From: Steve Larson <9larsons@gmail.com> Date: Tue, 13 Jan 2026 14:55:36 -0600 Subject: [PATCH 1/3] Added slash menu and cards to email editor ref https://linear.app/ghost/issue/NY-914/ --- packages/koenig-lexical/demo/DemoApp.jsx | 13 +++- .../koenig-lexical/src/nodes/EmailNodes.js | 8 +- .../test/e2e/editors/email-editor.test.js | 78 +++++++++++++++++-- 3 files changed, 89 insertions(+), 10 deletions(-) diff --git a/packages/koenig-lexical/demo/DemoApp.jsx b/packages/koenig-lexical/demo/DemoApp.jsx index 382289fd0..176142fc6 100644 --- a/packages/koenig-lexical/demo/DemoApp.jsx +++ b/packages/koenig-lexical/demo/DemoApp.jsx @@ -5,7 +5,6 @@ import FloatingButton from './components/FloatingButton'; import InitialContentToggle from './components/InitialContentToggle'; import LockIcon from './assets/icons/kg-lock.svg?react'; import React, {useState} from 'react'; -import ReplacementStringsPlugin from '../src/plugins/ReplacementStringsPlugin'; import Sidebar from './components/Sidebar'; import TitleTextBox from './components/TitleTextBox'; import Watermark from './components/Watermark'; @@ -20,6 +19,13 @@ import { KoenigComposableEditor, KoenigComposer, KoenigEditor, ListPlugin, MINIMAL_NODES, MINIMAL_TRANSFORMERS, RestrictContentPlugin, TKCountPlugin, WordCountPlugin } from '../src'; +// These plugins must be imported AFTER '../src' to avoid circular dependency issues +import CalloutPlugin from '../src/plugins/CalloutPlugin'; +import HorizontalRulePlugin from '../src/plugins/HorizontalRulePlugin'; +import ImagePlugin from '../src/plugins/ImagePlugin'; +import ReplacementStringsPlugin from '../src/plugins/ReplacementStringsPlugin'; +import SlashCardMenuPlugin from '../src/plugins/SlashCardMenuPlugin'; +import {ButtonPlugin} from '../src/plugins/ButtonPlugin'; import {defaultHeaders as defaultUnsplashHeaders} from './utils/unsplashConfig'; import {fetchEmbed} from './utils/fetchEmbed'; import {fileTypes, useFileUpload} from './utils/useFileUpload'; @@ -169,6 +175,11 @@ function DemoEditor({editorType, registerAPI, cursorDidExitAtTop, darkMode, setW + + + + + ); } diff --git a/packages/koenig-lexical/src/nodes/EmailNodes.js b/packages/koenig-lexical/src/nodes/EmailNodes.js index 1f1d0126b..f665d7c3d 100644 --- a/packages/koenig-lexical/src/nodes/EmailNodes.js +++ b/packages/koenig-lexical/src/nodes/EmailNodes.js @@ -10,7 +10,10 @@ import {LinkNode} from '@lexical/link'; import {ListItemNode, ListNode} from '@lexical/list'; import {AsideNode} from './AsideNode'; +import {ButtonNode} from './ButtonNode'; +import {CalloutNode} from './CalloutNode'; import {HorizontalRuleNode} from './HorizontalRuleNode'; +import {ImageNode} from './ImageNode'; const EMAIL_NODES = [ ExtendedTextNode, @@ -23,7 +26,10 @@ const EMAIL_NODES = [ ListNode, ListItemNode, LinkNode, - HorizontalRuleNode + ButtonNode, + CalloutNode, + HorizontalRuleNode, + ImageNode ]; export default EMAIL_NODES; diff --git a/packages/koenig-lexical/test/e2e/editors/email-editor.test.js b/packages/koenig-lexical/test/e2e/editors/email-editor.test.js index c7b233ce0..1bf0dbf37 100644 --- a/packages/koenig-lexical/test/e2e/editors/email-editor.test.js +++ b/packages/koenig-lexical/test/e2e/editors/email-editor.test.js @@ -93,7 +93,7 @@ test.describe('Koenig Editor with email template nodes', async function () { test('can create horizontal rules with --- shortcut', async function () { await focusEditor(page); - await page.keyboard.type('--- '); + await page.keyboard.type('---'); await assertHTML(page, html`
@@ -127,6 +127,75 @@ test.describe('Koenig Editor with email template nodes', async function () { }); }); + test.describe('Slash menu', function () { + test('opens on / keystroke', async function () { + await focusEditor(page); + await expect(page.locator('[data-kg-slash-menu]')).toHaveCount(0); + await page.keyboard.type('/'); + await expect(page.locator('[data-kg-slash-menu]')).toBeVisible(); + }); + + test('shows button card option', async function () { + await focusEditor(page); + await page.keyboard.type('/button'); + await expect(page.locator('[data-kg-slash-menu]')).toBeVisible(); + await expect(page.locator('[data-kg-cardmenu-idx]', {hasText: 'Button'})).toBeVisible(); + }); + + test('shows divider option', async function () { + await focusEditor(page); + await page.keyboard.type('/hr'); + await expect(page.locator('[data-kg-slash-menu]')).toBeVisible(); + await expect(page.locator('[data-kg-cardmenu-idx]', {hasText: 'Divider'})).toBeVisible(); + }); + + test('shows image option', async function () { + await focusEditor(page); + await page.keyboard.type('/image'); + await expect(page.locator('[data-kg-slash-menu]')).toBeVisible(); + await expect(page.locator('[data-kg-cardmenu-idx]', {hasText: 'Image'})).toBeVisible(); + }); + + test('shows callout option', async function () { + await focusEditor(page); + await page.keyboard.type('/callout'); + await expect(page.locator('[data-kg-slash-menu]')).toBeVisible(); + await expect(page.locator('[data-kg-cardmenu-idx]', {hasText: 'Callout'})).toBeVisible(); + }); + + test('can insert button card', async function () { + await focusEditor(page); + await page.keyboard.type('/button'); + await page.keyboard.press('Enter'); + + await expect(page.locator('[data-kg-card="button"]')).toBeVisible(); + }); + + test('can insert divider card', async function () { + await focusEditor(page); + await page.keyboard.type('/hr'); + await page.keyboard.press('Enter'); + + await expect(page.locator('[data-kg-card="horizontalrule"]')).toBeVisible(); + }); + + test('can insert image card', async function () { + await focusEditor(page); + await page.keyboard.type('/image'); + await page.keyboard.press('Enter'); + + await expect(page.locator('[data-kg-card="image"]')).toBeVisible(); + }); + + test('can insert callout card', async function () { + await focusEditor(page); + await page.keyboard.type('/callout'); + await page.keyboard.press('Enter'); + + await expect(page.locator('[data-kg-card="callout"]')).toBeVisible(); + }); + }); + test.describe('Unsupported features', function () { test('code block shortcut does NOT create code block', async function () { await focusEditor(page); @@ -138,13 +207,6 @@ test.describe('Koenig Editor with email template nodes', async function () { `); }); - test('slash menu is not available', async function () { - await focusEditor(page); - await expect(page.locator('[data-kg-slash-menu]')).toHaveCount(0); - await page.keyboard.type('/'); - await expect(page.locator('[data-kg-slash-menu]')).toHaveCount(0); - }); - test('plus button is not shown', async function () { await focusEditor(page); await expect(page.locator('[data-kg-plus-button]')).toHaveCount(0); From bb7b9b856746a4c22b55f58c74e6412288db94a6 Mon Sep 17 00:00:00 2001 From: Steve Larson <9larsons@gmail.com> Date: Tue, 13 Jan 2026 15:07:33 -0600 Subject: [PATCH 2/3] Updated email editor to a component --- packages/koenig-lexical/demo/DemoApp.jsx | 27 ++++---------- .../src/components/KoenigEmailEditor.jsx | 35 +++++++++++++++++++ packages/koenig-lexical/src/index.js | 2 ++ 3 files changed, 43 insertions(+), 21 deletions(-) create mode 100644 packages/koenig-lexical/src/components/KoenigEmailEditor.jsx diff --git a/packages/koenig-lexical/demo/DemoApp.jsx b/packages/koenig-lexical/demo/DemoApp.jsx index 176142fc6..fec670ecb 100644 --- a/packages/koenig-lexical/demo/DemoApp.jsx +++ b/packages/koenig-lexical/demo/DemoApp.jsx @@ -15,17 +15,11 @@ import emailContent from './content/email-content.json'; import minimalContent from './content/minimal-content.json'; import {$getRoot, $isDecoratorNode} from 'lexical'; import { - BASIC_NODES, BASIC_TRANSFORMERS, EMAIL_NODES, EMAIL_TRANSFORMERS, - KoenigComposableEditor, KoenigComposer, KoenigEditor, ListPlugin, MINIMAL_NODES, - MINIMAL_TRANSFORMERS, RestrictContentPlugin, TKCountPlugin, WordCountPlugin + BASIC_NODES, BASIC_TRANSFORMERS, EMAIL_NODES, + KoenigComposableEditor, KoenigComposer, KoenigEditor, KoenigEmailEditor, + MINIMAL_NODES, MINIMAL_TRANSFORMERS, RestrictContentPlugin, + TKCountPlugin, WordCountPlugin } from '../src'; -// These plugins must be imported AFTER '../src' to avoid circular dependency issues -import CalloutPlugin from '../src/plugins/CalloutPlugin'; -import HorizontalRulePlugin from '../src/plugins/HorizontalRulePlugin'; -import ImagePlugin from '../src/plugins/ImagePlugin'; -import ReplacementStringsPlugin from '../src/plugins/ReplacementStringsPlugin'; -import SlashCardMenuPlugin from '../src/plugins/SlashCardMenuPlugin'; -import {ButtonPlugin} from '../src/plugins/ButtonPlugin'; import {defaultHeaders as defaultUnsplashHeaders} from './utils/unsplashConfig'; import {fetchEmbed} from './utils/fetchEmbed'; import {fileTypes, useFileUpload} from './utils/useFileUpload'; @@ -165,22 +159,13 @@ function DemoEditor({editorType, registerAPI, cursorDidExitAtTop, darkMode, setW ); } else if (editorType === 'email') { return ( - - - - - - - - - + ); } diff --git a/packages/koenig-lexical/src/components/KoenigEmailEditor.jsx b/packages/koenig-lexical/src/components/KoenigEmailEditor.jsx new file mode 100644 index 000000000..3ce9d3d1e --- /dev/null +++ b/packages/koenig-lexical/src/components/KoenigEmailEditor.jsx @@ -0,0 +1,35 @@ +import '../styles/index.css'; +import HorizontalRulePlugin from '../plugins/HorizontalRulePlugin'; +import ImagePlugin from '../plugins/ImagePlugin'; +import KoenigComposableEditor from './KoenigComposableEditor'; +import React from 'react'; +import ReplacementStringsPlugin from '../plugins/ReplacementStringsPlugin'; +import SlashCardMenuPlugin from '../plugins/SlashCardMenuPlugin'; +import {ButtonPlugin} from '../plugins/ButtonPlugin'; +import {CalloutPlugin} from '../plugins/CalloutPlugin'; +import {EMAIL_TRANSFORMERS} from '../plugins/MarkdownShortcutPlugin'; +import {ListPlugin} from '@lexical/react/LexicalListPlugin'; + +const KoenigEmailEditor = ({ + children, + ...props +}) => { + return ( + + + + + + + + + {children} + + ); +}; + +export default KoenigEmailEditor; diff --git a/packages/koenig-lexical/src/index.js b/packages/koenig-lexical/src/index.js index f5ee8ec65..a30fec012 100644 --- a/packages/koenig-lexical/src/index.js +++ b/packages/koenig-lexical/src/index.js @@ -4,6 +4,7 @@ import KoenigCardWrapper from './components/KoenigCardWrapper'; import KoenigComposableEditor from './components/KoenigComposableEditor'; import KoenigComposer from './components/KoenigComposer'; import KoenigEditor from './components/KoenigEditor'; +import KoenigEmailEditor from './components/KoenigEmailEditor'; import KoenigNestedComposer from './components/KoenigNestedComposer'; /* Plugins */ @@ -64,6 +65,7 @@ export { KoenigComposableEditor, KoenigComposer, KoenigEditor, + KoenigEmailEditor, KoenigNestedComposer, KoenigCardWrapper, From 0f0b138d84e1b41d16c5e16538ba3994e6e6c3db Mon Sep 17 00:00:00 2001 From: Chris Raible Date: Wed, 14 Jan 2026 14:03:32 -0800 Subject: [PATCH 3/3] Removed Slash menu and image nodes --- .../src/components/KoenigEmailEditor.jsx | 4 - .../koenig-lexical/src/nodes/EmailNodes.js | 4 +- .../test/e2e/editors/email-editor.test.js | 76 ++----------------- 3 files changed, 8 insertions(+), 76 deletions(-) diff --git a/packages/koenig-lexical/src/components/KoenigEmailEditor.jsx b/packages/koenig-lexical/src/components/KoenigEmailEditor.jsx index 3ce9d3d1e..a3eeaffe0 100644 --- a/packages/koenig-lexical/src/components/KoenigEmailEditor.jsx +++ b/packages/koenig-lexical/src/components/KoenigEmailEditor.jsx @@ -1,10 +1,8 @@ import '../styles/index.css'; import HorizontalRulePlugin from '../plugins/HorizontalRulePlugin'; -import ImagePlugin from '../plugins/ImagePlugin'; import KoenigComposableEditor from './KoenigComposableEditor'; import React from 'react'; import ReplacementStringsPlugin from '../plugins/ReplacementStringsPlugin'; -import SlashCardMenuPlugin from '../plugins/SlashCardMenuPlugin'; import {ButtonPlugin} from '../plugins/ButtonPlugin'; import {CalloutPlugin} from '../plugins/CalloutPlugin'; import {EMAIL_TRANSFORMERS} from '../plugins/MarkdownShortcutPlugin'; @@ -22,11 +20,9 @@ const KoenigEmailEditor = ({ > - - {children} ); diff --git a/packages/koenig-lexical/src/nodes/EmailNodes.js b/packages/koenig-lexical/src/nodes/EmailNodes.js index f665d7c3d..e9f7d6708 100644 --- a/packages/koenig-lexical/src/nodes/EmailNodes.js +++ b/packages/koenig-lexical/src/nodes/EmailNodes.js @@ -13,7 +13,6 @@ import {AsideNode} from './AsideNode'; import {ButtonNode} from './ButtonNode'; import {CalloutNode} from './CalloutNode'; import {HorizontalRuleNode} from './HorizontalRuleNode'; -import {ImageNode} from './ImageNode'; const EMAIL_NODES = [ ExtendedTextNode, @@ -28,8 +27,7 @@ const EMAIL_NODES = [ LinkNode, ButtonNode, CalloutNode, - HorizontalRuleNode, - ImageNode + HorizontalRuleNode ]; export default EMAIL_NODES; diff --git a/packages/koenig-lexical/test/e2e/editors/email-editor.test.js b/packages/koenig-lexical/test/e2e/editors/email-editor.test.js index 1bf0dbf37..c0e811989 100644 --- a/packages/koenig-lexical/test/e2e/editors/email-editor.test.js +++ b/packages/koenig-lexical/test/e2e/editors/email-editor.test.js @@ -127,75 +127,6 @@ test.describe('Koenig Editor with email template nodes', async function () { }); }); - test.describe('Slash menu', function () { - test('opens on / keystroke', async function () { - await focusEditor(page); - await expect(page.locator('[data-kg-slash-menu]')).toHaveCount(0); - await page.keyboard.type('/'); - await expect(page.locator('[data-kg-slash-menu]')).toBeVisible(); - }); - - test('shows button card option', async function () { - await focusEditor(page); - await page.keyboard.type('/button'); - await expect(page.locator('[data-kg-slash-menu]')).toBeVisible(); - await expect(page.locator('[data-kg-cardmenu-idx]', {hasText: 'Button'})).toBeVisible(); - }); - - test('shows divider option', async function () { - await focusEditor(page); - await page.keyboard.type('/hr'); - await expect(page.locator('[data-kg-slash-menu]')).toBeVisible(); - await expect(page.locator('[data-kg-cardmenu-idx]', {hasText: 'Divider'})).toBeVisible(); - }); - - test('shows image option', async function () { - await focusEditor(page); - await page.keyboard.type('/image'); - await expect(page.locator('[data-kg-slash-menu]')).toBeVisible(); - await expect(page.locator('[data-kg-cardmenu-idx]', {hasText: 'Image'})).toBeVisible(); - }); - - test('shows callout option', async function () { - await focusEditor(page); - await page.keyboard.type('/callout'); - await expect(page.locator('[data-kg-slash-menu]')).toBeVisible(); - await expect(page.locator('[data-kg-cardmenu-idx]', {hasText: 'Callout'})).toBeVisible(); - }); - - test('can insert button card', async function () { - await focusEditor(page); - await page.keyboard.type('/button'); - await page.keyboard.press('Enter'); - - await expect(page.locator('[data-kg-card="button"]')).toBeVisible(); - }); - - test('can insert divider card', async function () { - await focusEditor(page); - await page.keyboard.type('/hr'); - await page.keyboard.press('Enter'); - - await expect(page.locator('[data-kg-card="horizontalrule"]')).toBeVisible(); - }); - - test('can insert image card', async function () { - await focusEditor(page); - await page.keyboard.type('/image'); - await page.keyboard.press('Enter'); - - await expect(page.locator('[data-kg-card="image"]')).toBeVisible(); - }); - - test('can insert callout card', async function () { - await focusEditor(page); - await page.keyboard.type('/callout'); - await page.keyboard.press('Enter'); - - await expect(page.locator('[data-kg-card="callout"]')).toBeVisible(); - }); - }); - test.describe('Unsupported features', function () { test('code block shortcut does NOT create code block', async function () { await focusEditor(page); @@ -207,6 +138,13 @@ test.describe('Koenig Editor with email template nodes', async function () { `); }); + test('slash menu is not available', async function () { + await focusEditor(page); + await expect(page.locator('[data-kg-slash-menu]')).toHaveCount(0); + await page.keyboard.type('/'); + await expect(page.locator('[data-kg-slash-menu]')).toHaveCount(0); + }); + test('plus button is not shown', async function () { await focusEditor(page); await expect(page.locator('[data-kg-plus-button]')).toHaveCount(0);