npm package: @owomark/react
Description: React bindings and components for the OwoMark editor stack.
Latest version: 0.1.4

# 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)

```tsx
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:

```tsx
<OwoMarkEditor
  value={md}
  onChange={setMd}
  config={{
    theme: 'light',
    indentMode: '2',
    enableSideAnnotation: true,
    enableMath: true,
  }}
/>
```

### Imperative Commands

```tsx
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:

```tsx
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`:

```tsx
<OwoMarkEditor
  controller={controller}
  onChange={(md) => saveDraft(md)}
/>
```

### Custom Block Renderer

Provide a host-side rendering function for full Markdown fidelity (GFM, math, syntax highlighting):

```tsx
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`.

```tsx
<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 `/side` slash 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:

```md
白菜 (>} 都是蔬菜)
```

Continuation chain:

```md
香蕉 (>} 都是水果)
菠萝 (>+)
苹果 (>+)
```

Container form:

```md
:::side
(>! 高危操作)

删除数据库前必须先备份。
确认备份可恢复后再执行破坏性操作。
:::
```

Reference form:

```md
:::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)

```ts
import '@owomark/view/style.css'; // includes mdx-components.css + owomark.css + side-annotation.css + slash-menu.css + light.css + dark.css
```

### Selective imports

```ts
import '@owomark/view/owomark.css'; // base editor styles (required)
import '@owomark/view/light.css';   // light preset tokens
// import '@owomark/view/dark.css'; // dark preset tokens
```

### Third-party theme layering

```ts
import '@owomark/view/style.css';
import 'owomark-theme-acme/style.css';
```

```tsx
<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:

```css
.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:

```ts
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:

```ts
import { createOwoMarkCore } from '@owomark/core';

const core = createOwoMarkCore({ initialMarkdown: '# Hello' });
console.log(core.getMarkdown());
```

## Building

From the project root:

```bash
npm run build:packages
```

This builds `@owomark/core` -> `@owomark/view` -> `@owomark/react` in dependency order using tsup (ESM + DTS).