Conversation
WalkthroughThe PR converts large parts of the koenig-lexical package and demo to stricter TypeScript: adding interfaces and prop typings across components, hooks, nodes, plugins, and contexts; replacing runtime PropTypes with compile-time types; introducing null-safety guards and casts; and tightening event/ref signatures. It adds new/rewrites modules (TypeScript CodeBlockCard, UnsplashModal, TransistorCard, many UI components), removes some JS/PropTypes files, and updates Storybook stories with ts-nocheck. Other notable changes include ESLint config updates, package.json script/dependency edits, Playwright config tweaks, and DragDropReorder ghost rendering switched to renderToString. Estimated code review effort🎯 5 (Critical) | ⏱️ ~120 minutes ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
|
d5269fd to
9d91044
Compare
4e6b80d to
5516184
Compare
5516184 to
c91da8a
Compare
There was a problem hiding this comment.
Actionable comments posted: 5
Note
Due to the large number of review comments, Critical severity comments were prioritized as inline comments.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (23)
packages/koenig-lexical/demo/components/Navigator.tsx (1)
9-16:⚠️ Potential issue | 🟠 MajorWrap the side effect in
useEffectand add cleanup.The global window mutation on line 13 runs in the component body, which executes on every render. React side effects should be placed in
useEffectto:
- Run only when dependencies change (mount/navigate updates)
- Enable proper cleanup on unmount to avoid stale references
🔧 Proposed fix using useEffect with cleanup
+import {useEffect} from 'react'; import {useNavigate} from 'react-router-dom'; declare global { interface Window { navigate: ReturnType<typeof useNavigate>; } } const Navigator = () => { const navigate = useNavigate(); - // Hack, used to allow Playwright to navigate without triggering a full page reload. - window.navigate = navigate; + useEffect(() => { + // Hack, used to allow Playwright to navigate without triggering a full page reload. + window.navigate = navigate; + + return () => { + delete window.navigate; + }; + }, [navigate]); return null; }; export default Navigator;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/koenig-lexical/demo/components/Navigator.tsx` around lines 9 - 16, The Navigator component assigns window.navigate in the render body which runs every render; move that side effect into a useEffect inside the Navigator function so the assignment runs only when the navigate function changes (use [navigate] as dependency), perform a safety check for window (typeof window !== 'undefined') and add cleanup in the effect to remove the global (either delete window.navigate or set it to undefined) on unmount to avoid stale references.packages/koenig-lexical/src/hooks/usePinturaEditor.ts (1)
166-169:⚠️ Potential issue | 🔴 CriticalEvent listener not properly removed due to mismatched
captureoption.The listener is added with
{capture: true}at line 166, but removed without it at line 169. Thecaptureflag must match forremoveEventListenerto find and remove the correct listener. This causes a memory leak where handlers accumulate on each component remount.🐛 Proposed fix
return () => { - window.removeEventListener('click', handleCloseClick); + window.removeEventListener('click', handleCloseClick, {capture: true}); };🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/koenig-lexical/src/hooks/usePinturaEditor.ts` around lines 166 - 169, The event listener added in usePinturaEditor uses window.addEventListener('click', handleCloseClick, {capture: true}) but is removed without the same options, so removeEventListener won't match; update the cleanup in the usePinturaEditor hook to call window.removeEventListener('click', handleCloseClick, {capture: true}) (matching the addEventListener options) so the handleCloseClick listener is correctly removed on unmount/remount.packages/koenig-lexical/src/components/ui/TenorSelector.tsx (1)
74-80:⚠️ Potential issue | 🟠 Major
handleGifHighlightreferenced before definition causes stale closure.The
useEffecton lines 74-80 referenceshandleGifHighlightin its dependency array, buthandleGifHighlightis defined later (line 225) and recreated on every render. This can cause the event listener to reference stale state.Wrap handleGifHighlight in useCallback or move before the effect
+ const handleGifHighlight = React.useCallback((event: KeyboardEvent) => { + switch (event.key) { + case 'Tab': + return handleTab(event); + // ... rest of cases + } + }, [highlightedGif, gifs, columns]); + useEffect(() => { document.addEventListener('keydown', handleGifHighlight); return () => { document.removeEventListener('keydown', handleGifHighlight); }; }, [handleGifHighlight]);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/koenig-lexical/src/components/ui/TenorSelector.tsx` around lines 74 - 80, The effect that adds/removes the keydown listener references handleGifHighlight which is defined later and recreated each render; make handleGifHighlight stable by wrapping it in useCallback (e.g., useCallback(handleGifHighlight, [/* include any state/props it uses */])) or move its definition above the useEffect so the same function instance is used for add/remove, and then ensure the useEffect dependency array contains the stable handleGifHighlight (or the correct dependency list) so the cleanup/unsubscribe works with the exact same function reference.packages/koenig-lexical/src/hooks/useTypeaheadTriggerMatch.ts (1)
15-21:⚠️ Potential issue | 🟠 MajorEscape
triggerbefore interpolating into regex character classes.
Lines 15 and 17 concatenatetriggerdirectly into regex character classes without escaping. Special characters like],^,\,-change regex semantics or create invalid patterns. For example, a trigger value of]would produce invalid syntax, and^would negate the character class instead of matching the literal character.Proposed patch
+const escapeForCharClass = (value: string) => value.replace(/[\\\]\[-^]/g, '\\$&'); + export default function useBasicTypeaheadTriggerMatch(trigger: string, {minLength = 1, maxLength = 75}: {minLength?: number; maxLength?: number}) { return useCallback( (text: string): TriggerMatch | null => { - const invalidChars = '[^' + trigger + '\\s]'; // escaped set - these cannot be present in the matched string + const safeTrigger = escapeForCharClass(trigger); + const invalidChars = `[^${safeTrigger}\\s]`; // escaped set - these cannot be present in the matched string const TypeaheadTriggerRegex = new RegExp( - '[' + trigger + ']' + + '[' + safeTrigger + ']' + '(' + '(?:' + invalidChars + ')' + '{0,' + maxLength + '}' + ')$', );🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/koenig-lexical/src/hooks/useTypeaheadTriggerMatch.ts` around lines 15 - 21, The code builds character classes using the raw trigger string (see invalidChars and TypeaheadTriggerRegex) which breaks for metacharacters like ], ^, -, \; fix by escaping regex-special characters in trigger before interpolating it into the character-class expressions (create an escapeRegExp helper or reuse one, produce escapedTrigger = escapeRegExp(trigger)), then use escapedTrigger when constructing invalidChars and the TypeaheadTriggerRegex so the trigger is always treated as a literal character in the regex.packages/koenig-lexical/src/components/ui/cards/MarkdownCard/MarkdownEditor.tsx (1)
163-175:⚠️ Potential issue | 🟡 MinorNon-null assertions throughout assume editor is always initialized.
Functions like
addShortcuts,toggleSpellcheck,toggleButtonClass,closeHelpDialog, andcloseUnsplashDialogall usemarkdownEditor.current!. If any of these are called after unmount (e.g., during async cleanup or event handler race conditions), they will throw.Consider adding a guard at the top of each function, or creating a helper that safely accesses the editor:
🛡️ Example helper approach
function getEditor(): SimpleMDEInstance | null { return markdownEditor.current; } function addShortcuts() { const editor = getEditor(); if (!editor) return; const codemirror = editor.codemirror; // ... }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/koenig-lexical/src/components/ui/cards/MarkdownCard/MarkdownEditor.tsx` around lines 163 - 175, Several functions (addShortcuts, toggleSpellcheck, toggleButtonClass, closeHelpDialog, closeUnsplashDialog) directly use markdownEditor.current! and can throw if the editor is unmounted; create a small safe accessor (e.g., getEditor() that returns markdownEditor.current or null) and at the top of each affected function use const editor = getEditor(); if (!editor) return; then use editor.codemirror instead of markdownEditor.current! and update calls like codemirror.setOption('extraKeys', ...) to operate only when editor is present so no non-null assertions remain.packages/koenig-lexical/src/components/ui/cards/MarkdownCard/useMarkdownImageUploader.ts (1)
53-88:⚠️ Potential issue | 🟡 MinorNon-null assertions on
editor.currentmay cause runtime errors.Lines 54 and 83 use
editor.current!to assert the editor is non-null, butSimpleMDEEditorRefexplicitly typescurrentas{ ... } | null. IfinsertImagesis called before the editor initializes or after it unmounts, this will throw a runtime exception.Consider adding an early return guard or using optional chaining with appropriate fallback behavior:
🛡️ Proposed guard
function insertImages(urls: (string | UploadResult)[] = []) { + if (!editor.current) { + return; + } - const codemirror = editor.current!.codemirror; + const codemirror = editor.current.codemirror; // ... rest of function - editor.current!.codemirror.focus(); + editor.current.codemirror.focus();🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/koenig-lexical/src/components/ui/cards/MarkdownCard/useMarkdownImageUploader.ts` around lines 53 - 88, The insertImages function uses non-null assertions on editor.current (SimpleMDEEditorRef) and then calls editor.current.codemirror.focus() and codemirror.replaceSelection(...), which can throw if the ref is null; update insertImages to early-return when editor.current is null (e.g., const ed = editor.current; if (!ed) return) and then use the local ed.codemirror safely (or optional chaining) before calling focus() and replaceSelection to avoid runtime exceptions when the editor is not initialized or has unmounted.packages/koenig-lexical/src/components/ui/Modal.tsx (1)
12-17:⚠️ Potential issue | 🟠 MajorMove
preventDefault()andstopPropagation()inside the Escape key conditionCalling
preventDefault()on all keydown events (lines 13–14) blocks keyboard interaction with any interactive content inside the modal. Restrict these calls to Escape key handling only.Proposed fix
const controlByKeys = (event: React.KeyboardEvent) => { - event.stopPropagation(); - event.preventDefault(); - if (event.key === 'Escape') { + event.stopPropagation(); + event.preventDefault(); onClose(); } };🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/koenig-lexical/src/components/ui/Modal.tsx` around lines 12 - 17, In controlByKeys, stop calling event.preventDefault() and event.stopPropagation() for every key; instead, detect if event.key === 'Escape' and then call event.preventDefault() and event.stopPropagation() immediately before invoking onClose(); leave all other keys untouched so interactive content inside the modal can receive normal keyboard events. Ensure the change is made inside the controlByKeys function and keeps the onClose() call tied to the Escape branch.packages/koenig-lexical/src/components/ui/Button.tsx (1)
49-49:⚠️ Potential issue | 🟠 MajorLogic bug:
width === 'full'never appliesw-fullclass.The expression
width === 'regular' || 'w-full'is always truthy:
- When
width === 'regular': evaluates totrue- When
width === 'full': evaluates to'w-full'(truthy string)This means the
w-fullclass is always included, regardless of thewidthprop value. The intended logic appears to be applyingw-fullonly whenwidth === 'full'.Proposed fix
className={clsx( 'not-kg-prose inline-block cursor-pointer text-center font-sans font-medium', (!shrink && 'shrink-0'), - width === 'regular' || 'w-full', + width === 'full' && 'w-full', rounded && 'rounded-md',🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/koenig-lexical/src/components/ui/Button.tsx` at line 49, The class expression in Button.tsx mistakenly uses "width === 'regular' || 'w-full'", which always evaluates truthy and thus always adds "w-full"; update the conditional so "w-full" is only added when the width prop equals 'full' (e.g., replace that part with a conditional that checks width === 'full' before adding 'w-full'). Locate the Button component and the className/array where the width variable is used and change the faulty expression to a proper conditional that only includes 'w-full' when width === 'full'.packages/koenig-lexical/src/plugins/HeaderPlugin.tsx (1)
11-28:⚠️ Potential issue | 🟠 MajorAdd
useEffectdependency array to prevent re-registering command on every render.The
useEffectat lines 11-28 is missing a dependency array, causing it to re-run on every render and repeatedly register/unregister the command handler. Add[editor]as the dependency array to match the pattern used in other similar plugins.💡 Proposed fix
- }); + }, [editor]);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/koenig-lexical/src/plugins/HeaderPlugin.tsx` around lines 11 - 28, The useEffect in HeaderPlugin (the effect that checks editor.hasNodes(HeaderNode) and registers INSERT_HEADER_COMMAND via editor.registerCommand and mergeRegister) is missing a dependency array and therefore re-registers on every render; update that useEffect to include [editor] as its dependency so the registration/unregistration runs only when the editor instance changes, retaining the existing logic that creates the card node via $createHeaderNode and dispatches INSERT_CARD_COMMAND with COMMAND_PRIORITY_LOW.packages/koenig-lexical/src/plugins/CalloutPlugin.tsx (1)
11-28:⚠️ Potential issue | 🟠 Major
useEffectis missing the[editor]dependency array.At line 28, the effect closes without a dependency array, causing the command registration to re-run on every render. Comparable plugin files (EmbedPlugin, HtmlPlugin) include
[editor]as a dependency since the effect uses the editor object.Proposed fix
- }); + }, [editor]);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/koenig-lexical/src/plugins/CalloutPlugin.tsx` around lines 11 - 28, The useEffect in CalloutPlugin is missing its dependency array which causes the command registration to run on every render; update the React.useEffect call that checks editor.hasNodes([CalloutNode]) and registers the INSERT_CALLOUT_COMMAND to include [editor] as its dependency so the effect only re-runs when the editor instance changes, ensuring proper lifecycle for editor.registerCommand and avoiding duplicate registrations for INSERT_CALLOUT_COMMAND, the $createCalloutNode call, and editor.dispatchCommand.packages/koenig-lexical/src/plugins/FilePlugin.tsx (1)
11-28:⚠️ Potential issue | 🟠 MajorAdd a dependency array to avoid repeated command re-registration.
At line 11,
React.useEffectruns after every render because it has no dependency list, which repeatedly tears down and re-registersINSERT_FILE_COMMAND. Properly implemented plugins like AudioPlugin use[editor]as the dependency array.💡 Proposed fix
- }, [editor]); + }, [editor]); });Change line 28 from
});to}, [editor]);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/koenig-lexical/src/plugins/FilePlugin.tsx` around lines 11 - 28, The useEffect in FilePlugin re-registers INSERT_FILE_COMMAND on every render because it has no dependency array; update the React.useEffect call that checks editor.hasNodes(FileNode) and registers the command (the block using mergeRegister, editor.registerCommand, INSERT_FILE_COMMAND, INSERT_CARD_COMMAND and $createFileNode) to include [editor] as its dependency array so the registration is only created/cleaned when the editor instance changes.packages/koenig-lexical/src/plugins/PaywallPlugin.tsx (1)
11-14:⚠️ Potential issue | 🟠 MajorBug:
hasNodes([])with empty array doesn't validate PaywallNode registration.The check passes an empty array, so it always returns
trueand never logs the error. This should check forPaywallNode.🐛 Proposed fix
+import {$createPaywallNode, INSERT_PAYWALL_COMMAND, PaywallNode} from '../nodes/PaywallNode'; -import {$createPaywallNode, INSERT_PAYWALL_COMMAND} from '../nodes/PaywallNode';useEffect(() => { - if (!editor.hasNodes([])) { + if (!editor.hasNodes([PaywallNode])) { console.error('PaywallPlugin: PaywallNode not registered'); return; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/koenig-lexical/src/plugins/PaywallPlugin.tsx` around lines 11 - 14, The current check in PaywallPlugin uses editor.hasNodes([]) which always succeeds; change the validation to explicitly check for the PaywallNode type (e.g., use editor.hasNodes([PaywallNode]) or the correct API form) and ensure PaywallNode is imported/available, so the console.error('PaywallPlugin: PaywallNode not registered') path is reachable when the node is missing; update the condition in the PaywallPlugin initialization where editor.hasNodes([]) is called.packages/koenig-lexical/src/plugins/KoenigFocusPlugin.tsx (1)
7-16:⚠️ Potential issue | 🔴 CriticalMissing cleanup:
registerCommandreturns an unsubscribe function that must be returned fromuseEffect.The command listener is never unregistered, causing a memory leak. Each time
onFocuschanges, a new listener is added without removing the previous one.🐛 Proposed fix
useEffect(() => { - editor.registerCommand( + return editor.registerCommand( FOCUS_COMMAND, () => { onFocus?.(); return false; }, COMMAND_PRIORITY_EDITOR ); }, [editor, onFocus]);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/koenig-lexical/src/plugins/KoenigFocusPlugin.tsx` around lines 7 - 16, The useEffect registering the FOCUS_COMMAND currently never unsubscribes; capture the unsubscribe returned by editor.registerCommand inside the useEffect and return it as the cleanup function so the listener is removed when onFocus or editor changes. Specifically, update the useEffect that calls editor.registerCommand(FOCUS_COMMAND, ... , COMMAND_PRIORITY_EDITOR) to store the returned disposer and return that disposer (or a function that calls it) so previous registrations are cleaned up when onFocus updates.packages/koenig-lexical/src/plugins/HtmlOutputPlugin.tsx (1)
49-53:⚠️ Potential issue | 🟡 MinorInconsistent optional chaining on
setHtml.Line 50 uses
setHtml?.(htmlString)with optional chaining, but line 52 callssetHtml('')directly. SincesetHtmlis typed as required (setHtml: (html: string) => void), the optional chaining on line 50 is unnecessary.For consistency, either:
- Remove the optional chaining since
setHtmlis required, or- If
setHtmlshould be optional, update the type and add optional chaining on line 52Suggested fix (if setHtml remains required)
if (captionText) { - setHtml?.(htmlString); + setHtml(htmlString); } else { setHtml(''); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/koenig-lexical/src/plugins/HtmlOutputPlugin.tsx` around lines 49 - 53, The conditional uses optional chaining on setHtml (setHtml?.(htmlString)) but later calls setHtml('') directly; since setHtml is declared required (setHtml: (html: string) => void) remove the unnecessary optional chaining and call setHtml(htmlString) for consistency; alternatively, if setHtml should be optional, update its type to (html?: string) => void (or setHtml?: (html: string) => void) and add optional chaining on the empty-string call as well — locate the usage in HtmlOutputPlugin.tsx around the captionText/htmlString handling and update the setHtml calls and signature accordingly.packages/koenig-lexical/src/plugins/FloatingToolbarPlugin.tsx (1)
83-100:⚠️ Potential issue | 🟠 MajorReturn the disposer from
registerCommand.The effect at lines 83-100 registers a command handler but doesn't return the cleanup function from
editor.registerCommand(). This causes handlers to accumulate across remounts, especially problematic under React Strict Mode, which intentionally invokes effects twice during development.💡 Suggested fix
React.useEffect(() => { - editor.registerCommand( + return editor.registerCommand( KEY_MODIFIER_COMMAND, (event: KeyboardEvent) => { const {keyCode, ctrlKey, metaKey, shiftKey} = event; // ctrl/cmd K with selected text should prompt for link insertion if (!shiftKey && keyCode === 75 && (ctrlKey || metaKey)) { const selection = $getSelection(); if ($isRangeSelection(selection) && !selection.isCollapsed()) { setToolbarItemType(toolbarItemTypes.link); event.preventDefault(); return true; } } return false; }, COMMAND_PRIORITY_LOW ); }, [editor]);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/koenig-lexical/src/plugins/FloatingToolbarPlugin.tsx` around lines 83 - 100, The effect registers a command via editor.registerCommand (for KEY_MODIFIER_COMMAND) but never returns the disposer, causing handlers to accumulate; update the React.useEffect to capture the disposer returned by editor.registerCommand and return a cleanup function that calls that disposer (or unregisters the command) so the handler is removed on unmount/re-run—place this around the registration for KEY_MODIFIER_COMMAND and ensure setToolbarItemType usage remains unchanged inside the handler.packages/koenig-lexical/src/components/ui/UrlSearchInput.tsx (1)
51-58:⚠️ Potential issue | 🟡 MinorDon't render a dead “Paste URL as link” action.
Line 57 made the callback optional, but this button still renders for every error state. If a caller omits
handlePasteAsLink, users get a visible action that can never do anything.🩹 Proposed fix
- <button className="mr-3 cursor-pointer" data-testid={`${dataTestId}-error-pasteAsLink`} type="button"><span className="font-semibold underline" onClick={() => handlePasteAsLink?.(value)}>Paste URL as link</span></button> + {handlePasteAsLink && ( + <button + className="mr-3 cursor-pointer" + data-testid={`${dataTestId}-error-pasteAsLink`} + type="button" + onClick={() => handlePasteAsLink(value)} + > + <span className="font-semibold underline">Paste URL as link</span> + </button> + )}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/koenig-lexical/src/components/ui/UrlSearchInput.tsx` around lines 51 - 58, The error UI renders a non-functional "Paste URL as link" button even when the optional handler handlePasteAsLink is not provided; update the UrlSearchInput component so the error-state button (data-testid `${dataTestId}-error-pasteAsLink`) is only rendered when handlePasteAsLink is defined (e.g. guard the button with handlePasteAsLink) and preserve the existing markup/attributes and onClick invocation when present.packages/koenig-lexical/src/components/ui/cards/VideoCard.tsx (1)
168-176:⚠️ Potential issue | 🟡 MinorIncorrect event type cast for form
onChange.Line 168 casts
onFileChange(which expectsReact.ChangeEvent<HTMLInputElement>) toReact.FormEventHandler<HTMLFormElement>. These are different event types - form events bubble from child inputs and haveHTMLFormElementascurrentTarget, notHTMLInputElement.The pattern of using form
onChangeto capture file input changes is unconventional. Consider attachingonChangedirectly to the<input>element instead.🐛 Proposed fix
- <form onChange={onFileChange as unknown as React.FormEventHandler<HTMLFormElement>}> + <form> <input ref={fileInputRef as React.RefObject<HTMLInputElement>} accept={videoMimeTypes.join(',')} hidden={true} name="image-input" type='file' + onChange={onFileChange} /> </form>🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/koenig-lexical/src/components/ui/cards/VideoCard.tsx` around lines 168 - 176, The form is incorrectly using onChange with a wrong event type cast; remove the onChange from the <form> and instead attach onFileChange (typed as React.ChangeEvent<HTMLInputElement>) directly to the <input> (the element referenced by fileInputRef) so the event currentTarget is an HTMLInputElement and no unsafe cast is required; update VideoCard.tsx to move the handler to the input, remove the cast to React.FormEventHandler<HTMLFormElement>, and keep accept, name, hidden and ref as-is.packages/koenig-lexical/src/components/ui/LinkInputWithSearch.tsx (1)
65-78:⚠️ Potential issue | 🟡 MinorIncorrect type cast for
onClickprop.Line 74 casts
onItemSelected as () => void, butonItemSelectedhas signature(item: {value: string; type?: string}) => void. The cast misrepresents the function signature. IfLinkInputSearchItempasses the item when callingonClick, this will work at runtime but the typing is incorrect.Consider properly typing the
LinkInputSearchItemcomponent'sonClickprop or using a more accurate cast that reflects the actual callback signature.♻️ Proposed fix
- onClick={onItemSelected as () => void} + onClick={() => onItemSelected(item)}Or if
LinkInputSearchItempasses the item internally:- onClick={onItemSelected as () => void} + onClick={onItemSelected as (item: {value: string; label: string}) => void}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/koenig-lexical/src/components/ui/LinkInputWithSearch.tsx` around lines 65 - 78, The onClick prop is being miscast in getItem; instead of using onItemSelected as () => void, either adjust LinkInputSearchItem's onClick prop type to accept (item: {value: string; type?: string}) => void, or change the call site in getItem to pass a zero-arg wrapper that forwards the item (e.g., onClick={() => onItemSelected(item)}), ensuring the signatures for LinkInputSearchItem.onClick and the onItemSelected callback match; update the types for LinkInputSearchItem props if you choose the first approach so TypeScript reflects the actual (item) => void signature.packages/koenig-lexical/src/components/ui/ColorOptionButtons.tsx (1)
70-74:⚠️ Potential issue | 🟡 MinorAccessibility:
<li>withonClicklacks keyboard support.The "background-image" list item has an
onClickhandler but no keyboard accessibility attributes (role="button",tabIndex,onKeyDown). This makes it inaccessible to keyboard users.♿ Proposed fix to add keyboard accessibility
-<li key='background-image' className={`mb-0 flex size-[3rem] cursor-pointer items-center justify-center rounded-full border-2 ${selectedName === name ? 'border-green' : 'border-transparent'}`} data-testid="background-image-color-button" onClick={() => onClick(name)}> +<li key='background-image' className={`mb-0 flex size-[3rem] cursor-pointer items-center justify-center rounded-full border-2 ${selectedName === name ? 'border-green' : 'border-transparent'}`} data-testid="background-image-color-button" role="button" tabIndex={0} onClick={() => onClick(name)} onKeyDown={(e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); onClick(name); } }}>🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/koenig-lexical/src/components/ui/ColorOptionButtons.tsx` around lines 70 - 74, The list item with key 'background-image' uses an onClick handler but lacks keyboard accessibility; update that <li> (the element rendering the background-image option) to include role="button", tabIndex={0}, and an onKeyDown handler that calls the existing onClick(name) when Enter or Space is pressed (use the same onClick prop passed into the component), and add an appropriate aria-label or aria-pressed if needed to convey state (use selectedName to reflect selection). This will ensure keyboard users can focus and activate the background-image option the same way mouse users can.packages/koenig-lexical/src/nodes/ProductNodeComponent.tsx (1)
70-89:⚠️ Potential issue | 🟠 MajorAlways release the preview URL in a
finallyblock.If
getImageDimensions()orimgUploader.upload()rejects,imgPreviewnever clears and the blob URL is never revoked. Guard the empty-file case up front and move cleanup intofinally.🧹 Safer upload flow
const handleImgUpload = async (files: File[] | FileList) => { - const imgPreviewUrl = URL.createObjectURL(files[0]); + const fileArray = Array.from(files); + const [file] = fileArray; + if (!file) { + return; + } + + const imgPreviewUrl = URL.createObjectURL(file); setImgPreview(imgPreviewUrl); - const {width, height} = await getImageDimensions(imgPreviewUrl); - const imgUploadResult = await imgUploader.upload(Array.from(files)); - const imageUrl = imgUploadResult?.[0]?.url; - - if (imageUrl) { - editor.update(() => { - const node = $getProductNodeByKey(nodeKey); - if (!node) {return;} - node.productImageSrc = imageUrl; - node.productImageHeight = height; - node.productImageWidth = width; - }); + try { + const {width, height} = await getImageDimensions(imgPreviewUrl); + const imgUploadResult = await imgUploader.upload(fileArray); + const imageUrl = imgUploadResult?.[0]?.url; + + if (imageUrl) { + editor.update(() => { + const node = $getProductNodeByKey(nodeKey); + if (!node) {return;} + node.productImageSrc = imageUrl; + node.productImageHeight = height; + node.productImageWidth = width; + }); + } + } finally { + setImgPreview(''); + URL.revokeObjectURL(imgPreviewUrl); } - - setImgPreview(''); - URL.revokeObjectURL(imgPreviewUrl); };🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/koenig-lexical/src/nodes/ProductNodeComponent.tsx` around lines 70 - 89, The handleImgUpload function currently sets a blob preview and performs async work but only clears setImgPreview and calls URL.revokeObjectURL when all awaits succeed; move the cleanup into a finally block so the preview is always cleared and the object URL always revoked even if getImageDimensions or imgUploader.upload throws. Also guard against empty files up front (return early if files.length === 0) and keep the editor.update block that sets node.productImageSrc/productImageHeight/productImageWidth via $getProductNodeByKey inside the try path; ensure you reference handleImgUpload, getImageDimensions, imgUploader.upload, setImgPreview, URL.revokeObjectURL and $getProductNodeByKey when making the change.packages/koenig-lexical/src/components/ui/KeyboardSelectionWithGroups.tsx (1)
41-63:⚠️ Potential issue | 🟠 MajorKeep Enter consistent with the new hover guard.
Hover now refuses to select items without
value, but the Enter path still callsonSelect(items[selectedIndex])even when the list is empty or the current item is non-selectable.⌨️ Match the keyboard path to the hover rule
if (event.key === 'Enter') { event.preventDefault(); event.stopPropagation(); - onSelect(items[selectedIndex]); + const item = items[selectedIndex]; + if (item?.value) { + onSelect(item); + } }Also applies to: 81-84
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/koenig-lexical/src/components/ui/KeyboardSelectionWithGroups.tsx` around lines 41 - 63, In handleKeydown (the callback handling ArrowUp/ArrowDown/Enter) ensure the Enter path mirrors the hover guard by verifying the selected index is in range and the targeted item has a selectable value before calling onSelect; specifically, replace the direct call onSelect(items[selectedIndex]) with a guard that checks items.length > 0, selectedIndex >= 0 and < items.length, and that items[selectedIndex].value is present/truthy, and only then call onSelect; apply the same additional guard logic to the other Enter-handling occurrence around the code referenced (lines ~81-84) so Enter never selects when the list is empty or the current item is non-selectable.packages/koenig-lexical/src/components/ui/cards/CallToActionCard.tsx (2)
417-433:⚠️ Potential issue | 🟡 MinorNon-null assertion on
htmlEditormay cause runtime error.Similar to the sponsor label editor,
htmlEditor!at line 421 assumes the editor is always provided, but it's typed as optional. TheKoenigNestedEditorcomponent likely requires a valid editor instance.🛡️ Suggested fix: Guard the rendering
-{/* HTML content */} -<KoenigNestedEditor +{/* HTML content */} +{htmlEditor && <KoenigNestedEditor autoFocus={true} dataTestId={'cta-card-content-editor'} hasSettingsPanel={true} - initialEditor={htmlEditor!} + initialEditor={htmlEditor} ... > <ReplacementStringsPlugin /> -</KoenigNestedEditor> +</KoenigNestedEditor>}Or make
htmlEditorrequired in the interface if it's always expected.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/koenig-lexical/src/components/ui/cards/CallToActionCard.tsx` around lines 417 - 433, The code uses a non-null assertion htmlEditor! when rendering KoenigNestedEditor which can crash if htmlEditor is undefined; update the render to guard this usage by only rendering KoenigNestedEditor when htmlEditor is defined (check htmlEditor before rendering) or alternatively make htmlEditor required in the component props/interface so callers must always provide it; ensure related props like htmlEditorInitialState and initialTheme are also only passed/used when htmlEditor exists to avoid further runtime errors.
370-384:⚠️ Potential issue | 🟡 MinorNon-null assertion on
sponsorLabelHtmlEditormay cause runtime error.The assertion
sponsorLabelHtmlEditor!at line 374 assumes the editor is always provided whenhasSponsorLabelis true. However, the prop is typed as optional (sponsorLabelHtmlEditor?: LexicalEditor). If the parent component passeshasSponsorLabel={true}without providing the editor, this will throw.Consider adding a guard or making the prop required when
hasSponsorLabelis true:🛡️ Option 1: Add a guard
{/* Sponsor label */} -{hasSponsorLabel && ( +{hasSponsorLabel && sponsorLabelHtmlEditor && ( <div className={clsx( 'py-3', {'mx-6': color !== 'none'} )}> <KoenigNestedEditor autoFocus={true} dataTestId={'sponsor-label-editor'} hasSettingsPanel={true} - initialEditor={sponsorLabelHtmlEditor!} + initialEditor={sponsorLabelHtmlEditor}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/koenig-lexical/src/components/ui/cards/CallToActionCard.tsx` around lines 370 - 384, The code uses a non-null assertion on sponsorLabelHtmlEditor when rendering KoenigNestedEditor which can throw if hasSponsorLabel is true but sponsorLabelHtmlEditor is undefined; update the render logic in CallToActionCard to guard before using sponsorLabelHtmlEditor (and related sponsorLabelHtmlEditorInitialState) — e.g. only render KoenigNestedEditor when hasSponsorLabel && sponsorLabelHtmlEditor are truthy, or make sponsorLabelHtmlEditor a required prop when hasSponsorLabel is true; ensure RestrictContentPlugin and props (autoFocus, initialEditor, initialEditorState, initialTheme, textClassName) are passed only after the guard so no runtime null assertion occurs.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro
Run ID: b635288d-adc7-4a80-a19f-c848467913ba
📒 Files selected for processing (300)
packages/koenig-lexical/.storybook/main.tspackages/koenig-lexical/demo/DemoApp.tsxpackages/koenig-lexical/demo/HtmlOutputDemo.tsxpackages/koenig-lexical/demo/RestrictedContentDemo.tsxpackages/koenig-lexical/demo/components/DarkModeToggle.tsxpackages/koenig-lexical/demo/components/EmailEditorWrapper.tsxpackages/koenig-lexical/demo/components/FloatingButton.tsxpackages/koenig-lexical/demo/components/InitialContentToggle.tsxpackages/koenig-lexical/demo/components/Navigator.tsxpackages/koenig-lexical/demo/components/SerializedStateTextarea.tsxpackages/koenig-lexical/demo/components/Sidebar.tsxpackages/koenig-lexical/demo/components/TitleTextBox.tsxpackages/koenig-lexical/demo/components/TreeView.tsxpackages/koenig-lexical/demo/components/Watermark.tsxpackages/koenig-lexical/demo/components/WordCount.tsxpackages/koenig-lexical/demo/demo.tsxpackages/koenig-lexical/demo/utils/fetchEmbed.tspackages/koenig-lexical/demo/utils/tenorConfig.tspackages/koenig-lexical/demo/utils/unsplashConfig.tspackages/koenig-lexical/demo/utils/useFileUpload.tspackages/koenig-lexical/demo/utils/useSnippets.tspackages/koenig-lexical/eslint.config.jspackages/koenig-lexical/package.jsonpackages/koenig-lexical/playwright.config.tspackages/koenig-lexical/src/components/DesignSandbox.tsxpackages/koenig-lexical/src/components/KoenigCaptionEditor.tsxpackages/koenig-lexical/src/components/KoenigCardWrapper.tsxpackages/koenig-lexical/src/components/KoenigComposableEditor.tsxpackages/koenig-lexical/src/components/KoenigComposer.tsxpackages/koenig-lexical/src/components/KoenigEditor.tsxpackages/koenig-lexical/src/components/KoenigErrorBoundary.tsxpackages/koenig-lexical/src/components/KoenigNestedComposer.tsxpackages/koenig-lexical/src/components/KoenigNestedEditor.tsxpackages/koenig-lexical/src/components/ui/ActionToolbar.tsxpackages/koenig-lexical/src/components/ui/AtLinkResultsPopup.tsxpackages/koenig-lexical/src/components/ui/AudioUploadForm.jsxpackages/koenig-lexical/src/components/ui/AudioUploadForm.tsxpackages/koenig-lexical/src/components/ui/BackgroundImagePicker.tsxpackages/koenig-lexical/src/components/ui/Button.stories.tsxpackages/koenig-lexical/src/components/ui/Button.tsxpackages/koenig-lexical/src/components/ui/ButtonGroup.stories.tsxpackages/koenig-lexical/src/components/ui/ButtonGroup.tsxpackages/koenig-lexical/src/components/ui/CardCaptionEditor.tsxpackages/koenig-lexical/src/components/ui/CardMenu.stories.tsxpackages/koenig-lexical/src/components/ui/CardMenu.tsxpackages/koenig-lexical/src/components/ui/CardVisibilityMessage.tsxpackages/koenig-lexical/src/components/ui/CardWrapper.tsxpackages/koenig-lexical/src/components/ui/ColorOptionButtons.tsxpackages/koenig-lexical/src/components/ui/ColorPicker.stories.tsxpackages/koenig-lexical/src/components/ui/ColorPicker.tsxpackages/koenig-lexical/src/components/ui/Delayed.tsxpackages/koenig-lexical/src/components/ui/Dropdown.stories.tsxpackages/koenig-lexical/src/components/ui/Dropdown.tsxpackages/koenig-lexical/src/components/ui/DropdownContainer.tsxpackages/koenig-lexical/src/components/ui/EditorPlaceholder.tsxpackages/koenig-lexical/src/components/ui/EmojiPicker.tsxpackages/koenig-lexical/src/components/ui/EmojiPickerPortal.tsxpackages/koenig-lexical/src/components/ui/FileUploadForm.jsxpackages/koenig-lexical/src/components/ui/FileUploadForm.tsxpackages/koenig-lexical/src/components/ui/FloatingFormatToolbar.tsxpackages/koenig-lexical/src/components/ui/FloatingLinkToolbar.tsxpackages/koenig-lexical/src/components/ui/FloatingToolbar.tsxpackages/koenig-lexical/src/components/ui/FormatToolbar.tsxpackages/koenig-lexical/src/components/ui/HighlightedString.tsxpackages/koenig-lexical/src/components/ui/IconButton.stories.tsxpackages/koenig-lexical/src/components/ui/IconButton.tsxpackages/koenig-lexical/src/components/ui/ImageUploadForm.tsxpackages/koenig-lexical/src/components/ui/ImageUploadSwatch.tsxpackages/koenig-lexical/src/components/ui/Input.stories.tsxpackages/koenig-lexical/src/components/ui/Input.tsxpackages/koenig-lexical/src/components/ui/InputList.stories.tsxpackages/koenig-lexical/src/components/ui/InputList.tsxpackages/koenig-lexical/src/components/ui/KeyboardSelection.tsxpackages/koenig-lexical/src/components/ui/KeyboardSelectionWithGroups.tsxpackages/koenig-lexical/src/components/ui/LinkActionToolbar.tsxpackages/koenig-lexical/src/components/ui/LinkActionToolbarWithSearch.tsxpackages/koenig-lexical/src/components/ui/LinkInput.stories.tsxpackages/koenig-lexical/src/components/ui/LinkInput.tsxpackages/koenig-lexical/src/components/ui/LinkInputSearchItem.tsxpackages/koenig-lexical/src/components/ui/LinkInputWithSearch.stories.tsxpackages/koenig-lexical/src/components/ui/LinkInputWithSearch.tsxpackages/koenig-lexical/src/components/ui/LinkToolbar.stories.tsxpackages/koenig-lexical/src/components/ui/LinkToolbar.tsxpackages/koenig-lexical/src/components/ui/MediaPlaceholder.stories.tsxpackages/koenig-lexical/src/components/ui/MediaPlaceholder.tsxpackages/koenig-lexical/src/components/ui/MediaPlayer.stories.tsxpackages/koenig-lexical/src/components/ui/MediaPlayer.tsxpackages/koenig-lexical/src/components/ui/MediaUploader.tsxpackages/koenig-lexical/src/components/ui/Modal.stories.tsxpackages/koenig-lexical/src/components/ui/Modal.tsxpackages/koenig-lexical/src/components/ui/MultiSelectDropdown.tsxpackages/koenig-lexical/src/components/ui/PlusMenu.stories.tsxpackages/koenig-lexical/src/components/ui/PlusMenu.tsxpackages/koenig-lexical/src/components/ui/Portal.tsxpackages/koenig-lexical/src/components/ui/ProgressBar.stories.tsxpackages/koenig-lexical/src/components/ui/ProgressBar.tsxpackages/koenig-lexical/src/components/ui/ReadOnlyOverlay.tsxpackages/koenig-lexical/src/components/ui/SettingsPanel.stories.tsxpackages/koenig-lexical/src/components/ui/SettingsPanel.tsxpackages/koenig-lexical/src/components/ui/SlashMenu.jsxpackages/koenig-lexical/src/components/ui/SlashMenu.tsxpackages/koenig-lexical/src/components/ui/Slider.stories.tsxpackages/koenig-lexical/src/components/ui/Slider.tsxpackages/koenig-lexical/src/components/ui/SnippetActionToolbar.tsxpackages/koenig-lexical/src/components/ui/SnippetInput.stories.tsxpackages/koenig-lexical/src/components/ui/SnippetInput.tsxpackages/koenig-lexical/src/components/ui/SnippetInput/Dropdown.tsxpackages/koenig-lexical/src/components/ui/SnippetInput/Input.tsxpackages/koenig-lexical/src/components/ui/Spinner.tsxpackages/koenig-lexical/src/components/ui/SubscribeForm.stories.tsxpackages/koenig-lexical/src/components/ui/SubscribeForm.tsxpackages/koenig-lexical/src/components/ui/TabView.tsxpackages/koenig-lexical/src/components/ui/TenorPlugin.tsxpackages/koenig-lexical/src/components/ui/TenorSelector.stories.tsxpackages/koenig-lexical/src/components/ui/TenorSelector.tsxpackages/koenig-lexical/src/components/ui/TextInput.tsxpackages/koenig-lexical/src/components/ui/Toggle.stories.tsxpackages/koenig-lexical/src/components/ui/Toggle.tsxpackages/koenig-lexical/src/components/ui/ToolbarMenu.stories.tsxpackages/koenig-lexical/src/components/ui/ToolbarMenu.tsxpackages/koenig-lexical/src/components/ui/ToolbarMenuItem.stories.tsxpackages/koenig-lexical/src/components/ui/Tooltip.tsxpackages/koenig-lexical/src/components/ui/UnsplashPlugin.tsxpackages/koenig-lexical/src/components/ui/UrlInput.stories.tsxpackages/koenig-lexical/src/components/ui/UrlInput.tsxpackages/koenig-lexical/src/components/ui/UrlSearchInput.stories.tsxpackages/koenig-lexical/src/components/ui/UrlSearchInput.tsxpackages/koenig-lexical/src/components/ui/VisibilitySettings.tsxpackages/koenig-lexical/src/components/ui/cards/AudioCard.stories.tsxpackages/koenig-lexical/src/components/ui/cards/AudioCard.tsxpackages/koenig-lexical/src/components/ui/cards/BookmarkCard.stories.tsxpackages/koenig-lexical/src/components/ui/cards/BookmarkCard.tsxpackages/koenig-lexical/src/components/ui/cards/ButtonCard.stories.tsxpackages/koenig-lexical/src/components/ui/cards/ButtonCard.tsxpackages/koenig-lexical/src/components/ui/cards/CallToActionCard.stories.tsxpackages/koenig-lexical/src/components/ui/cards/CallToActionCard.tsxpackages/koenig-lexical/src/components/ui/cards/CalloutCard.stories.tsxpackages/koenig-lexical/src/components/ui/cards/CalloutCard.tsxpackages/koenig-lexical/src/components/ui/cards/CodeBlockCard.jsxpackages/koenig-lexical/src/components/ui/cards/CodeBlockCard.stories.tsxpackages/koenig-lexical/src/components/ui/cards/CodeBlockCard.tsxpackages/koenig-lexical/src/components/ui/cards/EmailCard.stories.tsxpackages/koenig-lexical/src/components/ui/cards/EmailCard.tsxpackages/koenig-lexical/src/components/ui/cards/EmailCtaCard.stories.tsxpackages/koenig-lexical/src/components/ui/cards/EmailCtaCard.tsxpackages/koenig-lexical/src/components/ui/cards/EmbedCard.stories.tsxpackages/koenig-lexical/src/components/ui/cards/EmbedCard.tsxpackages/koenig-lexical/src/components/ui/cards/FileCard.stories.tsxpackages/koenig-lexical/src/components/ui/cards/FileCard.tsxpackages/koenig-lexical/src/components/ui/cards/GalleryCard.stories.tsxpackages/koenig-lexical/src/components/ui/cards/GalleryCard.tsxpackages/koenig-lexical/src/components/ui/cards/HeaderCard/v1/HeaderCard.tsxpackages/koenig-lexical/src/components/ui/cards/HeaderCard/v1/HeaderCard.v1.stories.tsxpackages/koenig-lexical/src/components/ui/cards/HeaderCard/v2/HeaderCard.tsxpackages/koenig-lexical/src/components/ui/cards/HeaderCard/v2/HeaderCard.v2.stories.tsxpackages/koenig-lexical/src/components/ui/cards/HorizontalRuleCard.stories.tsxpackages/koenig-lexical/src/components/ui/cards/HorizontalRuleCard.tsxpackages/koenig-lexical/src/components/ui/cards/HtmlCard.stories.tsxpackages/koenig-lexical/src/components/ui/cards/HtmlCard.tsxpackages/koenig-lexical/src/components/ui/cards/HtmlCard/HtmlEditor.tsxpackages/koenig-lexical/src/components/ui/cards/ImageCard.stories.tsxpackages/koenig-lexical/src/components/ui/cards/ImageCard.tsxpackages/koenig-lexical/src/components/ui/cards/MarkdownCard.stories.tsxpackages/koenig-lexical/src/components/ui/cards/MarkdownCard.tsxpackages/koenig-lexical/src/components/ui/cards/MarkdownCard/MarkdownEditor.tsxpackages/koenig-lexical/src/components/ui/cards/MarkdownCard/MarkdownHelpDialog.tsxpackages/koenig-lexical/src/components/ui/cards/MarkdownCard/MarkdownImageUploader.tsxpackages/koenig-lexical/src/components/ui/cards/MarkdownCard/useMarkdownImageUploader.tspackages/koenig-lexical/src/components/ui/cards/PaywallCard.stories.tsxpackages/koenig-lexical/src/components/ui/cards/PaywallCard.tsxpackages/koenig-lexical/src/components/ui/cards/ProductCard.stories.tsxpackages/koenig-lexical/src/components/ui/cards/ProductCard.tsxpackages/koenig-lexical/src/components/ui/cards/ProductCard/ProductCardImage.tsxpackages/koenig-lexical/src/components/ui/cards/ProductCard/RatingButton.tsxpackages/koenig-lexical/src/components/ui/cards/SignupCard.stories.tsxpackages/koenig-lexical/src/components/ui/cards/SignupCard.tsxpackages/koenig-lexical/src/components/ui/cards/ToggleCard.stories.tsxpackages/koenig-lexical/src/components/ui/cards/ToggleCard.tsxpackages/koenig-lexical/src/components/ui/cards/TransistorCard.tsxpackages/koenig-lexical/src/components/ui/cards/VideoCard.stories.tsxpackages/koenig-lexical/src/components/ui/cards/VideoCard.tsxpackages/koenig-lexical/src/components/ui/file-selectors/Tenor/Error.tsxpackages/koenig-lexical/src/components/ui/file-selectors/Tenor/Gif.tsxpackages/koenig-lexical/src/components/ui/file-selectors/Tenor/Loader.tsxpackages/koenig-lexical/src/components/ui/file-selectors/Unsplash/UnsplashButton.tsxpackages/koenig-lexical/src/components/ui/file-selectors/Unsplash/UnsplashGallery.tsxpackages/koenig-lexical/src/components/ui/file-selectors/Unsplash/UnsplashImage.tsxpackages/koenig-lexical/src/components/ui/file-selectors/Unsplash/UnsplashSelector.stories.tsxpackages/koenig-lexical/src/components/ui/file-selectors/Unsplash/UnsplashSelector.tsxpackages/koenig-lexical/src/components/ui/file-selectors/Unsplash/UnsplashZoomed.tsxpackages/koenig-lexical/src/components/ui/file-selectors/UnsplashModal.jsxpackages/koenig-lexical/src/components/ui/file-selectors/UnsplashModal.tsxpackages/koenig-lexical/src/context/CardContext.jsxpackages/koenig-lexical/src/context/CardContext.tsxpackages/koenig-lexical/src/context/KoenigComposerContext.jsxpackages/koenig-lexical/src/context/KoenigComposerContext.tsxpackages/koenig-lexical/src/context/KoenigSelectedCardContext.tsxpackages/koenig-lexical/src/context/SharedHistoryContext.tsxpackages/koenig-lexical/src/context/SharedOnChangeContext.jsxpackages/koenig-lexical/src/context/SharedOnChangeContext.tsxpackages/koenig-lexical/src/context/TKContext.tsxpackages/koenig-lexical/src/hooks/useCardDragAndDrop.tspackages/koenig-lexical/src/hooks/useClickOutside.tspackages/koenig-lexical/src/hooks/useFileDragAndDrop.tspackages/koenig-lexical/src/hooks/useGalleryReorder.tspackages/koenig-lexical/src/hooks/useInputSelection.tspackages/koenig-lexical/src/hooks/useKoenigTextEntity.tspackages/koenig-lexical/src/hooks/useMovable.tspackages/koenig-lexical/src/hooks/usePinturaEditor.tspackages/koenig-lexical/src/hooks/usePreviousFocus.jspackages/koenig-lexical/src/hooks/usePreviousFocus.tspackages/koenig-lexical/src/hooks/useSearchLinks.tspackages/koenig-lexical/src/hooks/useSettingsPanelReposition.tspackages/koenig-lexical/src/hooks/useTypeaheadTriggerMatch.tspackages/koenig-lexical/src/hooks/useVisibilityToggle.tspackages/koenig-lexical/src/index.tspackages/koenig-lexical/src/nodes/AsideNode.tspackages/koenig-lexical/src/nodes/AudioNode.tsxpackages/koenig-lexical/src/nodes/AudioNodeComponent.tsxpackages/koenig-lexical/src/nodes/BasicNodes.tspackages/koenig-lexical/src/nodes/BookmarkNode.tsxpackages/koenig-lexical/src/nodes/BookmarkNodeComponent.tsxpackages/koenig-lexical/src/nodes/ButtonNode.tsxpackages/koenig-lexical/src/nodes/ButtonNodeComponent.tsxpackages/koenig-lexical/src/nodes/CallToActionNode.tsxpackages/koenig-lexical/src/nodes/CallToActionNodeComponent.tsxpackages/koenig-lexical/src/nodes/CalloutNode.tsxpackages/koenig-lexical/src/nodes/CalloutNodeComponent.tsxpackages/koenig-lexical/src/nodes/CodeBlockNode.tsxpackages/koenig-lexical/src/nodes/CodeBlockNodeComponent.tsxpackages/koenig-lexical/src/nodes/DefaultNodes.tspackages/koenig-lexical/src/nodes/EmailCtaNode.tsxpackages/koenig-lexical/src/nodes/EmailCtaNodeComponent.tsxpackages/koenig-lexical/src/nodes/EmailEditorNodes.tspackages/koenig-lexical/src/nodes/EmailEmbedNode.tsxpackages/koenig-lexical/src/nodes/EmailNode.tsxpackages/koenig-lexical/src/nodes/EmailNodeComponent.tsxpackages/koenig-lexical/src/nodes/EmailNodes.tspackages/koenig-lexical/src/nodes/EmbedNode.tsxpackages/koenig-lexical/src/nodes/EmbedNodeComponent.tsxpackages/koenig-lexical/src/nodes/FileNode.tsxpackages/koenig-lexical/src/nodes/FileNodeComponent.tsxpackages/koenig-lexical/src/nodes/GalleryNode.tsxpackages/koenig-lexical/src/nodes/GalleryNodeComponent.tsxpackages/koenig-lexical/src/nodes/HeaderNode.tsxpackages/koenig-lexical/src/nodes/HorizontalRuleNode.tsxpackages/koenig-lexical/src/nodes/HtmlNode.tsxpackages/koenig-lexical/src/nodes/HtmlNodeComponent.tsxpackages/koenig-lexical/src/nodes/ImageNode.tsxpackages/koenig-lexical/src/nodes/ImageNodeComponent.tsxpackages/koenig-lexical/src/nodes/MarkdownNode.tsxpackages/koenig-lexical/src/nodes/MarkdownNodeComponent.tsxpackages/koenig-lexical/src/nodes/MinimalNodes.tspackages/koenig-lexical/src/nodes/PaywallNode.tsxpackages/koenig-lexical/src/nodes/ProductNode.tsxpackages/koenig-lexical/src/nodes/ProductNodeComponent.tsxpackages/koenig-lexical/src/nodes/SignupNode.tsxpackages/koenig-lexical/src/nodes/SignupNodeComponent.tsxpackages/koenig-lexical/src/nodes/ToggleNode.tsxpackages/koenig-lexical/src/nodes/ToggleNodeComponent.tsxpackages/koenig-lexical/src/nodes/TransistorNode.tsxpackages/koenig-lexical/src/nodes/TransistorNodeComponent.tsxpackages/koenig-lexical/src/nodes/VideoNode.tsxpackages/koenig-lexical/src/nodes/VideoNodeComponent.tsxpackages/koenig-lexical/src/nodes/header/v1/HeaderNodeComponent.tsxpackages/koenig-lexical/src/nodes/header/v2/HeaderNodeComponent.tsxpackages/koenig-lexical/src/plugins/AllDefaultPlugins.tsxpackages/koenig-lexical/src/plugins/AtLinkPlugin.tsxpackages/koenig-lexical/src/plugins/AudioPlugin.tsxpackages/koenig-lexical/src/plugins/BookmarkPlugin.tsxpackages/koenig-lexical/src/plugins/ButtonPlugin.tsxpackages/koenig-lexical/src/plugins/CallToActionPlugin.tsxpackages/koenig-lexical/src/plugins/CalloutPlugin.tsxpackages/koenig-lexical/src/plugins/CardMenuPlugin.tsxpackages/koenig-lexical/src/plugins/DragDropPastePlugin.tsxpackages/koenig-lexical/src/plugins/DragDropReorderPlugin.tsxpackages/koenig-lexical/src/plugins/EmEnDashPlugin.tsxpackages/koenig-lexical/src/plugins/EmailCtaPlugin.tsxpackages/koenig-lexical/src/plugins/EmailPlugin.tsxpackages/koenig-lexical/src/plugins/EmbedPlugin.tsxpackages/koenig-lexical/src/plugins/EmojiPickerPlugin.tsxpackages/koenig-lexical/src/plugins/ExternalControlPlugin.tsxpackages/koenig-lexical/src/plugins/FilePlugin.tsxpackages/koenig-lexical/src/plugins/FloatingToolbarPlugin.tsxpackages/koenig-lexical/src/plugins/GalleryPlugin.tsxpackages/koenig-lexical/src/plugins/HeaderPlugin.tsxpackages/koenig-lexical/src/plugins/HorizontalRulePlugin.tsxpackages/koenig-lexical/src/plugins/HtmlOutputPlugin.tsxpackages/koenig-lexical/src/plugins/HtmlPlugin.tsxpackages/koenig-lexical/src/plugins/ImagePlugin.tsxpackages/koenig-lexical/src/plugins/KoenigBehaviourPlugin.tsxpackages/koenig-lexical/src/plugins/KoenigBlurPlugin.tsxpackages/koenig-lexical/src/plugins/KoenigFocusPlugin.tsxpackages/koenig-lexical/src/plugins/KoenigNestedEditorPlugin.tsxpackages/koenig-lexical/src/plugins/KoenigSelectorPlugin.tsxpackages/koenig-lexical/src/plugins/KoenigSnippetPlugin.tsxpackages/koenig-lexical/src/plugins/MarkdownPastePlugin.tsxpackages/koenig-lexical/src/plugins/MarkdownPlugin.tsxpackages/koenig-lexical/src/plugins/MarkdownShortcutPlugin.tsxpackages/koenig-lexical/src/plugins/PaywallPlugin.tsx
💤 Files with no reviewable changes (9)
- packages/koenig-lexical/src/components/ui/SlashMenu.jsx
- packages/koenig-lexical/src/components/ui/file-selectors/UnsplashModal.jsx
- packages/koenig-lexical/src/context/CardContext.jsx
- packages/koenig-lexical/src/context/KoenigComposerContext.jsx
- packages/koenig-lexical/src/components/ui/AudioUploadForm.jsx
- packages/koenig-lexical/src/components/ui/FileUploadForm.jsx
- packages/koenig-lexical/src/components/ui/cards/CodeBlockCard.jsx
- packages/koenig-lexical/src/context/SharedOnChangeContext.jsx
- packages/koenig-lexical/src/hooks/usePreviousFocus.js
| interface SidebarProps { | ||
| isOpen: boolean; | ||
| view: string; | ||
| saveContent?: () => void; | ||
| } | ||
|
|
||
| const Sidebar = ({isOpen, view, saveContent}: SidebarProps) => { |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Verify Sidebar call sites pass valid literal views and correct saveContent pairing.
rg -n -C2 '<Sidebar\b|view=\{?["'\'']?(json|tree)|saveContent=' --type=tsx --type=jsxRepository: TryGhost/Koenig
Length of output: 86
🏁 Script executed:
# Search for Sidebar component usage with corrected flags
rg -n '<Sidebar' --glob='*.{tsx,jsx}' -C2Repository: TryGhost/Koenig
Length of output: 1765
🏁 Script executed:
# Also read the full Sidebar.tsx file to understand the implementation
cat packages/koenig-lexical/demo/components/Sidebar.tsxRepository: TryGhost/Koenig
Length of output: 941
🏁 Script executed:
# Check RestrictedContentDemo for sidebarView initialization/values
rg -n 'sidebarView' packages/koenig-lexical/demo/RestrictedContentDemo.tsx -B3 -A3Repository: TryGhost/Koenig
Length of output: 1086
🏁 Script executed:
# Check HtmlOutputDemo for sidebarView initialization/values
rg -n 'sidebarView' packages/koenig-lexical/demo/HtmlOutputDemo.tsx -B3 -A3Repository: TryGhost/Koenig
Length of output: 1245
🏁 Script executed:
# Check DemoApp for sidebarView initialization/values
rg -n 'sidebarView' packages/koenig-lexical/demo/DemoApp.tsx -B3 -A3Repository: TryGhost/Koenig
Length of output: 1164
🏁 Script executed:
# Check if openSidebar is ever called with a different view in RestrictedContentDemo
rg -n 'openSidebar' packages/koenig-lexical/demo/RestrictedContentDemo.tsxRepository: TryGhost/Koenig
Length of output: 191
🏁 Script executed:
# Check if openSidebar is ever called with a different view in HtmlOutputDemo
rg -n 'openSidebar' packages/koenig-lexical/demo/HtmlOutputDemo.tsxRepository: TryGhost/Koenig
Length of output: 195
🏁 Script executed:
# Check what FloatingButton does - does it ever call openSidebar with a 'tree' argument
rg -n 'FloatingButton' packages/koenig-lexical/demo/components/ -A20Repository: TryGhost/Koenig
Length of output: 2052
🏁 Script executed:
# Also verify the actual FloatingButton component implementation
find packages/koenig-lexical/demo -name 'FloatingButton*'Repository: TryGhost/Koenig
Length of output: 117
Fix runtime bug in RestrictedContentDemo and HtmlOutputDemo by refactoring SidebarProps with a discriminated union.
Currently, RestrictedContentDemo.tsx (line 117) and HtmlOutputDemo.tsx (line 111) pass Sidebar without saveContent, but when users click the "JSON output" button, the component attempts to render <button onClick={saveContent}> with an undefined callback, causing a runtime error. Refactor SidebarProps as a discriminated union to enforce that saveContent is required when view is 'json' and forbidden when it is 'tree'.
♻️ Proposed typing refactor
-interface SidebarProps {
- isOpen: boolean;
- view: string;
- saveContent?: () => void;
-}
+type SidebarProps =
+ | {isOpen: boolean; view: 'json'; saveContent: () => void}
+ | {isOpen: boolean; view: 'tree'; saveContent?: never};🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/koenig-lexical/demo/components/Sidebar.tsx` around lines 4 - 10,
SidebarProps currently allows absence of saveContent causing a runtime error
when Sidebar (component Sidebar) is rendered with view === 'json' but
saveContent is undefined; refactor SidebarProps into a discriminated union: one
variant { view: 'json'; isOpen: boolean; saveContent: () => void } and another
variant { view: 'tree'; isOpen: boolean } so the type system enforces
saveContent is required for 'json' and forbidden for 'tree'; update the Sidebar
component signature to use this union and narrow by view before calling
saveContent, and update callers (RestrictedContentDemo and HtmlOutputDemo) to
pass saveContent when they render Sidebar with view='json' or switch their view
to 'tree' to avoid passing saveContent.
| import React, {useState} from 'react'; | ||
| import {Modal} from './Modal'; | ||
| import {useState} from 'react'; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
cat -n packages/koenig-lexical/src/components/ui/Modal.stories.tsx | head -30Repository: TryGhost/Koenig
Length of output: 1004
Remove duplicate useState import to avoid module-scope redeclaration.
useState is imported on Line 2 and again on Line 4, which causes a duplicate identifier error in TS/ESM parsing.
Proposed fix
import React, {useState} from 'react';
import {Modal} from './Modal';
-import {useState} from 'react';📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| import React, {useState} from 'react'; | |
| import {Modal} from './Modal'; | |
| import {useState} from 'react'; | |
| import React, {useState} from 'react'; | |
| import {Modal} from './Modal'; |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/koenig-lexical/src/components/ui/Modal.stories.tsx` around lines 2 -
4, The file imports useState twice causing a module-scope redeclaration; remove
the duplicate import statement (the second "import {useState} from 'react';")
and ensure useState is only imported once from React (e.g., keep "import React,
{useState} from 'react';") so references in Modal.stories.tsx and the Modal
component remain valid.
| function highlightPrev() { | ||
| if (highlightedGif.index === 0) { | ||
| if (highlightedGif!.index === 0) { | ||
| // reached the beginning, focus the search bar | ||
| focusSearch(); | ||
| } | ||
|
|
||
| setHighlightedGif(gifs[highlightedGif.index - 1]); | ||
| setHighlightedGif(gifs[highlightedGif!.index - 1]); | ||
| } |
There was a problem hiding this comment.
highlightPrev accesses gifs[-1] when index is 0.
When highlightedGif.index === 0, the function calls focusSearch() but does not return early. Execution continues to line 138 which accesses gifs[0 - 1] = gifs[-1], resulting in undefined being set as the highlighted GIF.
Add early return after focusSearch()
function highlightPrev() {
if (highlightedGif!.index === 0) {
// reached the beginning, focus the search bar
focusSearch();
+ return;
}
setHighlightedGif(gifs[highlightedGif!.index - 1]);
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| function highlightPrev() { | |
| if (highlightedGif.index === 0) { | |
| if (highlightedGif!.index === 0) { | |
| // reached the beginning, focus the search bar | |
| focusSearch(); | |
| } | |
| setHighlightedGif(gifs[highlightedGif.index - 1]); | |
| setHighlightedGif(gifs[highlightedGif!.index - 1]); | |
| } | |
| function highlightPrev() { | |
| if (highlightedGif!.index === 0) { | |
| // reached the beginning, focus the search bar | |
| focusSearch(); | |
| return; | |
| } | |
| setHighlightedGif(gifs[highlightedGif!.index - 1]); | |
| } |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/koenig-lexical/src/components/ui/TenorSelector.tsx` around lines 132
- 139, The bug is that highlightPrev() calls focusSearch() when
highlightedGif!.index === 0 but then continues and does
setHighlightedGif(gifs[highlightedGif!.index - 1]) which reads gifs[-1]; fix
highlightPrev() so that after calling focusSearch() it returns early to avoid
accessing a negative index, i.e. inside highlightPrev() check highlightedGif (or
keep the existing non-null assertion), call focusSearch() when
highlightedGif.index === 0 and immediately return; otherwise call
setHighlightedGif(gifs[highlightedGif.index - 1]).
| useEffect(() => { | ||
| editor.registerCommand( | ||
| BLUR_COMMAND, | ||
| () => { | ||
| onBlur?.(); | ||
| return false; | ||
| }, | ||
| COMMAND_PRIORITY_EDITOR | ||
| ); |
There was a problem hiding this comment.
Missing cleanup for command registration causes memory leak.
editor.registerCommand returns an unregister function that must be returned from the useEffect to properly clean up when the component unmounts or dependencies change. Without this, the command handler remains registered.
🐛 Proposed fix
useEffect(() => {
- editor.registerCommand(
+ return editor.registerCommand(
BLUR_COMMAND,
() => {
onBlur?.();
return false;
},
COMMAND_PRIORITY_EDITOR
);
}, [editor, onBlur]);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| useEffect(() => { | |
| editor.registerCommand( | |
| BLUR_COMMAND, | |
| () => { | |
| onBlur?.(); | |
| return false; | |
| }, | |
| COMMAND_PRIORITY_EDITOR | |
| ); | |
| useEffect(() => { | |
| return editor.registerCommand( | |
| BLUR_COMMAND, | |
| () => { | |
| onBlur?.(); | |
| return false; | |
| }, | |
| COMMAND_PRIORITY_EDITOR | |
| ); | |
| }, [editor, onBlur]); |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/koenig-lexical/src/plugins/KoenigBlurPlugin.tsx` around lines 7 -
15, The useEffect registers BLUR_COMMAND via editor.registerCommand (with
COMMAND_PRIORITY_EDITOR) but never cleans up, causing a leak; update the
useEffect that calls editor.registerCommand to capture the unregister function
it returns and return a cleanup function that calls it (ensuring onBlur and the
registration are removed when the component unmounts or dependencies change),
referencing the same BLUR_COMMAND/COMMAND_PRIORITY_EDITOR registration so the
exact handler is unregistered.
There was a problem hiding this comment.
Actionable comments posted: 9
Note
Due to the large number of review comments, Critical, Major severity comments were prioritized as inline comments.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (7)
packages/koenig-lexical/src/hooks/useCardDragAndDrop.ts (1)
85-120:⚠️ Potential issue | 🟠 MajorDestroy the registered drag container on effect cleanup.
This effect re-registers a container whenever its dependencies change, but it never calls
destroy(). That leaves stale listeners behind and can fire duplicate drag/drop callbacks after rerenders.💡 Suggested fix
React.useEffect(() => { if (!containerRef || !koenig.dragDropHandler) { return; } - dragDropContainer.current = (koenig.dragDropHandler as DragDropHandler).registerContainer( + const container = (koenig.dragDropHandler as DragDropHandler).registerContainer( containerRef, { draggableSelector, droppableSelector, isDragEnabled: enabled, @@ onDrop: _onDrop, onDropEnd: _onDropEnd } ); + + dragDropContainer.current = container; + + return () => { + container.destroy(); + if (dragDropContainer.current === container) { + dragDropContainer.current = null; + } + }; }, [ _getDraggableInfo, _getIndicatorPosition, _onDrop,🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/koenig-lexical/src/hooks/useCardDragAndDrop.ts` around lines 85 - 120, The effect registers a drag container via (koenig.dragDropHandler as DragDropHandler).registerContainer and never destroys it; add a cleanup function to the React.useEffect that, when run, calls destroy() on dragDropContainer.current (if present) and clears dragDropContainer.current to null to avoid leaking listeners and duplicate callbacks; keep the existing dependencies (_getDraggableInfo, _getIndicatorPosition, _onDrop, _onDropEnd, containerRef, draggableSelector, droppableSelector, enabled, koenig.dragDropHandler, onDragEnd, onDragEnterContainer, onDragLeaveContainer, onDragStart) and ensure the cleanup runs before re-registering.packages/koenig-lexical/src/plugins/EmojiPickerPlugin.tsx (1)
170-176:⚠️ Potential issue | 🟡 MinorGuard the DOM selection before calling
getRangeAt(0).
window.getSelection()can benull, andrangeCountcan be0after blur or while the menu is settling. The non-null assertion turns that into a render-time crash.💡 Suggested fix
function getPositionStyles() { - const selectedRange = window.getSelection()!.getRangeAt(0); + const selection = window.getSelection(); + if (!selection || selection.rangeCount === 0) { + return undefined; + } + const selectedRange = selection.getRangeAt(0); const rangeRect = selectedRange.getBoundingClientRect(); return { marginTop: `${rangeRect.height}px` };🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/koenig-lexical/src/plugins/EmojiPickerPlugin.tsx` around lines 170 - 176, The getPositionStyles function currently calls window.getSelection()! and getRangeAt(0) unguarded which can throw if selection is null or rangeCount is 0; update getPositionStyles to first get const sel = window.getSelection(), check sel is non-null and sel.rangeCount > 0 before calling sel.getRangeAt(0) and using its bounding rect, and return a safe fallback style (e.g., { marginTop: '0px' } or undefined) when there is no valid range so rendering cannot crash.packages/koenig-lexical/src/plugins/KoenigFocusPlugin.tsx (1)
7-16:⚠️ Potential issue | 🟠 MajorMissing cleanup return from
useEffectcauses listener leak.
editor.registerCommandreturns an unregister function that must be returned from theuseEffectto clean up when the component unmounts or dependencies change. Currently, the cleanup function is discarded.Proposed fix
useEffect(() => { - editor.registerCommand( + return editor.registerCommand( FOCUS_COMMAND, () => { onFocus?.(); return false; }, COMMAND_PRIORITY_EDITOR ); }, [editor, onFocus]);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/koenig-lexical/src/plugins/KoenigFocusPlugin.tsx` around lines 7 - 16, The effect registering the focus handler is leaking because editor.registerCommand (called inside useEffect) returns an unregister function that isn't used; update the useEffect that calls editor.registerCommand with FOCUS_COMMAND and COMMAND_PRIORITY_EDITOR to capture the returned cleanup function and return it from the effect so the listener is removed when dependencies (editor, onFocus) change or the component unmounts; keep the existing call to onFocus() in the registered command.packages/koenig-lexical/src/components/ui/UrlSearchInput.tsx (1)
53-59:⚠️ Potential issue | 🟠 MajorMove event handlers from nested
<span>to<button>elements for proper keyboard and click interaction.Lines 56–57 attach click handlers to inner
<span>elements instead of their parent<button>elements. This breaks keyboard activation (Enter/Space keys) and makes clicks on button padding ineffective—only clicks on the text itself will trigger the handlers.Suggested fix
-<button className="mr-3 cursor-pointer" data-testid={`${dataTestId}-error-retry`} type="button"><span className="font-semibold underline" onClick={handleRetry}>Retry</span></button> -<button className="mr-3 cursor-pointer" data-testid={`${dataTestId}-error-pasteAsLink`} type="button"><span className="font-semibold underline" onClick={() => handlePasteAsLink?.(value)}>Paste URL as link</span></button> +<button className="mr-3 cursor-pointer" data-testid={`${dataTestId}-error-retry`} type="button" onClick={handleRetry}><span className="font-semibold underline">Retry</span></button> +<button className="mr-3 cursor-pointer" data-testid={`${dataTestId}-error-pasteAsLink`} type="button" onClick={() => handlePasteAsLink?.(value)}><span className="font-semibold underline">Paste URL as link</span></button>🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/koenig-lexical/src/components/ui/UrlSearchInput.tsx` around lines 53 - 59, The click handlers for the retry and paste-as-link actions are attached to inner <span> elements which breaks keyboard activation and click area; move the onClick handlers from the inner spans to their parent <button> elements (the buttons with data-testid values `${dataTestId}-error-retry` and `${dataTestId}-error-pasteAsLink`) and remove the onClick from the spans so handleRetry and handlePasteAsLink(value) are invoked by the button elements instead; ensure the buttons keep type="button" and accessible labeling/styling (the spans may remain for styling only).packages/koenig-lexical/src/components/ui/KeyboardSelectionWithGroups.tsx (1)
41-63:⚠️ Potential issue | 🟠 MajorShort-circuit keyboard handling when there are no items.
When
items.length === 0, ArrowDown drivesselectedIndexto-1, and Enter passesundefinedtoonSelect. Unlike mouse interactions (which checkif (item.value)before updating selection), the keyboard handler lacks guards and can leave the component in an invalid state on no-results screens.Suggested fix
const handleKeydown = React.useCallback((event: KeyboardEvent) => { + if (items.length === 0) { + return; + } + if (event.key === 'ArrowDown') { event.preventDefault(); event.stopPropagation(); setSelectedIndex((i) => { return Math.min(i + 1, items.length - 1); }); setScrollSelectedIntoView(true); } if (event.key === 'ArrowUp') { event.preventDefault(); event.stopPropagation(); setSelectedIndex((i) => { return Math.max(i - 1, 0); }); setScrollSelectedIntoView(true); } if (event.key === 'Enter') { event.preventDefault(); event.stopPropagation(); - onSelect(items[selectedIndex]); + const selectedItem = items[selectedIndex]; + if (selectedItem) { + onSelect(selectedItem); + } } }, [items, selectedIndex, onSelect]);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/koenig-lexical/src/components/ui/KeyboardSelectionWithGroups.tsx` around lines 41 - 63, The keyboard handler handleKeydown should short-circuit when there are no items: at the start of handleKeydown check items.length === 0 and return early so ArrowDown/ArrowUp don't drive selectedIndex to -1 and Enter doesn't call onSelect with undefined; additionally, when handling Enter ensure the selected item exists (e.g., guard that items[selectedIndex] is truthy) before calling onSelect, and keep the existing use of setSelectedIndex and setScrollSelectedIntoView for valid navigation only.packages/koenig-lexical/src/components/ui/cards/HeaderCard/v1/HeaderCard.tsx (1)
232-247:⚠️ Potential issue | 🟡 MinorMake the button-editing handlers required when
buttonis enabled.This branch renders editable controls whenever
buttonis truthy (line 233), buthandleButtonTextandhandleButtonUrlare both optional in the props interface. The code passeshandleButtonUrl!with a non-null assertion (line 247) whilehandleButtonTextis passed without assertion (line 240), masking a type contract violation. Callers can passbutton={true}without providing these handlers, leading to runtime errors on the first change event. A discriminated union onbuttonwould enforce this invariant at the type level.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/koenig-lexical/src/components/ui/cards/HeaderCard/v1/HeaderCard.tsx` around lines 232 - 247, The component renders button-editing controls when the prop button is truthy but props handleButtonText and handleButtonUrl are optional and currently asserted with handleButtonUrl!, causing possible runtime errors; change the HeaderCard props to a discriminated union on button (e.g. {button: true; handleButtonText: ..., handleButtonUrl: ...} | {button?: false; handleButtonText?: ..., handleButtonUrl?: ...}) so that when button is true the handlers are required, update usages/callers to satisfy the new type, and remove the non-null assertion on handleButtonUrl when passing handlers into InputSetting and InputUrlSetting.packages/koenig-lexical/src/components/ui/SettingsPanel.tsx (1)
34-39:⚠️ Potential issue | 🟠 MajorGuard
tabContentshape before passing toTabView.Line 38 currently treats any object-like
childrenas tab-content map. React elements/arrays are also objects, so with tabs enabled this can pass an invalid shape intoTabViewat Line 52.Proposed fix
- const tabContent = React.useMemo(() => { + const tabContent = React.useMemo<Record<string, React.ReactNode>>(() => { if (!tabs) { return {default: children}; } - return typeof children === 'object' && children !== null ? children : {default: children}; + if ( + children !== null && + typeof children === 'object' && + !React.isValidElement(children) && + !Array.isArray(children) + ) { + return children as Record<string, React.ReactNode>; + } + return {default: children}; }, [tabs, children]); ... - <TabView defaultTab={defaultTab} tabContent={tabContent as Record<string, React.ReactNode>} tabs={tabs} /> + <TabView defaultTab={defaultTab} tabContent={tabContent} tabs={tabs} />Also applies to: 52-52
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/koenig-lexical/src/components/ui/SettingsPanel.tsx` around lines 34 - 39, When building tabContent in SettingsPanel (the tabContent const used by TabView), ensure you only treat children as a tab-content map when it's a plain object (not a React element or array). Replace the current typeof children === 'object' check with a guard that verifies children !== null && typeof children === 'object' && !Array.isArray(children) && !React.isValidElement(children) before returning children; otherwise return {default: children} so TabView always receives a valid tab-content shape.
♻️ Duplicate comments (1)
packages/koenig-lexical/src/components/ui/TenorSelector.tsx (1)
132-139:⚠️ Potential issue | 🔴 Critical
highlightPrevstill missing early return afterfocusSearch().The bug flagged in a previous review remains unfixed. When
highlightedGif.index === 0, execution continues pastfocusSearch()to line 138, accessinggifs[-1]which returnsundefined.Proposed fix
function highlightPrev() { if (highlightedGif!.index === 0) { // reached the beginning, focus the search bar focusSearch(); + return; } setHighlightedGif(gifs[highlightedGif!.index - 1]); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/koenig-lexical/src/components/ui/TenorSelector.tsx` around lines 132 - 139, In highlightPrev(), when highlightedGif!.index === 0 you must return immediately after calling focusSearch() to avoid executing setHighlightedGif(gifs[highlightedGif!.index - 1]) which accesses gifs[-1]; update the function (referencing highlightPrev, highlightedGif, focusSearch, setHighlightedGif, and gifs) to add an early return right after focusSearch() so the out-of-bounds access is prevented.
🟡 Minor comments (24)
packages/koenig-lexical/demo/utils/useFileUpload.ts-49-66 (1)
49-66:⚠️ Potential issue | 🟡 MinorPotential runtime error if
typeis not a valid key.If
defaultValidatoris called whentypeis an empty string (the default) or any string not present infileTypes, line 53 will throwCannot read properties of undefined (reading 'extensions'). The early return fortype === 'file'doesn't guard against other invalid types.🛡️ Proposed defensive check
function defaultValidator(file: File): string | true { if (type === 'file') { return true; } + if (!fileTypes[type]) { + return true; // Accept all files for unknown types + } const extensions = fileTypes[type].extensions;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/koenig-lexical/demo/utils/useFileUpload.ts` around lines 49 - 66, defaultValidator currently assumes fileTypes[type] exists and reads .extensions, which will throw if type is empty or not a valid key; add a defensive guard in defaultValidator to check that type is truthy and fileTypes[type] exists (and has .extensions) before accessing it, and if the check fails either accept the file (return true) or return a clear validation message; reference the defaultValidator function and the fileTypes and type variables when making this change.packages/koenig-lexical/src/components/ui/cards/FileCard.tsx-177-181 (1)
177-181:⚠️ Potential issue | 🟡 MinorNon-null assertion on
onFileChangecould cause runtime crash.
onFileChangeis typed as optional (onFileChange?: ...) inFileCardProps, but line 180 uses a non-null assertion (onFileChange!). If theEmptyFileCardbranch renders and a user selects a file withoutonFileChangebeing provided, this will throw a runtime error.🛡️ Proposed fix
Either make
onFileChangerequired when rendering the empty state, or add a guard:<FileUploadForm fileInputRef={onFileInputRef} setFileInputRef={setFileInputRef} - onFileChange={onFileChange!} + onFileChange={onFileChange ?? (() => {})} />Alternatively, if
FileUploadFormcan accept an optional handler, remove the assertion:- onFileChange={onFileChange!} + onFileChange={onFileChange}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/koenig-lexical/src/components/ui/cards/FileCard.tsx` around lines 177 - 181, The EmptyFileCard rendering uses a non-null assertion on the optional prop onFileChange when passing it into FileUploadForm (see FileCard.tsx around the FileUploadForm usage), which can crash at runtime; fix by either requiring onFileChange when rendering the empty state or guarding the prop: update the FileCard render logic to only pass onFileChange to FileUploadForm if it is defined (or provide a no-op fallback handler) so that FileUploadForm always receives a valid function, and ensure any prop types (FileCardProps or FileUploadForm props) reflect the optional/required status consistently.packages/koenig-lexical/src/components/ui/cards/FileCard.tsx-70-81 (1)
70-81:⚠️ Potential issue | 🟡 MinorNon-null assertions on optional handlers may cause runtime errors.
The
handleFileTitle!andhandleFileDesc!assertions assume these handlers are always provided when theTextInputcomponents are rendered. While the conditional rendering (isEditing || title/desc) provides some protection, ifisEditingis true but the handlers aren't provided, this will throw at runtime.Consider adding defensive guards or making the handlers required when
isEditingis expected to be true.🛡️ Proposed fix with defensive guards
<TextInput className="h-[30px] bg-transparent text-lg font-bold leading-none tracking-tight text-black dark:text-grey-200" data-kg-file-card="fileTitle" maxLength={80} placeholder={titlePlaceholder} value={title} - onChange={handleFileTitle!} + onChange={handleFileTitle} /><TextInput className="h-[26px] bg-transparent pb-1 text-[1.6rem] font-normal leading-none text-grey-700 placeholder:text-grey-500 dark:text-grey-300 dark:placeholder:text-grey-800" data-kg-file-card="fileDescription" maxLength={100} placeholder={descPlaceholder} value={desc} - onChange={handleFileDesc!} + onChange={handleFileDesc} />This requires
TextInputto accept an optionalonChangeprop, which would be the cleaner approach.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/koenig-lexical/src/components/ui/cards/FileCard.tsx` around lines 70 - 81, The code uses non-null assertions on handleFileTitle and handleFileDesc when rendering TextInput, which can throw if isEditing is true but handlers are undefined; remove the "!" assertions and either guard the prop (e.g., only pass onChange when handleFileTitle/handleFileDesc is defined) or provide a safe no-op fallback, and update TextInput's props/type to accept an optional onChange so the component compiles cleanly; reference the handleFileTitle, handleFileDesc, TextInput, isEditing, title and desc symbols when making the changes.packages/koenig-lexical/src/components/ui/TenorSelector.tsx-359-359 (1)
359-359:⚠️ Potential issue | 🟡 MinorUnsafe cast of
errortostring.The
errorprop is typed asunknownbut cast directly tostring. If error is anErrorobject or other type, the Error component may not render it correctly.Proposed fix: safely convert error to string
- {!!error && <div data-testid="tenor-selector-error"><Error error={error as string} /></div>} + {!!error && <div data-testid="tenor-selector-error"><Error error={error instanceof Error ? error.message : String(error)} /></div>}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/koenig-lexical/src/components/ui/TenorSelector.tsx` at line 359, The line in TenorSelector.tsx unsafely casts the unknown error to string; replace that cast by normalizing error into a string before passing to the Error component (e.g., check typeof error === 'string', else if error instanceof Error use error.message, otherwise use String(error or 'Unknown error')). Update the JSX around {!!error && <div data-testid="tenor-selector-error"><Error error={...} /></div>} to pass the safely converted string instead of (error as string).packages/koenig-lexical/src/nodes/EmailCtaNode.tsx-83-93 (1)
83-93:⚠️ Potential issue | 🟡 MinorUse public getter for
segmentproperty to match other properties.Line 90 accesses
this.__segmentdirectly while other base properties use public getters (this.alignment,this.buttonText,this.buttonUrl,this.showButton,this.showDividers). Change tothis.segmentfor consistency.Diff
<EmailCtaNodeComponent alignment={this.alignment as string} buttonText={this.buttonText as string} buttonUrl={this.buttonUrl as string} htmlEditor={this.__htmlEditor} htmlEditorInitialState={this.__htmlEditorInitialState as string | undefined} nodeKey={this.getKey()} - segment={this.__segment as string} + segment={this.segment as string} showButton={this.showButton as boolean} showDividers={this.showDividers as boolean} />🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/koenig-lexical/src/nodes/EmailCtaNode.tsx` around lines 83 - 93, The code is directly accessing the private field this.__segment when rendering EmailCtaNodeComponent; change it to use the public getter this.segment (matching how alignment, buttonText, buttonUrl, showButton, and showDividers are used) so replace the this.__segment prop passed to EmailCtaNodeComponent with this.segment to keep property access consistent in the EmailCtaNode render method.packages/koenig-lexical/src/nodes/FileNodeComponent.tsx-64-74 (1)
64-74:⚠️ Potential issue | 🟡 MinorNon-null assertion on
filescould cause runtime error.
e.target.filescan benull(e.g., if the user cancels the file dialog). Thefiles!assertion on line 74 bypasses this check. Consider adding an early return guard.Proposed fix
const onFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => { const files = e.target.files; + if (!files || files.length === 0) { + return; + } // reset original src so it can be replaced with preview and upload progress editor.update(() => { const node = $getNodeByKey(nodeKey) as FileNode | null; if (!node) {return;} node.src = ''; }); - return await fileUploadHandler(Array.from(files!), nodeKey, editor, uploader.upload); + return await fileUploadHandler(Array.from(files), nodeKey, editor, uploader.upload); };🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/koenig-lexical/src/nodes/FileNodeComponent.tsx` around lines 64 - 74, The handler onFileChange uses e.target.files with a non-null assertion (files!) which can throw if the user cancels the dialog; add an early guard to check if const files = e.target.files is null or has length 0 and return early before calling fileUploadHandler, then pass Array.from(files) without the non-null assertion; keep the existing editor.update block and use nodeKey, editor and uploader.upload as before.packages/koenig-lexical/demo/utils/fetchEmbed.ts-3-6 (1)
3-6:⚠️ Potential issue | 🟡 MinorDead code:
new URL()throws on invalid input, never returns falsy.The check
if (!urlObject)is unreachable becausenew URL(url)either succeeds and returns a validURLobject, or throws aTypeErrorfor invalid URLs. The error should be caught in the existing try-catch instead.Suggested fix
export async function fetchEmbed(url: string, {type}: {type: string}) { console.log('fetchEmbed', {url, type}); - const urlObject = new URL(url); - if (!urlObject) { - throw new Error('No URL specified.'); - } await delay(process.env.NODE_ENV === 'test' ? 50 : 1500); try { + const urlObject = new URL(url); if (type === 'bookmark') {Or wrap URL parsing in try-catch if you want a custom error message.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/koenig-lexical/demo/utils/fetchEmbed.ts` around lines 3 - 6, The null-check after constructing the URL is dead code because new URL(url) throws on invalid input; remove the unreachable if (!urlObject) branch in fetchEmbed (or the function containing const urlObject = new URL(url)) and rely on the existing try-catch to handle invalid URLs, or explicitly wrap the URL construction in its own try-catch to throw a custom error message before proceeding with the rest of the function.packages/koenig-lexical/src/components/ui/TabView.tsx-14-15 (1)
14-15:⚠️ Potential issue | 🟡 MinorGuard the initial tab selection.
tabs[0].idwill throw whentabsis empty, and an unknowndefaultTableaves the panel with no matching content. Validate the initial id before seeding state and bail out gracefully when there are no tabs.💡 Suggested fix
const TabView = ({tabs, defaultTab, tabContent}: TabViewProps) => { - const [activeTab, setActiveTab] = useState(defaultTab || tabs[0].id); + const initialTab = tabs.find(tab => tab.id === defaultTab)?.id ?? tabs[0]?.id; + + if (!initialTab) { + return null; + } + + const [activeTab, setActiveTab] = useState(initialTab);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/koenig-lexical/src/components/ui/TabView.tsx` around lines 14 - 15, The TabView component seeds its state with defaultTab or tabs[0].id which crashes when tabs is empty or selects a defaultTab that doesn't exist; update TabView (props TabViewProps, variables activeTab and setActiveTab, and rendering that uses tabContent) to first validate tabs is non-empty and that defaultTab exists in tabs (e.g., find by id), initialize activeTab to a safe value (the validated defaultTab id or the first tab's id) and if no tabs exist return early/empty UI to bail out gracefully so the panel never tries to render missing content.packages/koenig-lexical/src/nodes/header/v2/HeaderNodeComponent.tsx-143-152 (1)
143-152:⚠️ Potential issue | 🟡 MinorValidate
filesbefore clearing the current image.This handler accepts
null, butArray.from(files!)still assumes a value is present. If a caller passesnullor an empty selection here, the current background image is cleared before the upload path fails.💡 Suggested fix
const handleImageChange = async (files: File[] | FileList | null) => { + if (!files || files.length === 0) { + return; + } + // reset original src so it can be replaced with preview and upload progress editor.update(() => { const node = $getHeaderNodeByKey(nodeKey); if (!node) {return;} node.backgroundImageSrc = ''; }); - const result = await backgroundImageUploadHandler(Array.from(files!), imageUploader.upload); + const result = await backgroundImageUploadHandler(Array.from(files), imageUploader.upload);🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/koenig-lexical/src/nodes/header/v2/HeaderNodeComponent.tsx` around lines 143 - 152, The handler handleImageChange clears the header node's backgroundImageSrc before validating files and may wipe the image when files is null/empty; first validate that files is non-null and has at least one file (and/or FileList.length > 0) and return early if not, then proceed to call backgroundImageUploadHandler and only update node.backgroundImageSrc/preview/upload progress after a successful files array conversion and upload start; locate the logic in HeaderNodeComponent's handleImageChange and move the editor.update that clears backgroundImageSrc to after the files checks (or guard it with a check on files.length) so the image is not cleared on null/empty input.packages/koenig-lexical/src/nodes/ImageNodeComponent.tsx-166-177 (1)
166-177:⚠️ Potential issue | 🟡 MinorBail out before blanking the node when no files were selected.
e.target.filesis nullable here, but the code clearsnode.srcbefore passingfiles!into the upload handler. A null or empty selection path will leave the current image blank with no replacement.💡 Suggested fix
const onFileChange = async (e: React.ChangeEvent<HTMLInputElement>) => { const files = e.target.files; + + if (!files || files.length === 0) { + return; + } // reset original src so it can be replaced with preview and upload progress editor.update(() => { const node = $getImageNodeByKey(nodeKey); if (!node) {return;} if (!node) {return;} node.src = ''; }); - return await imageUploadHandler(files!, nodeKey, editor, imageUploader.upload); + return await imageUploadHandler(files, nodeKey, editor, imageUploader.upload); };🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/koenig-lexical/src/nodes/ImageNodeComponent.tsx` around lines 166 - 177, The onFileChange handler clears node.src before verifying files exist, which blanks the image when no file was selected; modify onFileChange to check e.target.files (and files.length) and return early if null/empty before calling editor.update or blanking the node, then only call imageUploadHandler(files, nodeKey, editor, imageUploader.upload) when files are present; reference the onFileChange function, $getImageNodeByKey, node.src, imageUploadHandler and imageUploader.upload to locate where to add the early-return guard.packages/koenig-lexical/src/components/ui/cards/CodeBlockCard.tsx-197-213 (1)
197-213:⚠️ Potential issue | 🟡 MinorHide the language input semantically, not just visually.
When
showLanguageis false the field only getsopacity-0, so it can still take focus and intercept clicks in the editor’s top-right corner. Making the wrapperrelativeand disabling pointer/tab interaction when hidden avoids that.💡 Suggested fix
- <div className="not-kg-prose min-h-[170px]"> + <div className="not-kg-prose relative min-h-[170px]"> <CodeMirror autoFocus={true} // autofocus the editor whenever it is rendered basicSetup={false} // basic setup includes unnecessary extensions extensions={extensions} value={code} @@ <input + aria-hidden={!showLanguage} aria-label="Code card language" - className={`z-999 absolute right-1.5 top-1.5 w-1/5 rounded-md border border-grey-300 px-2 py-1 font-sans text-[1.3rem] leading-4 text-grey-900 transition-opacity focus-visible:outline-none dark:border-grey-900 dark:text-grey-400 ${showLanguage ? 'opacity-100' : 'opacity-0'}`} + className={`z-999 absolute right-1.5 top-1.5 w-1/5 rounded-md border border-grey-300 px-2 py-1 font-sans text-[1.3rem] leading-4 text-grey-900 transition-opacity focus-visible:outline-none dark:border-grey-900 dark:text-grey-400 ${showLanguage ? 'opacity-100' : 'pointer-events-none opacity-0'}`} data-testid="code-card-language" placeholder="Language..." + tabIndex={showLanguage ? 0 : -1} type="text" value={language} onChange={onLanguageChange} />🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/koenig-lexical/src/components/ui/cards/CodeBlockCard.tsx` around lines 197 - 213, The language input is only visually hidden via opacity, so when showLanguage is false it still receives focus and intercepts clicks; update the CodeBlockCard component so the wrapper div (the element with className "not-kg-prose min-h-[170px]") is positioned relative and the input element (data-testid="code-card-language") is made non-interactive when hidden: add conditional classes/attributes such that when showLanguage is false the input gets pointer-events-none (or disabled), tabIndex={-1}, and aria-hidden="true" (and when true restore tabIndex=0 and remove aria-hidden), while keeping the existing visual opacity logic to preserve appearance. This targets the CodeBlockCard input and surrounding div to prevent the input from stealing focus/clicks when hidden.packages/koenig-lexical/src/components/ui/KeyboardSelection.tsx-39-43 (1)
39-43:⚠️ Potential issue | 🟡 MinorPotential
undefinedpassed toonSelectwhen items is empty.If
itemsis an empty array,items[selectedIndex]will beundefined, butonSelectexpectsunknown(implying a valid item). Consider guarding against empty arrays.Proposed fix
if (event.key === 'Enter') { event.preventDefault(); event.stopPropagation(); - onSelect(items[selectedIndex]); + if (items.length > 0) { + onSelect(items[selectedIndex]); + } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/koenig-lexical/src/components/ui/KeyboardSelection.tsx` around lines 39 - 43, The Enter key handler in KeyboardSelection.tsx may call onSelect(items[selectedIndex]) when items is empty or selectedIndex is out of range; update the handler in the component (the block that checks if (event.key === 'Enter')) to first verify items.length > 0 and that selectedIndex is a valid index (e.g., selectedIndex >= 0 && selectedIndex < items.length) before calling onSelect, and bail out (or optionally call a safe fallback) if the check fails so undefined is never passed to onSelect.packages/koenig-lexical/demo/RestrictedContentDemo.tsx-89-89 (1)
89-89:⚠️ Potential issue | 🟡 MinorNon-null assertion on ref may cause runtime error.
containerRef.current!assumes the ref is always attached, but if this code runs before the ref is set or after unmount, it will throw. Consider adding a null check.Proposed fix
//scroll to the bottom of the container - containerRef.current!.scrollTop = containerRef.current!.scrollHeight; + if (containerRef.current) { + containerRef.current.scrollTop = containerRef.current.scrollHeight; + }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/koenig-lexical/demo/RestrictedContentDemo.tsx` at line 89, The current assignment uses a non-null assertion on containerRef.current (in RestrictedContentDemo) which can throw if the ref isn't attached; update the code that sets containerRef.current!.scrollTop = containerRef.current!.scrollHeight to first guard that containerRef.current exists (e.g., retrieve into a local const el = containerRef.current and return or skip if el is null) and only then set el.scrollTop = el.scrollHeight; this ensures safe access to scrollTop/scrollHeight and avoids runtime errors when the ref is undefined or after unmount.packages/koenig-lexical/src/components/ui/FloatingLinkToolbar.tsx-83-84 (1)
83-84:⚠️ Potential issue | 🟡 MinorSelection set even when range is invalid.
$setSelection(selection)andonEditLink({href})are called unconditionally after theifblock. IffirstChild/lastChildare null or not TextNodes, an empty/invalid selection is set andonEditLinkis still invoked, which may cause unexpected behavior.Proposed fix to only proceed when selection is valid
const firstChild = linkNode.getFirstChild(); const lastChild = linkNode.getLastChild(); - if (firstChild && lastChild) { + if (firstChild && lastChild && $isTextNode(firstChild) && $isTextNode(lastChild)) { selection.setTextNodeRange( firstChild, 0, lastChild, lastChild.getTextContentSize() ); + $setSelection(selection); + onEditLink({href}); } - $setSelection(selection); - onEditLink({href}); }); };🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/koenig-lexical/src/components/ui/FloatingLinkToolbar.tsx` around lines 83 - 84, The code currently calls $setSelection(selection) and onEditLink({href}) unconditionally which can set an invalid selection when firstChild/lastChild are null or not TextNodes; update the logic in FloatingLinkToolbar so you only call $setSelection(selection) and onEditLink({href}) when the computed selection is valid (i.e., firstChild and lastChild exist and are TextNodes and the selection range is non-empty/valid). Concretely, add a guard around the existing calls (or move them inside the existing if block that verifies firstChild/lastChild), referencing the existing variables/operations: firstChild, lastChild, selection, $setSelection, and onEditLink to ensure those functions are invoked only after validation.packages/koenig-lexical/src/components/ui/FloatingLinkToolbar.tsx-73-82 (1)
73-82:⚠️ Potential issue | 🟡 MinorAdd type guards for
setTextNodeRangearguments.
linkNode.getFirstChild()andgetLastChild()returnLexicalNode | null, butsetTextNodeRangeexpectsTextNodearguments. If a link contains non-text content, this will cause a runtime error. Add$isTextNodeguards before callingsetTextNodeRange:Proposed fix
+import {$isTextNode} from 'lexical'; ... const firstChild = linkNode.getFirstChild(); const lastChild = linkNode.getLastChild(); - if (firstChild && lastChild) { + if (firstChild && lastChild && $isTextNode(firstChild) && $isTextNode(lastChild)) { selection.setTextNodeRange( firstChild, 0, lastChild, lastChild.getTextContentSize() ); }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/koenig-lexical/src/components/ui/FloatingLinkToolbar.tsx` around lines 73 - 82, The code calls selection.setTextNodeRange using linkNode.getFirstChild() and getLastChild() which return LexicalNode|null but setTextNodeRange requires TextNode; add type guards using $isTextNode to ensure both firstChild and lastChild are TextNode before calling selection.setTextNodeRange (i.e., check $isTextNode(firstChild) && $isTextNode(lastChild) and only then pass them to selection.setTextNodeRange), so non-text children inside the link won't cause a runtime error.packages/koenig-lexical/src/components/ui/cards/SignupCard.tsx-520-520 (1)
520-520:⚠️ Potential issue | 🟡 MinorAdditional non-null assertions on optional props.
Similar to line 295,
onFileChange!(line 520) andhandleLabels!(line 567) use non-null assertions on optional props. These should either be made required or guarded.Also applies to: 567-567
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/koenig-lexical/src/components/ui/cards/SignupCard.tsx` at line 520, The code is using non-null assertions on optional props (onFileChange! and handleLabels!) in SignupCard; either make these props required in the SignupCardProps/type or guard their use. Fix by updating the prop type to mark onFileChange and handleLabels as required, or remove the "!" and add runtime guards where used (e.g., only pass onFileChange to the child or call handleLabels when it's defined). Reference the SignupCard component and the prop names onFileChange and handleLabels to locate and update the prop interface and all usages accordingly.packages/koenig-lexical/src/components/ui/cards/EmbedCard.tsx-117-127 (1)
117-127:⚠️ Potential issue | 🟡 MinorNon-null assertions in
handleLoadmay fail for cross-origin iframes.
iframeRef.current!.contentDocument!.bodyandiframeRef.current!.contentWindow!.documentassume the iframe's content is accessible. For cross-origin content (e.g., some embeds),contentDocumentwill benulldue to same-origin policy, causing a runtime error.🛡️ Proposed defensive guards
const handleLoad = () => { - const iframeBody = iframeRef.current!.contentDocument!.body; + const iframeBody = iframeRef.current?.contentDocument?.body; + if (!iframeBody) { + return; + } // apply styles iframeBody.style.display = 'flex'; iframeBody.style.margin = '0'; iframeBody.style.justifyContent = 'center'; // resize first load handleResize(); // start listening to mutations when the iframe content is loaded - mutationObserver.observe(iframeRef.current!.contentWindow!.document, config); + const contentDoc = iframeRef.current?.contentWindow?.document; + if (contentDoc) { + mutationObserver.observe(contentDoc, config); + } };🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/koenig-lexical/src/components/ui/cards/EmbedCard.tsx` around lines 117 - 127, The handleLoad function uses non-null assertions on iframeRef.current!.contentDocument!.body and iframeRef.current!.contentWindow!.document which will throw for cross-origin embeds; update handleLoad to defensively check iframeRef.current exists and that iframeRef.current.contentDocument and iframeRef.current.contentWindow are non-null and accessible before touching body styles or calling mutationObserver.observe, and only call handleResize and start observing when those checks pass (otherwise skip styling/observing for cross-origin iframes); replace uses of non-null assertions with these safe guards and bail out early if the iframe is cross-origin.packages/koenig-lexical/src/components/ui/cards/SignupCard.tsx-295-295 (1)
295-295:⚠️ Potential issue | 🟡 MinorNon-null assertion on optional
onFileChangeprop.
onFileChangeis declared as optional (onFileChange?: ...) inSignupCardProps, but it's used with a non-null assertion (onFileChange!). If the parent component doesn't passonFileChange, this will throw a runtime error.Either make
onFileChangerequired in the interface, or guard theMediaUploaderrendering with a check foronFileChange.🛡️ Option 1: Make prop required
interface SignupCardProps { // ... - onFileChange?: (e: React.ChangeEvent<HTMLInputElement>) => void; + onFileChange: (e: React.ChangeEvent<HTMLInputElement>) => void; // ... }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/koenig-lexical/src/components/ui/cards/SignupCard.tsx` at line 295, SignupCard uses the optional prop onFileChange with a non-null assertion (onFileChange!) when rendering MediaUploader, which can crash if the parent omits it; update either the SignupCardProps to make onFileChange required or (preferred) guard the MediaUploader render/prop by checking if onFileChange is defined before passing it (e.g., only render MediaUploader or pass the prop when onFileChange !== undefined) so that SignupCard and MediaUploader never receive a forced-null value.packages/koenig-lexical/src/nodes/BookmarkNodeComponent.tsx-146-149 (1)
146-149:⚠️ Potential issue | 🟡 MinorDuplicate null guard should be removed.
Same issue as above — lines 148-149 contain duplicate
if (!node) {return;}statements with inconsistent indentation on line 148.🧹 Proposed fix
editor.update(() => { const node = $getBookmarkNodeByKey(nodeKey); - if (!node) {return;} if (!node) {return;} node.url = response.url;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/koenig-lexical/src/nodes/BookmarkNodeComponent.tsx` around lines 146 - 149, In BookmarkNodeComponent.tsx inside the editor.update callback where you call $getBookmarkNodeByKey(nodeKey), remove the duplicate null-check so only a single if (!node) { return; } remains (currently duplicated on two adjacent lines) and correct the indentation to match surrounding code; locate the checks near the call to $getBookmarkNodeByKey and keep one properly indented guard before using the node.packages/koenig-lexical/src/components/KoenigComposableEditor.tsx-135-135 (1)
135-135:⚠️ Potential issue | 🟡 MinorRemove empty JSX expression.
The
{ }expression on line 135 appears to be a leftover from refactoring and serves no purpose.🧹 Proposed fix
- { } {floatingAnchorElem && (<FloatingToolbarPlugin anchorElem={floatingAnchorElem} hiddenFormats={hiddenFormats} isSnippetsEnabled={isSnippetsEnabled} />)}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/koenig-lexical/src/components/KoenigComposableEditor.tsx` at line 135, Remove the stray empty JSX expression "{ }" inside the KoenigComposableEditor component JSX (file: KoenigComposableEditor.tsx) which is a no-op leftover from refactoring; locate the JSX render/return block inside the KoenigComposableEditor function/class and delete the empty expression so the JSX tree contains only meaningful nodes and no empty expressions.packages/koenig-lexical/src/components/ui/InputList.tsx-120-123 (1)
120-123:⚠️ Potential issue | 🟡 MinorType mismatch:
onChangecalled with extra argument.When
onSelectis undefined,onChangeis called withtypedItem.typeas a second argument, butonChangeis typed as(value: string) => void. While JavaScript ignores extra arguments, this indicates a type design issue.🔧 Proposed fix
const onSelectEvent = (item: unknown) => { const typedItem = item as {value: string; type?: string}; - (onSelect || onChange)(typedItem.value, typedItem.type); + if (onSelect) { + onSelect(typedItem.value, typedItem.type); + } else { + onChange(typedItem.value); + } };🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/koenig-lexical/src/components/ui/InputList.tsx` around lines 120 - 123, onSelectEvent currently calls (onSelect || onChange)(typedItem.value, typedItem.type) which passes a second arg to onChange even though onChange is typed as (value: string) => void; change the call so it respects each handler's signature: if onSelect is defined call onSelect(typedItem.value, typedItem.type), otherwise call onChange(typedItem.value). Update the onSelectEvent implementation to explicitly branch on onSelect rather than using the || shorthand to avoid the type mismatch (referencing onSelectEvent, onSelect, onChange, and typedItem).packages/koenig-lexical/src/nodes/BookmarkNodeComponent.tsx-120-123 (1)
120-123:⚠️ Potential issue | 🟡 MinorDuplicate null guard should be removed.
Lines 122-123 contain two identical
if (!node) {return;}statements. The second check is redundant and appears to be a copy-paste error. Additionally, line 122 has inconsistent indentation (3 spaces instead of 4).🧹 Proposed fix
editor.update(() => { const node = $getBookmarkNodeByKey(nodeKey); - if (!node) {return;} if (!node) {return;} node.url = href;🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/koenig-lexical/src/nodes/BookmarkNodeComponent.tsx` around lines 120 - 123, Remove the duplicate null guard inside the editor.update callback: keep a single check after obtaining const node = $getBookmarkNodeByKey(nodeKey) and delete the redundant if (!node) { return; } that follows it; also correct the indentation of the remaining guard to match surrounding code (use 4 spaces). This change affects the editor.update(...) callback in BookmarkNodeComponent.tsx where $getBookmarkNodeByKey and node are used.packages/koenig-lexical/src/components/KoenigComposableEditor.tsx-87-88 (1)
87-88:⚠️ Potential issue | 🟡 MinorType mismatch: passing serialized JSON where EditorState is expected.
Line 88 passes
json(a serialized JSON object from.toJSON()) tosharedOnChange, which expects anEditorStateinstance according to theSharedOnChangeContextcallback signature. The castas unknown as import('lexical').EditorStateworks around the type mismatch but is semantically incorrect and misleading. The callback signature should accept the actual JSON type being passed.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/koenig-lexical/src/components/KoenigComposableEditor.tsx` around lines 87 - 88, The code is serializing primaryEditorState via primaryEditorState.toJSON() and casting that JSON to import('lexical').EditorState when calling sharedOnChange, which is a type mismatch; either pass the real EditorState instance to sharedOnChange or change the SharedOnChangeContext callback type to accept the serialized shape. Fix by updating the sharedOnChange/SharedOnChangeContext signature to accept the JSON type returned by primaryEditorState.toJSON() (e.g., the concrete serialized editor state type or a Record<string, unknown>/SerializedEditorState alias) and adjust all call sites (including the call in KoenigComposableEditor where you currently call sharedOnChange(json, editor, new Set<string>())) to use that new serialized type, or alternatively stop calling .toJSON() and pass the actual EditorState object into sharedOnChange so the existing EditorState type remains correct.packages/koenig-lexical/src/components/ui/SettingsPanel.tsx-373-377 (1)
373-377:⚠️ Potential issue | 🟡 Minor
dataTestIdexists in props but is not wired inMediaUploadSetting.
MediaUploadSettingPropsincludesdataTestId(Line 376), but Line 400 does not destructure it and the container test id is hardcoded. This makes the prop ineffective.Proposed fix
-export function MediaUploadSetting({className, imgClassName, label, hideLabel, onFileChange, isDraggedOver, placeholderRef, src, alt, isLoading, errors = [], progress, onRemoveMedia, icon, desc, size, type, stacked, borderStyle, mimeTypes, isPinturaEnabled, openImageEditor, setFileInputRef}: MediaUploadSettingProps) { +export function MediaUploadSetting({className, imgClassName, dataTestId, label, hideLabel, onFileChange, isDraggedOver, placeholderRef, src, alt, isLoading, errors = [], progress, onRemoveMedia, icon, desc, size, type, stacked, borderStyle, mimeTypes, isPinturaEnabled, openImageEditor, setFileInputRef}: MediaUploadSettingProps) { return ( - <div className={clsx(className, !stacked && 'flex justify-between gap-3')} data-testid="media-upload-setting"> + <div className={clsx(className, !stacked && 'flex justify-between gap-3')} data-testid={dataTestId ?? 'media-upload-setting'}>Also applies to: 400-403
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@packages/koenig-lexical/src/components/ui/SettingsPanel.tsx` around lines 373 - 377, The MediaUploadSetting component doesn't use the dataTestId prop declared in MediaUploadSettingProps; update the MediaUploadSetting function signature to destructure dataTestId (alongside className, imgClassName, label) and replace the hardcoded data-testid on the container element with the passed-in dataTestId (optionally keep a fallback string if needed) so the prop is actually wired and controls the test id.
| extends: [ | ||
| js.configs.recommended, | ||
| tseslint.configs.recommended, | ||
| reactHooks.configs['recommended-latest'] | ||
| ], |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check typescript-eslint version
fd -t f 'package.json' packages/koenig-lexical --exec cat {} | jq '.dependencies["typescript-eslint"] // .devDependencies["typescript-eslint"]'Repository: TryGhost/Koenig
Length of output: 67
🏁 Script executed:
cat -n packages/koenig-lexical/eslint.config.jsRepository: TryGhost/Koenig
Length of output: 1925
🏁 Script executed:
# Check typescript-eslint v8 documentation behavior for configs.recommended
web_search
What does tseslint.configs.recommended return in typescript-eslint v8Repository: TryGhost/Koenig
Length of output: 152
🌐 Web query:
typescript-eslint v8 tseslint.configs.recommended array flat config
💡 Result:
In typescript-eslint v8, tseslint.configs.recommended is a flat-config array, so in an eslint.config.(m)js you typically pass it to defineConfig(...) (recommended) or spread it into your exported array.
Option A (recommended): defineConfig(...)
// eslint.config.mjs
import eslint from '@eslint/js';
import { defineConfig } from 'eslint/config';
import tseslint from 'typescript-eslint';
export default defineConfig(
eslint.configs.recommended,
tseslint.configs.recommended,
);[1]
Option B: export a flat-config array (note the spread)
// eslint.config.mjs
import eslint from '@eslint/js';
import tseslint from 'typescript-eslint';
export default [
eslint.configs.recommended,
...tseslint.configs.recommended,
];[2]
If you want “recommended” to apply only to TS files
Using tseslint.config(...) (their helper) lets you attach files + extends cleanly:
import eslint from '@eslint/js';
import tseslint from 'typescript-eslint';
export default tseslint.config({
files: ['**/*.{ts,tsx,mts,cts}'],
extends: [eslint.configs.recommended, tseslint.configs.recommended],
});[2]
Sources: typescript-eslint “Getting Started” flat config quickstart [1], typescript-eslint package docs (flat config array + spread + tseslint.config helper) [2], shared configs (recommended) docs [3].
[1] https://typescript-eslint.io/getting-started/
[2] https://typescript-eslint.io/packages/typescript-eslint/
[3] https://typescript-eslint.io/users/configs/
🏁 Script executed:
# Check if there's a way to test the eslint config in the repo
cd packages/koenig-lexical && ls -la | head -20Repository: TryGhost/Koenig
Length of output: 1236
🏁 Script executed:
# Check package.json for test/lint scripts
cat packages/koenig-lexical/package.json | grep -A 5 '"scripts"'Repository: TryGhost/Koenig
Length of output: 526
🏁 Script executed:
# Check ESLint flat config spec to understand if extends array can handle nested arrays
cat packages/koenig-lexical/package.json | jq '.devDependencies | keys[] | select(. | contains("eslint"))'Repository: TryGhost/Koenig
Length of output: 212
🏁 Script executed:
# Try to see if there are any build or lint errors documented
cd packages/koenig-lexical && cat package.json | jq '.devDependencies'Repository: TryGhost/Koenig
Length of output: 2721
Spread tseslint.configs.recommended in the extends array.
In typescript-eslint v8, tseslint.configs.recommended is a flat-config array. Placing it directly in the extends array creates a nested array structure, which is invalid. Use the spread operator instead:
extends: [
js.configs.recommended,
...tseslint.configs.recommended,
reactHooks.configs['recommended-latest']
],
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/koenig-lexical/eslint.config.js` around lines 12 - 16, The extends
array currently includes tseslint.configs.recommended directly which yields a
nested array; update the extends entry in eslint.config.js so you spread
tseslint.configs.recommended into the array (use
...tseslint.configs.recommended) alongside js.configs.recommended and
reactHooks.configs['recommended-latest'] to produce a flat extends array.
| const getItem = (item: unknown, selected: boolean, onMouseOver: () => void, scrollIntoView: boolean) => { | ||
| const typedItem = item as {value: string; label: string; highlight?: boolean; Icon?: React.ComponentType; MetaIcon?: React.ComponentType; metaText?: string; metaIconTitle?: string}; | ||
| return ( | ||
| <LinkInputSearchItem | ||
| key={item.value} | ||
| key={typedItem.value} | ||
| dataTestId={testId} | ||
| highlightString={query} | ||
| item={item} | ||
| item={typedItem} | ||
| scrollIntoView={scrollIntoView} | ||
| selected={selected} | ||
| onClick={onSelect} | ||
| onClick={onSelect as () => void} | ||
| onMouseOver={onMouseOver} | ||
| /> | ||
| ); | ||
| }; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Description: Verify LinkInputSearchItem's onClick signature to confirm the fix
ast-grep --pattern 'function LinkInputSearchItem($$$) {
$$$
}'
rg -n 'onClick' packages/koenig-lexical/src/components/ui/LinkInputSearchItem.tsx -A2 -B2Repository: TryGhost/Koenig
Length of output: 3825
🏁 Script executed:
#!/bin/bash
# Find InputListItem definition and see how it handles onClick
rg -n 'InputListItem' packages/koenig-lexical/src/components/ui/ -l
fd -t f 'InputListItem' packages/koenig-lexical/src/
# Search for InputListItem interface/component definition
rg -B5 -A15 'interface InputListItemProps|type InputListItemProps|export.*InputListItem' packages/koenig-lexical/src/ -g '*.tsx' -g '*.ts'Repository: TryGhost/Koenig
Length of output: 3234
🏁 Script executed:
#!/bin/bash
# Search for all uses of LinkInputSearchItem to understand if the () => void signature is intentional
rg -n 'LinkInputSearchItem' packages/koenig-lexical/src/ -g '*.tsx' -g '*.ts' --type-add 'tsx:*.tsx' --type-add 'ts:*.ts'
# Also check the LinkInputSearchItem props definition specifically
rg -B2 -A8 'interface LinkInputSearchItemProps' packages/koenig-lexical/src/components/ui/LinkInputSearchItem.tsxRepository: TryGhost/Koenig
Length of output: 1424
🏁 Script executed:
#!/bin/bash
# Check how UrlSearchInput uses LinkInputSearchItem
rg -B5 -A10 '<LinkInputSearchItem' packages/koenig-lexical/src/components/ui/UrlSearchInput.tsx
# Check how LinkInputWithSearch uses LinkInputSearchItem
rg -B5 -A10 '<LinkInputSearchItem' packages/koenig-lexical/src/components/ui/LinkInputWithSearch.tsxRepository: TryGhost/Koenig
Length of output: 1228
🏁 Script executed:
#!/bin/bash
# Get the full LinkInputSearchItem interface definition
rg -B2 -A15 'interface LinkInputSearchItemProps' packages/koenig-lexical/src/components/ui/LinkInputSearchItem.tsx -A 20Repository: TryGhost/Koenig
Length of output: 792
Fix onClick handler to properly receive the item parameter.
LinkInputSearchItemProps incorrectly defines onClick?: () => void while InputListItem (which it wraps) expects onClick?: (item: unknown) => void and calls it with the item. The cast onSelect as () => void masks this type mismatch. The proposed fix using onClick={() => onSelect(typedItem)} correctly captures and passes the item to the handler.
Note: This type mismatch affects all three callers of LinkInputSearchItem (AtLinkResultsPopup, UrlSearchInput, LinkInputWithSearch), suggesting the root cause is LinkInputSearchItemProps.onClick being incorrectly typed. Ideally, the prop type should be fixed to onClick?: (item: unknown) => void to resolve this systematically across all usage sites.
🐛 Proposed fix
return (
<LinkInputSearchItem
key={typedItem.value}
dataTestId={testId}
highlightString={query}
item={typedItem}
scrollIntoView={scrollIntoView}
selected={selected}
- onClick={onSelect as () => void}
+ onClick={() => onSelect(typedItem)}
onMouseOver={onMouseOver}
/>
);📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| const getItem = (item: unknown, selected: boolean, onMouseOver: () => void, scrollIntoView: boolean) => { | |
| const typedItem = item as {value: string; label: string; highlight?: boolean; Icon?: React.ComponentType; MetaIcon?: React.ComponentType; metaText?: string; metaIconTitle?: string}; | |
| return ( | |
| <LinkInputSearchItem | |
| key={item.value} | |
| key={typedItem.value} | |
| dataTestId={testId} | |
| highlightString={query} | |
| item={item} | |
| item={typedItem} | |
| scrollIntoView={scrollIntoView} | |
| selected={selected} | |
| onClick={onSelect} | |
| onClick={onSelect as () => void} | |
| onMouseOver={onMouseOver} | |
| /> | |
| ); | |
| }; | |
| const getItem = (item: unknown, selected: boolean, onMouseOver: () => void, scrollIntoView: boolean) => { | |
| const typedItem = item as {value: string; label: string; highlight?: boolean; Icon?: React.ComponentType; MetaIcon?: React.ComponentType; metaText?: string; metaIconTitle?: string}; | |
| return ( | |
| <LinkInputSearchItem | |
| key={typedItem.value} | |
| dataTestId={testId} | |
| highlightString={query} | |
| item={typedItem} | |
| scrollIntoView={scrollIntoView} | |
| selected={selected} | |
| onClick={() => onSelect(typedItem)} | |
| onMouseOver={onMouseOver} | |
| /> | |
| ); | |
| }; |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/koenig-lexical/src/components/ui/AtLinkResultsPopup.tsx` around
lines 110 - 124, The onClick handler is currently mismatched:
LinkInputSearchItemProps defines onClick?: () => void but InputListItem and
callers expect onClick?: (item: unknown) => void, and getItem in
AtLinkResultsPopup casts onSelect to () => void; fix by updating the prop type
on LinkInputSearchItemProps to onClick?: (item: unknown) => void (so
UrlSearchInput, LinkInputWithSearch and AtLinkResultsPopup align) and update
getItem (in AtLinkResultsPopup) to pass the selected item into the handler
(e.g., call onSelect(typedItem) via an arrow) instead of casting onSelect;
ensure LinkInputSearchItem and its uses accept and forward the item parameter
correctly.
| onFileChange?: (e: React.ChangeEvent<HTMLInputElement>) => void; | ||
| setFileInputRef: (ref: React.RefObject<HTMLInputElement | null>) => void; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
rg -n -C3 '\bonFileChange\b' \
packages/koenig-lexical/src/components/ui/AudioUploadForm.tsx \
packages/koenig-lexical/src/components/ui/ImageUploadForm.tsx \
packages/koenig-lexical/src/components/ui/cards/AudioCard.tsxRepository: TryGhost/Koenig
Length of output: 11169
Make onFileChange handlers required in component props interfaces.
Lines 45, 98, 194 declare onFileChange as optional (?), but lines 84 and 177 use non-null assertions (!) when passing to AudioUploadForm. Both AudioUploadForm (line 4 of AudioUploadForm.tsx) and ImageUploadForm (line 4 of ImageUploadForm.tsx) require this handler—they invoke it directly in the form's onChange without guards. If the handler is omitted, file selection will crash at runtime. Make these handlers required in the prop interfaces instead of using type assertions.
Proposed fix
interface EmptyAudioCardProps {
audioUploader: FileUploader;
audioMimeTypes?: string[];
- onFileChange?: (e: React.ChangeEvent<HTMLInputElement>) => void;
+ onFileChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
setFileInputRef: (ref: React.RefObject<HTMLInputElement | null>) => void;
audioDragHandler?: DragHandler;
}
interface AudioThumbnailProps {
@@
- onFileChange?: (e: React.ChangeEvent<HTMLInputElement>) => void;
+ onFileChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
@@
interface AudioCardProps {
@@
- onAudioFileChange?: (e: React.ChangeEvent<HTMLInputElement>) => void;
- onThumbnailFileChange?: (e: React.ChangeEvent<HTMLInputElement>) => void;
+ onAudioFileChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
+ onThumbnailFileChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
@@
- onFileChange={onFileChange!}
+ onFileChange={onFileChange}
@@
- onFileChange={onFileChange!}
+ onFileChange={onFileChange}Also applies to: 84-85, 98-99, 177-178, 278-279
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/koenig-lexical/src/components/ui/cards/AudioCard.tsx` around lines
45 - 46, The onFileChange prop is declared optional but later passed with
non-null assertions to AudioUploadForm and ImageUploadForm, which call it
unguarded; remove the optionality by making onFileChange required in the
component props interfaces (replace onFileChange?: ... with onFileChange: ...)
for the relevant components and update any related prop types (e.g., where
setFileInputRef is declared) and then remove the non-null assertion operators
(!) where onFileChange is passed into AudioUploadForm and ImageUploadForm so the
types align and the handler is guaranteed present.
| interface ColorPickerSettingProps { | ||
| label: string; | ||
| isExpanded?: boolean; | ||
| onSwatchChange: (value: string) => void; | ||
| onPickerChange: (value: string) => void; | ||
| onTogglePicker: (expanded: boolean) => void; | ||
| value?: string; | ||
| swatches: {hex?: string; accent?: boolean; transparent?: boolean; image?: boolean; title: string; customContent?: React.ReactNode}[]; | ||
| eyedropper?: boolean; | ||
| hasTransparentOption?: boolean; | ||
| dataTestId?: string; | ||
| children?: React.ReactNode; | ||
| showChildren?: boolean; | ||
| } |
There was a problem hiding this comment.
showChildren is declared but ignored in ColorPickerSetting.
Line 341 exposes showChildren, but the component never reads it and always renders children (Line 365). That’s an API/behavior mismatch and can regress callers relying on hidden children state.
Proposed fix
-export function ColorPickerSetting({label, isExpanded, onSwatchChange, onPickerChange, onTogglePicker, value, swatches, eyedropper, hasTransparentOption, dataTestId, children}: ColorPickerSettingProps) {
+export function ColorPickerSetting({label, isExpanded, onSwatchChange, onPickerChange, onTogglePicker, value, swatches, eyedropper, hasTransparentOption, dataTestId, children, showChildren = true}: ColorPickerSettingProps) {
...
- {children}
+ {showChildren ? children : null}
</ColorIndicator>Also applies to: 344-366
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/koenig-lexical/src/components/ui/SettingsPanel.tsx` around lines 329
- 342, The prop showChildren on ColorPickerSettingProps is declared but never
used by the ColorPickerSetting component; update the ColorPickerSetting
component to conditionally render the children based on the showChildren prop
(falling back to current behavior when showChildren is undefined), i.e., change
the unconditional children render to something like: if showChildren is true
render children, if false do not render, and if undefined preserve existing
behavior; ensure you reference the prop name showChildren in the component’s
parameter destructuring and use it where children are rendered so callers can
control visibility.
| import {Tooltip} from './Tooltip'; | ||
|
|
||
| export const TOOLBAR_ICONS = { | ||
| export const TOOLBAR_ICONS: Record<string, React.ComponentType<React.SVGProps<SVGSVGElement>>> = { |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Verify all ToolbarMenuItem icon callsites and compare against TOOLBAR_ICONS keys.
rg -nP --type=tsx '<ToolbarMenuItem\b[^>]*\bicon=' -C2
rg -nP --type=tsx 'TOOLBAR_ICONS\s*=\s*\{' -A40Repository: TryGhost/Koenig
Length of output: 114
🏁 Script executed:
#!/bin/bash
# Read the ToolbarMenu.tsx file to verify the current state
cat -n packages/koenig-lexical/src/components/ui/ToolbarMenu.tsxRepository: TryGhost/Koenig
Length of output: 4690
🏁 Script executed:
#!/bin/bash
# Search for ToolbarMenuItem usage to verify icon values
rg 'ToolbarMenuItem' --max-count=20 -B2 -A2 packages/koenig-lexical/Repository: TryGhost/Koenig
Length of output: 42422
🏁 Script executed:
#!/bin/bash
# Check FormatToolbar.tsx for ToolbarMenuItem icon props
cat packages/koenig-lexical/src/components/ui/FormatToolbar.tsx | head -100Repository: TryGhost/Koenig
Length of output: 2938
🏁 Script executed:
#!/bin/bash
# Check FormatToolbar.tsx for ToolbarMenuItem icon props - full component render section
sed -n '100,200p' packages/koenig-lexical/src/components/ui/FormatToolbar.tsxRepository: TryGhost/Koenig
Length of output: 3455
🏁 Script executed:
#!/bin/bash
# Check FormatToolbar.tsx for all ToolbarMenuItem icon props - continue reading
sed -n '180,250p' packages/koenig-lexical/src/components/ui/FormatToolbar.tsxRepository: TryGhost/Koenig
Length of output: 2766
🏁 Script executed:
#!/bin/bash
# Search for all icon prop assignments to ToolbarMenuItem to ensure all are valid
rg 'icon=' packages/koenig-lexical/src --type ts --type tsx -o | grep -oP 'icon="\K[^"]+|icon=\{\K[^}]+(?=\})' | sort | uniqRepository: TryGhost/Koenig
Length of output: 86
🏁 Script executed:
#!/bin/bash
# Search for all icon prop assignments more reliably
rg 'icon=' packages/koenig-lexical/src -A0 | grep -oP 'icon="\K[^"]+|icon=\{[^}]+\}' | sort | uniqRepository: TryGhost/Koenig
Length of output: 319
🏁 Script executed:
#!/bin/bash
# Find the exact files using invalid icon values
rg 'icon="(download|heart)"' packages/koenig-lexical/src -B3 -A1Repository: TryGhost/Koenig
Length of output: 1478
🏁 Script executed:
#!/bin/bash
# Check what UnsplashButton is and if it uses ToolbarMenuItem
cat packages/koenig-lexical/src/components/ui/file-selectors/Unsplash/UnsplashImage.tsx | head -40Repository: TryGhost/Koenig
Length of output: 1220
🏁 Script executed:
#!/bin/bash
# Check UnsplashButton to see if it uses ToolbarMenuItem or TOOLBAR_ICONS
cat packages/koenig-lexical/src/components/ui/file-selectors/Unsplash/UnsplashButton.tsxRepository: TryGhost/Koenig
Length of output: 1167
🏁 Script executed:
#!/bin/bash
# Check the dynamic icon assignments - find files with icon={icon} pattern
rg 'icon=\{icon\}' packages/koenig-lexical/src -B5 -A2Repository: TryGhost/Koenig
Length of output: 2650
🏁 Script executed:
#!/bin/bash
# Check if ToolbarMenuItem is used with dynamic icon values like icon={icon}
rg 'ToolbarMenuItem' packages/koenig-lexical/src -A5 | grep -B3 'icon='Repository: TryGhost/Koenig
Length of output: 20700
Constrain icon keys to prevent undefined component renders
Line 77 can render undefined if icon is not in TOOLBAR_ICONS. Typing icon as string and TOOLBAR_ICONS as Record<string, ...> allows any string key at compile time despite only specific icons being available.
Suggested fix
-export const TOOLBAR_ICONS: Record<string, React.ComponentType<React.SVGProps<SVGSVGElement>>> = {
+export const TOOLBAR_ICONS = {
bold: BoldIcon,
italic: ItalicIcon,
headingTwo: HeadingTwoIcon,
headingThree: HeadingThreeIcon,
quote: QuoteIcon,
quoteOne: QuoteOneIcon,
quoteTwo: QuoteTwoIcon,
link: LinkIcon,
imgRegular: ImgRegularIcon,
imgWide: ImgWideIcon,
imgFull: ImgFullIcon,
imgReplace: ImgReplaceIcon,
add: AddIcon,
edit: EditIcon,
wand: WandIcon,
visibility: EyeIcon,
snippet: SnippetIcon,
remove: TrashIcon
-};
+} as const satisfies Record<string, React.ComponentType<React.SVGProps<SVGSVGElement>>>;
+
+type ToolbarIconName = keyof typeof TOOLBAR_ICONS;
@@
- icon: string;
+ icon: ToolbarIconName;Also applies to: 64, 77
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/koenig-lexical/src/components/ui/ToolbarMenu.tsx` at line 21, The
icon lookup can return undefined because TOOLBAR_ICONS and the icon prop are
typed as unrestricted strings; define a narrow union type (e.g., type
ToolbarIconKey = keyof typeof TOOLBAR_ICONS or an explicit union of allowed
names) and update TOOLBAR_ICONS to be Record<ToolbarIconKey,
React.ComponentType<...>> and the icon prop/type usage (the prop named icon
where the map lookup occurs) to use ToolbarIconKey so the compiler prevents
unknown keys and eliminates potential undefined renders for TOOLBAR_ICONS and
the icon lookup expression.
| setupNestedEditor(this, '__callToActionHtmlEditor', {editor: dataset.callToActionHtmlEditor as LexicalEditor | undefined, nodes: BASIC_NODES}); | ||
| setupNestedEditor(this, '__sponsorLabelHtmlEditor', {editor: dataset.sponsorLabelHtmlEditor as LexicalEditor | undefined, nodes: BASIC_NODES}); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# 1) Inspect setupNestedEditor definition and expected editor shape
rg -n -C3 --type=ts --type=tsx '\bsetupNestedEditor\b'
# 2) Find all places where unknown values are asserted to LexicalEditor
rg -n -C2 --type=ts --type=tsx 'as LexicalEditor \| undefined'
# 3) Trace CallToActionNode creation paths and dataset origins
rg -n -C3 --type=ts --type=tsx '\$createCallToActionNode\(|new CallToActionNode\(|INSERT_CALL_TO_ACTION_COMMAND'Repository: TryGhost/Koenig
Length of output: 86
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# 1) Inspect setupNestedEditor definition and expected editor shape
rg -n -C3 'setupNestedEditor' --glob '*.ts' --glob '*.tsx'
# 2) Find all places where unknown values are asserted to LexicalEditor
rg -n -C2 'as LexicalEditor' --glob '*.ts' --glob '*.tsx'
# 3) Trace CallToActionNode creation paths and dataset origins
rg -n -C3 '\$createCallToActionNode|new CallToActionNode|INSERT_CALL_TO_ACTION_COMMAND' --glob '*.ts' --glob '*.tsx'Repository: TryGhost/Koenig
Length of output: 35075
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Check CallToActionPlugin to see what calls $createCallToActionNode
cat -n packages/koenig-lexical/src/plugins/CallToActionPlugin.tsx | head -30
# Check if there's any validation in CallToActionNode constructor before setupNestedEditor
cat -n packages/koenig-lexical/src/nodes/CallToActionNode.tsx | head -80
# Check the SetupNestedEditorOptions type definition
rg -n -B5 -A10 'interface SetupNestedEditorOptions' --glob '*.ts' --glob '*.tsx'
# Check if dataset is validated/typed anywhere
rg -n 'createCallToActionNode\|INSERT_CALL_TO_ACTION_COMMAND' --glob '*.ts' --glob '*.tsx' | head -20Repository: TryGhost/Koenig
Length of output: 6662
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Get the full setupNestedEditor implementation
cat -n packages/koenig-lexical/src/utils/nested-editors.ts
# Also check populateNestedEditor which is called on line 65-68 of CallToActionNodeRepository: TryGhost/Koenig
Length of output: 2661
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Check how CallToActionNode instances are created/restored
# Look for deserialization or fromJSON patterns
rg -n 'fromJSON|deserialize|restore' packages/koenig-lexical/src/nodes/CallToActionNode.tsx
# Check what BaseCallToActionNode does in constructor
rg -n -A20 'class.*BaseCallToActionNode' --glob '*.ts' --glob '*.tsx' | head -40
# Check if there's any test data or mocking that might show invalid dataset usage
fd '*.test.*' packages/koenig-lexical/src | head -5
fd '*.spec.*' packages/koenig-lexical/src | head -5
# See if there are other places that dispatch INSERT_CALL_TO_ACTION_COMMAND
rg -n 'INSERT_CALL_TO_ACTION_COMMAND' --glob '*.ts' --glob '*.tsx' | grep -v 'import\|export'Repository: TryGhost/Koenig
Length of output: 41
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Check base class implementation
rg -n 'class CallToActionNode' packages/koenig-lexical/src/nodes/CallToActionNode.tsx -A 5
# Check for fromJSON method anywhere in the codebase
rg -n 'fromJSON' packages/koenig-lexical/src/ --glob '*.ts' --glob '*.tsx'
# Look at how getDataset() is used - indicates serialization/deserialization
rg -n 'getDataset\|fromJSON' packages/koenig-lexical/src/nodes/ --glob '*.ts' --glob '*.tsx' -lRepository: TryGhost/Koenig
Length of output: 332
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# Try to find test files with different patterns
find packages/koenig-lexical/src -name "*.test.*" -o -name "*.spec.*" | head -5
# Look for any externals that provide dataset to these nodes
rg -n 'callToActionHtmlEditor' packages/koenig-lexical/src --glob '*.ts' --glob '*.tsx'
# See what happens if populateNestedEditor is called with a non-LexicalEditor
cat -n packages/koenig-lexical/src/utils/nested-editors.ts | sed -n '44,60p'
# Examine CallToActionNode.getDataset to understand the inverse mapping
cat -n packages/koenig-lexical/src/nodes/CallToActionNode.tsx | sed -n '72,85p'Repository: TryGhost/Koenig
Length of output: 3236
Add runtime validation for LexicalEditor instances before passing to setupNestedEditor.
Type casting dataset.callToActionHtmlEditor as LexicalEditor | undefined only satisfies TypeScript and provides no runtime protection. If dataset contains a non-editor object that is truthy, setupNestedEditor will assign it directly (line 33 does only a truthy check), and later calls to getEditorState() (line 89) will fail at runtime.
This pattern affects multiple node files. Consider adding a type guard:
Suggested fix
+const isLexicalEditor = (value: unknown): value is LexicalEditor => {
+ return typeof value === 'object' &&
+ value !== null &&
+ 'getEditorState' in value &&
+ typeof (value as {getEditorState?: unknown}).getEditorState === 'function';
+};
+
setupNestedEditor(this, '__callToActionHtmlEditor', {
- editor: dataset.callToActionHtmlEditor as LexicalEditor | undefined,
+ editor: isLexicalEditor(dataset.callToActionHtmlEditor) ? dataset.callToActionHtmlEditor : undefined,
nodes: BASIC_NODES
});
setupNestedEditor(this, '__sponsorLabelHtmlEditor', {
- editor: dataset.sponsorLabelHtmlEditor as LexicalEditor | undefined,
+ editor: isLexicalEditor(dataset.sponsorLabelHtmlEditor) ? dataset.sponsorLabelHtmlEditor : undefined,
nodes: BASIC_NODES
});📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| setupNestedEditor(this, '__callToActionHtmlEditor', {editor: dataset.callToActionHtmlEditor as LexicalEditor | undefined, nodes: BASIC_NODES}); | |
| setupNestedEditor(this, '__sponsorLabelHtmlEditor', {editor: dataset.sponsorLabelHtmlEditor as LexicalEditor | undefined, nodes: BASIC_NODES}); | |
| const isLexicalEditor = (value: unknown): value is LexicalEditor => { | |
| return typeof value === 'object' && | |
| value !== null && | |
| 'getEditorState' in value && | |
| typeof (value as {getEditorState?: unknown}).getEditorState === 'function'; | |
| }; | |
| setupNestedEditor(this, '__callToActionHtmlEditor', {editor: isLexicalEditor(dataset.callToActionHtmlEditor) ? dataset.callToActionHtmlEditor : undefined, nodes: BASIC_NODES}); | |
| setupNestedEditor(this, '__sponsorLabelHtmlEditor', {editor: isLexicalEditor(dataset.sponsorLabelHtmlEditor) ? dataset.sponsorLabelHtmlEditor : undefined, nodes: BASIC_NODES}); |
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/koenig-lexical/src/nodes/CallToActionNode.tsx` around lines 60 - 61,
The dataset values are being blindly cast to LexicalEditor and passed into
setupNestedEditor, causing runtime failures if the object isn't a real editor;
add a runtime type guard before calling setupNestedEditor for both
dataset.callToActionHtmlEditor and dataset.sponsorLabelHtmlEditor by checking
the presence/type of an editor API (e.g., verify the object is truthy and has a
getEditorState function) and only pass it as the editor when that guard passes,
otherwise pass undefined; update the calls that reference
__callToActionHtmlEditor and __sponsorLabelHtmlEditor so setupNestedEditor
receives a validated LexicalEditor or undefined (this prevents later
getEditorState() calls from throwing).
| decorate() { | ||
| const props = this as unknown as ProductNodeProperties; |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Check BaseProductNode to see what properties are available
rg -n "class.*ProductNode" --type=ts -A 20Repository: TryGhost/Koenig
Length of output: 1802
🏁 Script executed:
# Find and examine BaseProductNode
fd -type f -name "*.tsx" -o -name "*.ts" | xargs rg -l "class.*BaseProductNode" | head -5Repository: TryGhost/Koenig
Length of output: 296
🏁 Script executed:
# Check the ProductNodeProperties interface and full context of the decorate method
cat -n packages/koenig-lexical/src/nodes/ProductNode.tsx | sed -n '1,120p'Repository: TryGhost/Koenig
Length of output: 6189
🏁 Script executed:
# Search for BaseProductNode definition across the codebase
rg -n "class.*BaseProductNode|interface.*ProductNodeProperties" --type=ts -A 10Repository: TryGhost/Koenig
Length of output: 1978
🏁 Script executed:
# Check if kg-default-nodes package is available in node_modules or dependencies
fd -type f -name "package.json" | xargs grep -l "kg-default-nodes"Repository: TryGhost/Koenig
Length of output: 231
🏁 Script executed:
# Look for similar nodes that might have the same pattern
rg -n "as unknown as" packages/koenig-lexical/src/nodes/ --type=tsxRepository: TryGhost/Koenig
Length of output: 86
🏁 Script executed:
# Check other nodes for how they handle property access
fd -type f -name "*.tsx" -path "*/nodes/*" | head -10Repository: TryGhost/Koenig
Length of output: 229
🏁 Script executed:
# Check package.json for dependencies
cat -n packages/koenig-lexical/package.json | grep -A 5 -B 5 "kg-default-nodes"Repository: TryGhost/Koenig
Length of output: 1315
🏁 Script executed:
# Search for similar type casting patterns
rg "as unknown as" packages/koenig-lexical/src/nodes/Repository: TryGhost/Koenig
Length of output: 630
🏁 Script executed:
# Check other node files to see if they use similar patterns
ls -la packages/koenig-lexical/src/nodes/*.tsx | head -10Repository: TryGhost/Koenig
Length of output: 1109
🏁 Script executed:
# Look at another node implementation for comparison
fd "Node.tsx$" packages/koenig-lexical/src/nodes/ | head -3 | xargs -I {} sh -c 'echo "=== {} ===" && head -120 {}'Repository: TryGhost/Koenig
Length of output: 7643
🏁 Script executed:
# Check if there's a node_modules or dist directory with type definitions
fd "ProductNode" packages/koenig-lexical/node_modules/@tryghost/kg-default-nodes 2>/dev/null | head -5Repository: TryGhost/Koenig
Length of output: 41
🏁 Script executed:
# Look at how AudioNode accesses properties directly without the interface cast
cat -n packages/koenig-lexical/src/nodes/AudioNode.tsx | sed -n '50,60p'Repository: TryGhost/Koenig
Length of output: 643
🏁 Script executed:
# Check the exact properties accessed in ProductNode and compare with ButtonNode
cat -n packages/koenig-lexical/src/nodes/ButtonNode.tsx | sed -n '30,45p'Repository: TryGhost/Koenig
Length of output: 687
Replace unsafe double-cast with direct property access.
The pattern this as unknown as ProductNodeProperties unnecessarily bypasses TypeScript's type checking. Other nodes in the codebase (e.g., AudioNode, ButtonNode) access inherited properties directly with individual type assertions, which is safer and more maintainable.
Replace line 100 with direct property access:
Suggested change
decorate() {
- const props = this as unknown as ProductNodeProperties;
return (
<KoenigCardWrapper nodeKey={this.getKey()}>
<ProductNodeComponent
- buttonText={props.productButton}
- buttonUrl={props.productUrl}
- description={props.productDescription}
+ buttonText={this.productButton as string}
+ buttonUrl={this.productUrl as string}
+ description={this.productDescription as string}
descriptionEditor={this.__productDescriptionEditor}
descriptionEditorInitialState={this.__productDescriptionEditorInitialState as string | undefined}
- imgHeight={props.productImageHeight}
- imgSrc={props.productImageSrc}
- imgWidth={props.productImageWidth}
- isButtonEnabled={props.productButtonEnabled}
- isRatingEnabled={props.productRatingEnabled}
+ imgHeight={this.productImageHeight as number}
+ imgSrc={this.productImageSrc as string}
+ imgWidth={this.productImageWidth as number}
+ isButtonEnabled={this.productButtonEnabled as boolean}
+ isRatingEnabled={this.productRatingEnabled as boolean}
nodeKey={this.getKey()}
- starRating={props.productStarRating}
- title={props.productTitle}
+ starRating={this.productStarRating as number}
+ title={this.productTitle as string}
titleEditor={this.__productTitleEditor}
titleEditorInitialState={this.__productTitleEditorInitialState as string | undefined}
/>🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/koenig-lexical/src/nodes/ProductNode.tsx` around lines 99 - 100, In
decorate() of ProductNode, remove the unsafe double-cast "this as unknown as
ProductNodeProperties" and instead access the node's inherited properties
directly using targeted assertions or native access (following the pattern used
in AudioNode/ButtonNode); update references inside decorate() to cast only where
necessary (e.g., cast this to the specific interface when accessing
ProductNodeProperties fields) so you avoid the broad unknown-to-type cast while
keeping ProductNode and ProductNodeProperties as the identifying symbols to
locate changes.
| const handleImageChange = async (files: File[] | FileList | null) => { | ||
| // reset original src so it can be replaced with preview and upload progress | ||
| editor.update(() => { | ||
| const node = $getNodeByKey(nodeKey); | ||
| const node = $getSignupNodeByKey(nodeKey); | ||
| if (!node) {return;} | ||
| node.backgroundImageSrc = ''; | ||
| }); | ||
|
|
||
| const {imageSrc} = await backgroundImageUploadHandler(files, imageUploader.upload); | ||
| const result = await backgroundImageUploadHandler(Array.from(files!), imageUploader.upload); | ||
| if (!result) {return;} | ||
| const {imageSrc} = result; | ||
|
|
||
| editor.update(() => { | ||
| const node = $getNodeByKey(nodeKey); | ||
| node.backgroundImageSrc = imageSrc; | ||
| const node = $getSignupNodeByKey(nodeKey); | ||
| if (!node) {return;} | ||
| node.backgroundImageSrc = imageSrc ?? ""; | ||
| }); | ||
|
|
||
| setLastBackgroundImage(imageSrc); | ||
| setLastBackgroundImage(imageSrc as string); |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
cd packages/koenig-lexical && wc -l src/nodes/SignupNodeComponent.tsxRepository: TryGhost/Koenig
Length of output: 96
🏁 Script executed:
cd packages/koenig-lexical && sed -n '120,160p' src/nodes/SignupNodeComponent.tsxRepository: TryGhost/Koenig
Length of output: 1435
🏁 Script executed:
cd packages/koenig-lexical && rg -B 3 -A 10 "backgroundImageUploadHandler" src/Repository: TryGhost/Koenig
Length of output: 7961
🏁 Script executed:
cd packages/koenig-lexical && rg "export.*backgroundImageUploadHandler|const backgroundImageUploadHandler|function backgroundImageUploadHandler" src/Repository: TryGhost/Koenig
Length of output: 221
🏁 Script executed:
cd packages/koenig-lexical && sed -n '1,30p' src/utils/imageUploadHandler.tsRepository: TryGhost/Koenig
Length of output: 1244
🏁 Script executed:
cd packages/koenig-lexical && rg -B 2 -A 8 "useFileDragAndDrop" src/Repository: TryGhost/Koenig
Length of output: 18643
🏁 Script executed:
cd packages/koenig-lexical && sed -n '1,50p' src/hooks/useFileDragAndDrop.tsRepository: TryGhost/Koenig
Length of output: 1602
Move file validation before mutating state.
handleImageChange accepts null (passed from onFileChange via e.target.files), but clears backgroundImageSrc before validating the input. If files is null, Array.from(null) throws. If the upload fails, the image remains cleared. Validate and return early on empty/null input before any state mutations.
Suggested fix
const handleImageChange = async (files: File[] | FileList | null) => {
+ const nextFiles = files ? Array.from(files) : [];
+ if (nextFiles.length === 0) {
+ return;
+ }
+
// reset original src so it can be replaced with preview and upload progress
editor.update(() => {
const node = $getSignupNodeByKey(nodeKey);
if (!node) {return;}
node.backgroundImageSrc = '';
});
- const result = await backgroundImageUploadHandler(Array.from(files!), imageUploader.upload);
+ const result = await backgroundImageUploadHandler(nextFiles, imageUploader.upload);
if (!result) {return;}
const {imageSrc} = result;
editor.update(() => {
const node = $getSignupNodeByKey(nodeKey);
if (!node) {return;}
node.backgroundImageSrc = imageSrc ?? "";
});
- setLastBackgroundImage(imageSrc as string);
+ if (imageSrc) {
+ setLastBackgroundImage(imageSrc);
+ }
setImageRemoved(false);
};🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/koenig-lexical/src/nodes/SignupNodeComponent.tsx` around lines 132 -
150, In handleImageChange, validate the incoming files and return early if files
is null/empty before calling editor.update or Array.from; move the null/empty
check ahead of the initial editor.update so you don't clear
node.backgroundImageSrc when there's nothing to upload, and also avoid calling
Array.from(files) on null. After validating, call
backgroundImageUploadHandler(Array.from(files), imageUploader.upload), then on
successful result run editor.update to set node.backgroundImageSrc and call
setLastBackgroundImage; keep references to $getSignupNodeByKey,
backgroundImageUploadHandler, editor.update, imageUploader.upload, and
setLastBackgroundImage when making the change.
| if (event.key === ':') { | ||
| if (cursorInInlineCodeBlock() === true) { | ||
| return false; | ||
| } | ||
| const emojis = await SearchIndex.search(queryString); | ||
| if (emojis.length === 0) { | ||
| return; | ||
| } | ||
| const emojiMatch = emojis?.[0].id === queryString; // only look for exact match | ||
| if (emojiMatch) { | ||
| handleCompletionInsertion(emojis[0]); | ||
| event.preventDefault(); | ||
| return true; | ||
| } | ||
| SearchIndex.search(queryString).then((emojis: Emoji[]) => { | ||
| if (emojis.length === 0) { | ||
| return; | ||
| } | ||
| const emojiMatch = emojis[0].id === queryString; | ||
| if (emojiMatch) { | ||
| handleCompletionInsertion(emojis[0]); | ||
| } | ||
| }); | ||
| event.preventDefault(); | ||
| return true; |
There was a problem hiding this comment.
Only swallow : after an exact shortcode match is confirmed.
This now prevents the closing colon for any active emoji query, even when there is no exact emoji id. Typing something like :notanemoji: will drop the final : instead of letting the text through.
💡 Suggested fix
if (event.key === ':') {
if (cursorInInlineCodeBlock() === true) {
return false;
}
- SearchIndex.search(queryString).then((emojis: Emoji[]) => {
- if (emojis.length === 0) {
- return;
- }
- const emojiMatch = emojis[0].id === queryString;
- if (emojiMatch) {
- handleCompletionInsertion(emojis[0]);
- }
- });
- event.preventDefault();
- return true;
+ const emojiMatch = searchResults?.find(emoji => emoji.id === queryString);
+ if (!emojiMatch) {
+ return false;
+ }
+ event.preventDefault();
+ handleCompletionInsertion(emojiMatch);
+ return true;
}🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@packages/koenig-lexical/src/plugins/EmojiPickerPlugin.tsx` around lines 74 -
88, The key handler currently prevents default for ':' unconditionally; change
it so SearchIndex.search(queryString) is awaited (or use its promise) and only
when an exact shortcode match is found (compare emojis[0].id === queryString)
call handleCompletionInsertion(emojis[0]) and then call event.preventDefault()
and return true; otherwise do not preventDefault and return false so the
trailing ':' is inserted normally. Keep the early cursorInInlineCodeBlock()
check as-is and update the branches around SearchIndex.search,
handleCompletionInsertion, and event.preventDefault to only run on confirmed
exact match.
Summary
.js/.jsxfiles to.ts/.tsxacross src/, test/, and demo/tsconfig.json,eslint.config.jswith@typescript-eslint)tseslint.config()withdefineConfig()fromeslint/configEmojifor emoji-mart,SimpleMDEInstance/CodeMirrorInstancefor kg-simplemde)test/types.d.tswith Window augmentation for Lexical editor in e2e browser contexteslint-disablecomments except intentionalreact-hooks/exhaustive-depsfor mount-only effects