Skip to content

Design Decisions

Jayden Smith edited this page Sep 6, 2026 · 5 revisions

Design Decisions

One handle owns one document session

NativeEditorDocumentHandle is the source of document identity, initial state, schema, policy, limits, collaboration attachment, and lifecycle. This prevents an editor view, a headless hook, and a collaboration controller from creating divergent native sessions for the same document.

The consequence is intentional: initial content and schema are creation config, not component props. An app keeps the handle stable and destroys it with its document owner. See Content and State.

Rust owns semantic truth

Schema validity, JSON/HTML conversion, command application, selection normalization, history, render preparation, snapshots, and Yjs state have one implementation in Rust. Android and iOS own platform interaction and presentation, but they apply Rust-authorized state rather than maintaining independent document models.

This keeps persistence and collaboration behavior aligned across platforms and gives boundary failures one typed vocabulary.

Native editing, not a WebView

The editing surface uses platform text input, keyboard behavior, selection/caret handling, accessibility, and native image/layout facilities. That avoids a DOM/WebView dependency while preserving the interaction conventions users expect on iOS and Android.

Ordinary prose is native text rather than a React view per paragraph. EditorStyleSheet gives that content a typed interface close to React Native styles, while style controls the outer view. Custom atoms are the exception: their registered React components mount within document layout.

Android owns text layout and atom placement

Per-element physical borders, padding, and margins require shaping paragraphs at their actual available widths. Android’s owned surface uses paragraph layouts and a direct native input connection while retaining Rust-authorized editing. Real atom children share the native scroll content, so native drawing and touch dispatch follow their measured bounds. Matching loaded images survive unrelated full refreshes to avoid transient placeholder heights and scroll clamping.

Optional capabilities stay optional

The addon array is a foundation for extension composition, with explicit supported capabilities and duplicate rejection. It is not yet an arbitrary plugin API. Syntax highlighting lives in a separately installed native package so the base editor does not carry syntect and syntax assets for applications that do not opt in.

Explicit controlled-content semantics

HTML and JSON are supported as controlled inputs, but value wins over valueJSON. JSON applies have a deliberate history choice: replace creates one undoable boundary and reset discards pending native input/composition and clears history. A revision mismatch refreshes the real engine state before a controlled effect can reapply; no operation is replayed against guessed document positions.

Room collaboration deliberately omits controlled document bindings. The native/Rust room session owns document updates, while React observes state and renders native-derived remote selections.

Typed, serializable toolbar configuration

Toolbar items and icon descriptors must travel to native keyboard toolbars as well as the React inline toolbar. The union therefore uses explicit mark/node/command/action items and default, glyph, or platform icon descriptors instead of arbitrary React elements.

String schema names are a deliberate extension point: custom schemas can add marks/nodes without a package enum release. Runtime capability from ActiveState decides whether a configured control is actionable.

Native and custom toolbar appearance are separate axes

toolbarPlacement decides where an editor toolbar is rendered: keyboard means a native platform toolbar; inline means React EditorToolbar. EditorToolbarAppearance decides chrome defaults: custom or native. Separating placement from appearance lets an app keep behavior portable while choosing a platform-oriented visual treatment.

Native appearance is not a portable caller-controlled geometry mode. On iOS it normally forces the bar border width to one physical pixel (1 / UIScreen.main.scale), ignoring borderWidth; with a Swift 6.2+ build on iOS 26+, non-empty mention buttons activate transparent mention chrome, which forces that width to 0. Earlier supported iOS uses a supplied bar borderRadius or a 20 fallback, while buttonBorderRadius remains caller-controlled with a 10 fallback; with a Swift 6.2+ build on iOS 26+, native capsule corner configuration with maximum radius 24 replaces the bar borderRadius. Android native appearance forces a zero stroke/border width, bar borderRadius 32, and buttonBorderRadius 20, ignoring those supplied geometry fields. Use custom appearance with explicit tokens when the app needs app-defined radius values across platforms.

The viewer is a dedicated Fabric renderer

Read-only rendering does not reuse an editable document handle. RichTextViewer compiles/prepares native layout from an HTML/JSON source and integrates with Fabric measurement, prepared-layout caching, drawing, and native accessibility/interaction.

This keeps list measurement and rendering independent of an editor session, but requires New Architecture and a finite width. Registered React atoms can be interactive; source changes remain controlled by the application through onUpdateAtomAttrs, without an editor history or document handle.

Collaboration ownership is deliberately narrow

The package owns its native WebSocket/Yjs client transport and exposes status, peers, snapshots, awareness, and an optional authentication prelude. The application owns server implementation, credentials, authorization, persistence, screen state, and recovery UX.

The optional protocol adapter is attempt-scoped so it can read current credentials for a reconnect without persisting sensitive values in native code. The adapter gates Yjs traffic until it returns ready; it does not take ownership of reconnect policy.

Limits are budgets, not sanitizers

Resource, editing, collaboration, and image-loading limits bound work at the JavaScript/native/Rust boundary. They do not make untrusted input safe by themselves and do not replace URL allowlists, server-side validation, compatible schema migrations, or a product recovery policy.

The package distinguishes legacy prop-resolution errors, typed document API imperative errors, and viewer callback errors so callers can respond at the correct boundary. See Production Limits and Errors.

Related pages

Clone this wiki locally