Marktion WYSIWYG Markdown Editor
repository·main·Indexed 43 days ago
https://github.com/microvoid/marktionA WYSIWYG Markdown editor built on ProseMirror featuring real-time rendering, slash menus, bubble menus, and built-in AI integration via Vercel AI. Includes a ReactEditor component, an API client with OpenAPI support, and utility packages like use-channel and use-context-immer.
What's inside Marktion
- If a component fails to render in a story, it is often because it requires a specific environment or context (e.g., a theme provider). You can use Storybook Decorators to provide this necessary context or mock data to your components.
Quickstart: Use ReactEditor in a React component
mainImport
ReactEditorand its corresponding CSS file to render a WYSIWYG Markdown editor. You can pass initial content via thecontentprop.import { ReactEditor } from 'marktion'; import 'marktion/dist/style.css'; function Editor() { return <ReactEditor content={`# Hello World`} />; }Enable type-aware ESLint rules for production
mainFor production applications, it is recommended to enable type-aware linting by updating your ESLint configuration.
- Update
parserOptionsto include yourtsconfigfiles:
parserOptions: { ecmaVersion: 'latest', sourceType: 'module', project: ['./tsconfig.json', './tsconfig.node.json'], tsconfigRootDir: __dirname, },- Update your
extendslist:
- Replace
plugin:@typescript-eslint/recommendedwithplugin:@typescript-eslint/recommended-type-checkedorplugin:@typescript-eslint/strict-type-checked. - Optionally add
plugin:@typescript-eslint/stylistic-type-checked. - Install
eslint-plugin-reactand addplugin:react/recommendedandplugin:react/jsx-runtimeto theextendslist.
- Update
Manage state with useDispatch and useContextSelector
mainFor a more structured approach, use
useDispatchto retrieve the dispatch function anduseContextSelectorto subscribe to specific parts of your context state. This allows components to re-render only when the selected slice of state changes.- Define a state class.
- Create a context with an initial instance of that class.
- Use
useDispatchto trigger updates viaproduce-style functions. - Use
useContextSelectorwith a selector function to extract specific values. - Wrap your application in the context provider (which can be used as a component directly).
class AppState { style = { theme: 'light', primary: 'tomato' }; } const AppStateContext = createContext(new AppState()); const setDarkMode = produce(draft => { draft.style.theme = 'dark'; }); const getThemeMode = (state: AppState) => { return state.style.theme; }; function App() { const dispatch = useDispatch(); const theme = useContextSelector(getThemeMode); const onToggle = () => { dispatch(setDarkMode); }; return ( <div> theme: {theme} <button onClick={onToggle}>Toggle Mode</button> </div> ); } render( <AppStateContext> <App /> </AppStateContext>, rootEl );Install Marktion via npm
mainTo use Marktion in your project, install the package using npm.
npm intall marktionUse use-channel for cross-component communication
mainThe
use-channelhook provides a mechanism for components to communicate via a shared channel. You can create a typed channel usingcreateChannel<T>()and then consume it in components using theuseChannel(channel)hook.To send data through a channel, call
channel.message(data). To access the history of messages sent through the channel, usechannel.messages(), which returns an array of the messages received.// 1. Define a typed channel export const EditArticleChannel = createChannel<Article[]>(); // 2. Consume the channel in a component to listen for messages function Editor() { const channel = useChannel(EditArticleChannel); useEffect(() => { // Get the latest message from the channel const followArticle = channel.messages().pop(); if (followArticle) { // Handle the new message } }, [channel]); // 3. Send a message through the channel const sendMessage = (data: Article[]) => { channel.message(data); }; } // 4. Send a message from a different component function ArticleList() { const channel = useChannel(EditArticleChannel); const onEditItem = (item: Article[]) => { channel.message(item); }; }Quick start with use-context-immer
mainTo use
use-context-immer, create a context and useuseSelectorto access specific slices of your state or the dispatch function. You can update the state by calling the dispatch function with a function that uses Immer-style mutations to modify the draft state.Note: The example uses
useSelectoranddispath(likely a typo fordispatchin the source) to interact with the context provider.const TodoContext = createContext(); function Todo() { const todos = useSelector(TodoContext, ctx => ctx.model.todos); const dispath = useSelector(TodoContext, ctx => ctx.dispath); const onCommit = () => { dispath(model => { model.todos[0].check = true; }) } } function App() { return <TodoContext.Provider><Todo /></TodoContext.Provider> }Use the AI Plugin with Marktion
mainMarktion supports an AI plugin based on Vercel AI. This provides a built-in AI chat interface that can be invoked by pressing
Spaceat the start of a line.To use it, you must first create an AI router (refer to Vercel AI SDK documentation). You can then use the
useAIhook to provide the plugin to theReactEditorvia thepluginsprop.function Editor() { const ai = useAI({ basePath: import.meta.env.VITE_OPENAI_BASE_URL }); return <ReactEditor ref={editorRef} plugins={[ai.plugin]} />; }Configure styling and CSS in Storybook
mainStorybook can be configured to include various styling technologies (CSS, CSS-in-JS, etc.) to match your application's setup. Refer to the official Storybook documentation for specific framework integration guides.Initialize the Marktion Editor with plugins
mainTo use the Marktion editor, initialize an
EditorStatewith a list of plugins. You can then pass this state to the<Editor />component. TheonChangecallback should be used to update the state when the editor content changes.Common plugins include
toolbar(),bubble(),slash(),upload(), andplaceholder(). You can also extend functionality by adding specialized plugins likeAIToolkit().const defaultPlugins = [toolbar(), bubble(), slash(), upload(), placeholder()]; function App() { const [state, setState] = useState(function () { return EditorState.create({ plugins: [...defaultPlugins, AIToolkit()] }); }); return <Editor state={state} onChange={setState} />; }Expand ESLint configuration for type-aware linting
mainFor production applications, it is recommended to enable type-aware lint rules in your ESLint configuration. This requires configuring
parserOptionsto point to your TypeScript configuration files and switching to type-checked plugin presets.Steps to enable type-aware linting:
- Update
parserOptionsin your ESLint config to includeprojectandtsconfigRootDir. - Replace standard TypeScript ESLint plugins with type-checked versions.
- (Optional) Add stylistic type-checked rules.
- (Optional) Add
eslint-plugin-reactfor React-specific linting.
// 1. Configure parserOptions parserOptions: { ecmaVersion: 'latest', sourceType: 'module', project: ['./tsconfig.json', './tsconfig.node.json'], tsconfigRootDir: __dirname, }, // 2. Update extends list (example logic) // Replace 'plugin:@typescript-eslint/recommended' // With 'plugin:@typescript-eslint/recommended-type-checked' // or 'plugin:@typescript-eslint/strict-type-checked' // 3. Optionally add: // 'plugin:@typescript-eslint/stylistic-type-checked' // 4. Add React plugins (requires eslint-plugin-react): // 'plugin:react/recommended' // 'plugin:react/jsx-runtime'- Update
Generate Prisma TypeScript types
mainAfter modifying your Prisma schema or running migrations, generate the corresponding TypeScript types using
prisma generate. This ensures your application code has type safety for the database schema.# gen ts type pnpm exec prisma generate