@owomark/react
v0.1.4
Published
React bindings and components for the OwoMark editor stack.
Maintainers
Readme
OwoMark
A self-contained Markdown editor built on a single-layer contenteditable surface.
Install the official package: @owomark/react.
Four packages:
| Package | Role |
|---|---|
| @owomark/core | Framework-agnostic editor engine (document model, input handling, commands, shared state) |
| @owomark/view | View engine + preview rendering (DOM events, active block proxy, DOM patching, render cache) |
| @owomark/react | React components (OwoMarkEditor, OwoMarkPreview) and shared state hooks |
| (theme built into @owomark/view) | Light / dark CSS theme presets and token definitions |
Quick Start (React)
import { OwoMarkEditor } from '@owomark/react';
import '@owomark/view/style.css';
function App() {
const [md, setMd] = useState('# Hello\n\nStart typing...');
return (
<OwoMarkEditor
value={md}
onChange={setMd}
config={{ theme: 'light', enableSideAnnotation: true }}
placeholder="Write something..."
/>
);
}Props
| Prop | Type | Default | Description |
|---|---|---|---|
| value | string | — | Controlled markdown value |
| defaultValue | string | — | Uncontrolled initial value |
| onChange | (markdown: string) => void | — | Called on content change |
| onSelectionChange | (selection: OwoMarkSelection) => void | — | Cursor/selection updates |
| onCompositionStateChange | (active: boolean) => void | — | IME composition state |
| onScroll | React.UIEventHandler<HTMLDivElement> | — | Forwards scroll events from the root editor container |
| readOnly | boolean | false | Disable editing |
| placeholder | string | — | Empty-state placeholder text |
| theme | 'light' \| 'dark' \| string | 'light' | Theme preset or custom class |
| themeClassName | string | — | Additional theme class |
| className | string | — | Extra CSS class on the root element |
| commandsRef | Ref<OwoMarkCommands> | — | Imperative command handle |
| coreRef | Ref<OwoMarkCore \| null> | — | Direct access to the underlying OwoMarkCore instance |
| controller | OwoMarkSharedStateController | — | Auto-connects editor to shared state (recommended for split editor) |
| indentMode | 'auto' \| '2' \| '4' | 'auto' | Tab indent width |
| config | OwoMarkEditorConfig | — | Unified editor config (indentMode, enableSideAnnotation, enableMath) |
| ariaLabel | string | — | Accessibility label |
When config is provided, it becomes the unified configuration surface for editor behavior:
<OwoMarkEditor
value={md}
onChange={setMd}
config={{
theme: 'light',
indentMode: '2',
enableSideAnnotation: true,
enableMath: true,
}}
/>Imperative Commands
const cmds = useRef<OwoMarkCommands>(null);
<OwoMarkEditor commandsRef={cmds} ... />
// Later:
cmds.current.toggleBold();
cmds.current.toggleItalic();
cmds.current.insertLink('https://example.com');
cmds.current.insertCodeFence('ts');
cmds.current.undo();
cmds.current.redo();
cmds.current.getMarkdown();
cmds.current.setMarkdown('# New content');
cmds.current.replaceMarkdown('# New content', { anchor: 13, focus: 13 });
cmds.current.focus();Use replaceMarkdown() when the host needs to replace the document and also control the resulting selection in one atomic step, such as toolbar-driven inserts.
Shared State + Preview
For a split editor with synchronized incremental preview:
import {
OwoMarkEditor,
OwoMarkPreview,
useOwoMarkSharedState,
} from '@owomark/react';
import type { PreviewBlock, PreviewRenderContext } from '@owomark/react';
function SplitEditor() {
const controller = useOwoMarkSharedState({ initialMarkdown: '# Hello' });
return (
<div style={{ display: 'flex' }}>
<OwoMarkEditor
controller={controller}
theme="light"
placeholder="Write something..."
/>
<OwoMarkPreview
state={controller}
themeKey="vitesse-light"
className="preview-panel"
/>
</div>
);
}The controller prop on OwoMarkEditor automatically connects the editor to the shared state controller. The editor drives all state updates (content, selection, composition) directly — no manual onChange → setMarkdown bridging needed.
For hosts that still need onChange callbacks (e.g. for local draft persistence), they work alongside controller:
<OwoMarkEditor
controller={controller}
onChange={(md) => saveDraft(md)}
/>Custom Block Renderer
Provide a host-side rendering function for full Markdown fidelity (GFM, math, syntax highlighting):
async function renderBlock(
block: PreviewBlock,
ctx: PreviewRenderContext,
): Promise<string> {
const result = await myUnifiedPipeline.process(block.raw);
return result.toString();
}
<OwoMarkPreview state={controller} renderBlock={renderBlock} themeKey="vitesse-light" />Side Annotation
OwoMark supports right-side annotations for preview rendering and editor workflows. The feature is enabled by default and can be turned off with config.enableSideAnnotation = false.
<OwoMarkEditor
value={md}
onChange={setMd}
config={{ enableSideAnnotation: false }}
/>With the flag disabled:
- side-annotation syntax is kept as plain Markdown text
- preview does not render side annotation layout
- the
/sideslash command is hidden - pressing Enter after an annotated block does not auto-insert
(>+)
Syntax Quick Reference
| Use case | Syntax |
|---|---|
| Single block annotation | Text (>} note) |
| Multi-block continuation (2-3 blocks) | A (>} group) + B (>+) |
| Container annotation (4+ blocks) | :::side + (>} note) + blocks + ::: |
| Long-form note reference | :::side [>id] ... ::: + [>id]: {type=}} ... |
Type Table
| Symbol | Meaning | Example |
|---|---|---|
| : | Plain note | (>: note) |
| } | Right brace | (>} grouped note) |
| { | Left brace | (>{ expanded note) |
| ] | Right bracket | (>] range) |
| [ | Left bracket | (>[ range) |
| | | Vertical line | (>| comment) |
| - | Dash | (>- remark) |
| -> | Thin arrow | (>-> conclusion) |
| => | Fat arrow | (>=> therefore) |
| ~> | Wave arrow | (>~> related) |
| ! | Warning | (>! caution) |
| ? | Question | (>? todo) |
Examples
Single block:
白菜 (>} 都是蔬菜)Continuation chain:
香蕉 (>} 都是水果)
菠萝 (>+)
苹果 (>+)Container form:
:::side
(>! 高危操作)
删除数据库前必须先备份。
确认备份可恢复后再执行破坏性操作。
:::Reference form:
:::side [>cook-tip]
炒菜时油温很重要。
热锅凉油是基本功。
:::
[>cook-tip]: {type=}}
烹饪小贴士:油温过高会产生油烟,
日常烹饪建议控制在合适范围内。Notes
- Side annotations are right-side only; there is no left-column layout mode.
(>...)must appear at the end of the block to be recognized.(>+)is a continuation marker only. It must follow a previous annotated sibling block.- On narrow viewports, side annotations fall back to inline callout blocks under the anchor content.
Shared State Hooks
| Hook | Description |
|------|-------------|
| useOwoMarkSharedState(options?) | Creates a persistent OwoMarkSharedStateController (survives re-renders) |
| useSharedStateSnapshot(controller) | Subscribes via useSyncExternalStore, returns current OwoMarkSharedState |
<OwoMarkPreview> Props
| Prop | Type | Description |
|------|------|-------------|
| state | OwoMarkSharedStateStore | Shared state store (e.g. controller from useOwoMarkSharedState) |
| themeKey | string | Theme identifier for rendering and cache keying |
| className | string | CSS class for the preview root |
| renderBlock | (block, context) => Promise<string> | Custom per-block Markdown renderer |
| registry | PreviewRendererRegistry | Custom renderer registry (e.g. Mermaid) |
| viewportFirst | boolean | Prioritize rendering visible blocks first |
| onContentUpdate | () => void | Called after every DOM mutation — including idle backfill and deferred renders (for scroll sync) |
| ariaLabel | string | Accessibility label |
CSS Setup
Minimal (use package presets)
import '@owomark/view/style.css'; // includes mdx-components.css + owomark.css + side-annotation.css + slash-menu.css + light.css + dark.cssSelective imports
import '@owomark/view/owomark.css'; // base editor styles (required)
import '@owomark/view/light.css'; // light preset tokens
// import '@owomark/view/dark.css'; // dark preset tokensThird-party theme layering
import '@owomark/view/style.css';
import 'owomark-theme-acme/style.css';<OwoMarkEditor
value={md}
onChange={setMd}
theme="owo-theme-acme"
/>The theme package should only override --owo-* tokens and optional visual details. It should not duplicate @owomark/view structural CSS. See the @owomark/view README for the authoring contract of owomark-theme-xxx.
Host Theme Mapping
OwoMark uses --owo-* CSS custom properties for all visual styles. To integrate with your design system, map your variables to OwoMark tokens on a wrapper element:
.my-editor-wrapper {
/* Surface */
--owo-editor-bg: var(--app-surface);
--owo-editor-text: var(--app-text-primary);
--owo-editor-heading: var(--app-text-strong);
/* Interaction */
--owo-editor-caret: var(--app-text-strong);
--owo-editor-link: var(--app-brand);
/* Border */
--owo-editor-border: var(--app-border);
/* Override default chrome if embedding */
& .owo-editor-root {
border: none;
padding: 0;
background: transparent;
}
}See docs/specs/owomark-theme-tokens.md for the full token list.
Standalone Usage (no React)
Use @owomark/core + @owomark/view for a framework-free DOM editor:
import { createOwoMarkCore } from '@owomark/core';
import { createOwoMarkView } from '@owomark/view';
const core = createOwoMarkCore({ initialMarkdown: '# Hello' });
const view = createOwoMarkView(core, document.getElementById('editor')!);
core.onChange((markdown) => console.log(markdown));
// Cleanup
view.destroy();
core.destroy();Or use createOwoMarkCore() directly for pure logic without DOM:
import { createOwoMarkCore } from '@owomark/core';
const core = createOwoMarkCore({ initialMarkdown: '# Hello' });
console.log(core.getMarkdown());Building
From the project root:
npm run build:packagesThis builds @owomark/core -> @owomark/view -> @owomark/react in dependency order using tsup (ESM + DTS).
