Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
3 changes: 3 additions & 0 deletions packages/koenig-lexical/.storybook/main.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { dirname, join } from "path";
import { createRequire } from "module";
import { mergeConfig } from 'vite';
import type {StorybookConfig} from '@storybook/react-vite';

const require = createRequire(import.meta.url);

const config: StorybookConfig = {
framework: {
name: getAbsolutePath("@storybook/react-vite"),
Expand Down

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,14 @@ function HtmlOutputDemo() {
const [html, setHtml] = useState('<p><span>check</span> <a href="https://ghost.org/changelog/markdown/" dir="ltr"><span data-lexical-text="true">ghost.org/changelog/markdown/</span></a></p>');
const [sidebarView, setSidebarView] = useState('json');
const [defaultContent] = useState(undefined);
const [editorAPI, setEditorAPI] = useState(null);
const titleRef = React.useRef(null);
const containerRef = React.useRef(null);
const [editorAPI, setEditorAPI] = useState<{
editorInstance: unknown;
insertParagraphAtBottom: () => void;
focusEditor: (options: {position: string}) => void;
[key: string]: unknown;
} | null>(null);
const titleRef = React.useRef<{focus: () => void} | null>(null);
const containerRef = React.useRef<HTMLDivElement>(null);
const {snippets, createSnippet, deleteSnippet} = useSnippets();

function openSidebar(view = 'json') {
Expand All @@ -37,13 +42,14 @@ function HtmlOutputDemo() {
titleRef.current?.focus();
}

function focusEditor(event) {
const clickedOnDecorator = (event.target.closest('[data-lexical-decorator]') !== null) || event.target.hasAttribute('data-lexical-decorator');
const clickedOnSlashMenu = (event.target.closest('[data-kg-slash-menu]') !== null) || event.target.hasAttribute('data-kg-slash-menu');
function focusEditor(event: React.MouseEvent<HTMLDivElement>) {
const target = event.target as HTMLElement;
const clickedOnDecorator = (target.closest('[data-lexical-decorator]') !== null) || target.hasAttribute('data-lexical-decorator');
const clickedOnSlashMenu = (target.closest('[data-kg-slash-menu]') !== null) || target.hasAttribute('data-kg-slash-menu');

if (editorAPI && !clickedOnDecorator && !clickedOnSlashMenu) {
let editor = editorAPI.editorInstance;
let {bottom} = editor._rootElement.getBoundingClientRect();
const editor = editorAPI.editorInstance as {_rootElement: HTMLElement; getEditorState: () => {read: (fn: () => void) => void}};
const {bottom} = editor._rootElement.getBoundingClientRect();

// if a mousedown and subsequent mouseup occurs below the editor
// canvas, focus the editor and put the cursor at the end of the document
Expand Down Expand Up @@ -72,7 +78,7 @@ function HtmlOutputDemo() {
editorAPI.focusEditor({position: 'bottom'});

//scroll to the bottom of the container
containerRef.current.scrollTop = containerRef.current.scrollHeight;
containerRef.current!.scrollTop = containerRef.current!.scrollHeight;
}
}
}
Expand All @@ -93,7 +99,7 @@ function HtmlOutputDemo() {
<div className="mx-auto max-w-[740px] px-6 py-[15vmin] lg:px-0">
<KoenigComposableEditor
cursorDidExitAtTop={focusTitle}
registerAPI={setEditorAPI}
registerAPI={setEditorAPI as (api: unknown) => void}
>
<HtmlOutputPlugin html={html} setHtml={setHtml}/>
</KoenigComposableEditor>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,15 +22,20 @@ function useQuery() {
return React.useMemo(() => new URLSearchParams(search), [search]);
}

function RestrictedContentDemo() {
let query = useQuery();
function RestrictedContentDemo(_props: {paragraphs?: number}) {
const query = useQuery();
const [isSidebarOpen, setIsSidebarOpen] = useState(false);
const [sidebarView, setSidebarView] = useState('json');
const [defaultContent] = useState(undefined);
const [editorAPI, setEditorAPI] = useState(null);
const titleRef = React.useRef(null);
const containerRef = React.useRef(null);
const paragraphs = query.get('paragraphs') || 1;
const [editorAPI, setEditorAPI] = useState<{
editorInstance: unknown;
insertParagraphAtBottom: () => void;
focusEditor: (options: {position: string}) => void;
[key: string]: unknown;
} | null>(null);
const titleRef = React.useRef<{focus: () => void} | null>(null);
const containerRef = React.useRef<HTMLDivElement>(null);
const paragraphs = Number(query.get('paragraphs')) || 1;
const {snippets, createSnippet, deleteSnippet} = useSnippets();

function openSidebar(view = 'json') {
Expand All @@ -45,13 +50,14 @@ function RestrictedContentDemo() {
titleRef.current?.focus();
}

function focusEditor(event) {
const clickedOnDecorator = (event.target.closest('[data-lexical-decorator]') !== null) || event.target.hasAttribute('data-lexical-decorator');
const clickedOnSlashMenu = (event.target.closest('[data-kg-slash-menu]') !== null) || event.target.hasAttribute('data-kg-slash-menu');
function focusEditor(event: React.MouseEvent<HTMLDivElement>) {
const target = event.target as HTMLElement;
const clickedOnDecorator = (target.closest('[data-lexical-decorator]') !== null) || target.hasAttribute('data-lexical-decorator');
const clickedOnSlashMenu = (target.closest('[data-kg-slash-menu]') !== null) || target.hasAttribute('data-kg-slash-menu');

if (editorAPI && !clickedOnDecorator && !clickedOnSlashMenu) {
let editor = editorAPI.editorInstance;
let {bottom} = editor._rootElement.getBoundingClientRect();
const editor = editorAPI.editorInstance as {_rootElement: HTMLElement; getEditorState: () => {read: (fn: () => void) => void}};
const {bottom} = editor._rootElement.getBoundingClientRect();

// if a mousedown and subsequent mouseup occurs below the editor
// canvas, focus the editor and put the cursor at the end of the document
Expand Down Expand Up @@ -80,7 +86,7 @@ function RestrictedContentDemo() {
editorAPI.focusEditor({position: 'bottom'});

//scroll to the bottom of the container
containerRef.current.scrollTop = containerRef.current.scrollHeight;
containerRef.current!.scrollTop = containerRef.current!.scrollHeight;
}
}
}
Expand All @@ -99,7 +105,7 @@ function RestrictedContentDemo() {
<div className="mx-auto max-w-[740px] px-6 py-[15vmin] lg:px-0">
<KoenigComposableEditor
cursorDidExitAtTop={focusTitle}
registerAPI={setEditorAPI}
registerAPI={setEditorAPI as (api: unknown) => void}
>
<RestrictContentPlugin paragraphs={paragraphs} />
</KoenigComposableEditor>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
const DarkModeToggle = ({darkMode, toggleDarkMode}) => {
interface DarkModeToggleProps {
darkMode: boolean;
toggleDarkMode: () => void;
}

const DarkModeToggle = ({darkMode, toggleDarkMode}: DarkModeToggleProps) => {
return (
<>
<button className="absolute right-20 top-4 z-20 block h-[22px] w-[42px] cursor-pointer rounded-full transition-all ease-in-out" type="button" onClick={toggleDarkMode}>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
const EmailEditorWrapper = ({children}) => {
import React from 'react';

const EmailEditorWrapper = ({children}: {children: React.ReactNode}) => {
return (
<div>
<div className="mb-6">
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
const FloatingButton = ({isOpen, ...props}) => {
interface FloatingButtonProps {
isOpen: boolean;
onClick: (view: string) => void;
}

const FloatingButton = ({isOpen, ...props}: FloatingButtonProps) => {
return (
<div className={`fixed bottom-4 right-6 z-20 rounded px-2 py-1 font-mono text-sm tracking-tight text-grey-600 transition-all duration-200 ease-in-out ${isOpen ? 'bg-transparent' : 'bg-white'}`}>
<button className="cursor-pointer" type="button" onClick={() => props.onClick('json')}>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,14 @@ import React from 'react';
import {$createParagraphNode, $getRoot} from 'lexical';
import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';

const InitialContentToggle = ({defaultContent, setTitle, searchParams, setSearchParams}) => {
interface InitialContentToggleProps {
defaultContent: string;
setTitle: (title: string) => void;
searchParams: URLSearchParams;
setSearchParams: (params: URLSearchParams) => void;
}

const InitialContentToggle = ({defaultContent, setTitle, searchParams, setSearchParams}: InitialContentToggleProps) => {
const [editor] = useLexicalComposerContext();
const [isOn, setIsOn] = React.useState(searchParams.get('content') !== 'false');

Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import {useNavigate} from 'react-router-dom';

declare global {
interface Window {
navigate: ReturnType<typeof useNavigate>;
}
}

const Navigator = () => {
const navigate = useNavigate();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import React from 'react';
import {OnChangePlugin} from '@lexical/react/LexicalOnChangePlugin';
import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';

const SerializedStateTextarea = ({isOpen}) => {
const SerializedStateTextarea = ({isOpen}: {isOpen: boolean}) => {
const [editor] = useLexicalComposerContext();

const renderEditorState = () => JSON.stringify(editor.getEditorState().toJSON(), null, 2);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,13 @@
import SerializedStateTextarea from './SerializedStateTextarea';
import TreeView from './TreeView';

const Sidebar = ({isOpen, view, saveContent}) => {
interface SidebarProps {
isOpen: boolean;
view: string;
saveContent?: () => void;
}

const Sidebar = ({isOpen, view, saveContent}: SidebarProps) => {
Comment on lines +4 to +10
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 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=jsx

Repository: TryGhost/Koenig

Length of output: 86


🏁 Script executed:

# Search for Sidebar component usage with corrected flags
rg -n '<Sidebar' --glob='*.{tsx,jsx}' -C2

Repository: 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.tsx

Repository: TryGhost/Koenig

Length of output: 941


🏁 Script executed:

# Check RestrictedContentDemo for sidebarView initialization/values
rg -n 'sidebarView' packages/koenig-lexical/demo/RestrictedContentDemo.tsx -B3 -A3

Repository: TryGhost/Koenig

Length of output: 1086


🏁 Script executed:

# Check HtmlOutputDemo for sidebarView initialization/values
rg -n 'sidebarView' packages/koenig-lexical/demo/HtmlOutputDemo.tsx -B3 -A3

Repository: TryGhost/Koenig

Length of output: 1245


🏁 Script executed:

# Check DemoApp for sidebarView initialization/values
rg -n 'sidebarView' packages/koenig-lexical/demo/DemoApp.tsx -B3 -A3

Repository: 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.tsx

Repository: 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.tsx

Repository: 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/ -A20

Repository: 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.

return (
<div className={`h-full grow overflow-hidden border-grey-100 bg-black pb-16 transition-all ease-in-out ${isOpen ? 'right-0 w-full opacity-100 sm:w-[440px]' : 'right-[-100%] w-0 opacity-0'}`}>
{view === 'json' && <SerializedStateTextarea isOpen={isOpen} />}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,21 @@
import React from 'react';

export const TitleTextBox = React.forwardRef(({title, setTitle, editorAPI}, ref) => {
const titleEl = React.useRef(null);
interface TitleTextBoxProps {
title: string;
setTitle: (title: string) => void;
editorAPI: {
editorIsEmpty: () => boolean;
insertParagraphAtTop: (options: {focus: boolean}) => void;
focusEditor: (options: {position: string}) => void;
} | null;
}

export interface TitleTextBoxHandle {
focus: () => void;
}

export const TitleTextBox = React.forwardRef<TitleTextBoxHandle, TitleTextBoxProps>(({title, setTitle, editorAPI}, ref) => {
const titleEl = React.useRef<HTMLTextAreaElement>(null);

React.useImperativeHandle(ref, () => ({
focus: () => {
Expand All @@ -16,21 +30,21 @@ export const TitleTextBox = React.forwardRef(({title, setTitle, editorAPI}, ref)
}
}, [titleEl, title]);

const handleTitleInput = (e) => {
const handleTitleInput = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
setTitle(e.target.value);
};

// move cursor to the editor on
// - Tab
// - Arrow Down/Right when input is empty or caret at end of input
// - Enter, creating an empty paragraph when editor is not empty
const handleTitleKeyDown = (event) => {
const handleTitleKeyDown = (event: React.KeyboardEvent<HTMLTextAreaElement>) => {
if (!editorAPI) {
return;
}

const {key} = event;
const {value, selectionStart} = event.target;
const {value, selectionStart} = event.target as HTMLTextAreaElement;

const couldLeaveTitle = !value || selectionStart === value.length;
const arrowLeavingTitle = ['ArrowDown', 'ArrowRight'].includes(key) && couldLeaveTitle;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
import {TreeView} from '@lexical/react/LexicalTreeView';
import {useLexicalComposerContext} from '@lexical/react/LexicalComposerContext';

const TreeViewPlugin = () => {
const TreeViewPlugin = (_props: {isOpen?: boolean}) => {
const [editor] = useLexicalComposerContext();

return (
<TreeView
editor={editor}
treeTypeButtonClassName="text-green font-sans text-md font-medium"
timeTravelButtonClassName="text-green pb-4 cursor-pointer font-sans text-md font-medium absolute bottom-0"
timeTravelPanelButtonClassName="text-green font-sans text-md font-medium"
timeTravelPanelClassName="absolute bottom-1 flex w-[400px]"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
import GhostFavicon from './icons/ghost-favicon.svg?react';
import {Link} from 'react-router-dom';

function EditorLink({editorType}) {
function EditorLink({editorType}: {editorType: {name: string; url: string}}) {
return (
<Link rel="nofollow ugc noopener noreferrer" to={editorType?.url}>
<span className="ml-[.7rem] hidden font-normal hover:font-bold group-hover:inline">/ {editorType?.name}</span>
</Link>
);
}

const Watermark = ({editorType}) => {
const Watermark = ({editorType}: {editorType?: string}) => {
if (!editorType) {
return (
<a className="absolute bottom-4 left-6 z-20 flex items-center rounded bg-white py-1 pl-1 pr-2 font-mono text-sm tracking-tight text-black" href="https://github.com/TryGhost/Koenig/tree/main/packages/koenig-lexical" rel="nofollow ugc noopener noreferrer" target="_blank">
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const WordCount = ({wordCount, tkCount}) => {
const WordCount = ({wordCount, tkCount}: {wordCount: number; tkCount: number}) => {
return (
<div className="absolute left-6 top-4 z-20 block cursor-pointer rounded bg-white px-2 py-1 font-mono text-sm tracking-tight text-grey-600 dark:bg-transparent">
<span data-testid="word-count">{wordCount}</span> words
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ import {
Routes
} from 'react-router-dom';

ReactDOM.createRoot(document.getElementById('root')).render(
ReactDOM.createRoot(document.getElementById('root')!).render(
<React.StrictMode>
<Router>
<Navigator />
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
export async function fetchEmbed(url, {type}) {
export async function fetchEmbed(url: string, {type}: {type: string}) {
console.log('fetchEmbed', {url, type});
let urlObject = new URL(url);
const urlObject = new URL(url);
if (!urlObject) {
throw new Error('No URL specified.');
}
await delay(process.env.NODE_ENV === 'test' ? 50 : 1500);
// let html = await (await fetch(url)).text();
try {
if (type === 'bookmark') {
let returnData = {
const returnData = {
url: 'https://www.ghost.org/',
metadata: {
icon: 'https://www.ghost.org/favicon.ico',
Expand Down Expand Up @@ -291,7 +291,7 @@ export async function fetchEmbed(url, {type}) {
// }
// }
// };
let returnData = {
const returnData = {
html: '<iframe width="200" height="113" src="https://www.youtube.com/embed/b52pBaObiY0?feature=oembed" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" allowfullscreen="" title="BOM at the Historic Rally Festival 2021" style="width: 100%; height: 418.1px; max-width: 100%;"></iframe>',
author_url: 'https://www.youtube.com/user/gorillaz',
provider_name: 'YouTube',
Expand All @@ -308,12 +308,12 @@ export async function fetchEmbed(url, {type}) {
}
return returnData;
}
} catch (e) {
// console.log(e);
} catch {
// console.log error
}
}

function delay(time) {
function delay(time: number) {
return new Promise((resolve) => {
setTimeout(resolve, time);
});
Expand Down
Loading
Loading