npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@octane-xplat/lexical

v0.19.0

Published

Unified rich-text editor facade for Octane xplat — live lexical on web/AppKit, Aztec + lexical document model on iOS/Android

Readme

@octane-xplat/lexical

Rich-text editing for Octane xplat apps through the Lexical document model — one LexicalEditor component on web and native. Web renders a fixed-plugin LexicalComposer through @octanejs/lexical. iOS and Android render the @octane-xplat/richtext leaf (AztecEditor-iOS / WordPress Aztec) and convert Lexical serialized state to and from HTML through the DOM-free packages (lexical, @lexical/html, the Aztec-shaped node packages) with a zeed-dom parse shim — no live Lexical editor runs there. macOS AppKit mounts a bundled Lexical editor in a WKWebView.

import { useState } from 'octane'
import { LexicalEditor, supported } from '@octane-xplat/lexical'
import { Text } from '@octane-xplat/ui'

export function Notes() {
	const [html, setHtml] = useState('<p>Trip notes</p>')
	return supported ? (
		<LexicalEditor value={html} onChange={setHtml} />
	) : (
		<Text>Editing is unavailable on this target</Text>
	)
}

value/onChange exchange document HTML. getJSON/setJSON exchange lexical serialized editor state (EditorState.toJSON()): synchronous on web, bridged on iOS/Android after onJSONReady(true) — the lazy bridge reports false (and getJSON() returns null) on runtimes that cannot host the document-model modules. On Android, HTML is the canonical interchange format; serialized state is a best-effort mapping. editable={false} renders a read-only document, placeholder shows hint text on an empty document, and autofocus focuses the field on mount.

import { LexicalEditor } from '@octane-xplat/lexical'

export function ReadOnlyNote({ html }: { html: string }) {
	return <LexicalEditor value={html} editable={false} />
}
import { useRef, useState } from 'octane'
import { LexicalEditor, type LexicalEditorHandle } from '@octane-xplat/lexical'
import { Button } from '@octane-xplat/ui'

export function DocumentCopy() {
	const editor = useRef<LexicalEditorHandle | null>(null)
	const [html, setHtml] = useState('<p>Trip notes</p>')
	const [jsonReady, setJsonReady] = useState(false)
	return (
		<>
			<LexicalEditor
				value={html}
				onChange={setHtml}
				ref={(handle) => {
					editor.current = handle
				}}
				onReady={() => setJsonReady(editor.current?.getJSON() != null)}
				onJSONReady={setJsonReady}
			/>
			<Button
				isDisabled={!jsonReady}
				clickAction={() => {
					const document = editor.current?.getJSON()
					if (document) editor.current?.setJSON(document)
				}}
			>
				Round-trip document
			</Button>
		</>
	)
}

Web customization

On web, web.nodes adds custom Lexical node classes to the facade's built-in nodes, and web.plugins renders additional Octane plugin components inside the Lexical composer. Set replaceNodes to true when web.nodes should be the full custom node registry. Set replacePlugins to true to replace the default history, list, link, and autofocus plugins with your own composition; the rich-text surface and facade change/ref bindings remain installed. This lets a custom node keep its Lexical JSON type and HTML representation during a round-trip.

import { LexicalEditor } from '@octane-xplat/lexical'
import { BadgeNode, BadgePlugin } from './lexical-extensions'

export function NoteEditor() {
	return (
		<LexicalEditor
			value="<p><span data-badge>Voyager</span></p>"
			web={{ nodes: [BadgeNode], plugins: <BadgePlugin /> }}
		/>
	)
}
import { LexicalEditor } from '@octane-xplat/lexical'
import { BadgeNode, BadgePlugin } from './lexical-extensions'

// Full replacement: register every node class Lexical needs for your documents.
export function CustomEditor() {
	return (
		<LexicalEditor
			web={{
				replaceNodes: true,
				nodes: [BadgeNode],
				replacePlugins: true,
				plugins: <BadgePlugin />,
			}}
		/>
	)
}

The web options are ignored on native, where Aztec does the editing and the fixed headless node set handles JSON conversion: no caller plugins, custom nodes, browser views, or arbitrary JavaScript transforms run there.

Engine and feature boundary

| Target | Editing engine | web options | Caller plugins, custom nodes, and browser views | | ------------ | ------------------------------------------------------------------------------------------------- | ----------------------------------------------- | ----------------------------------------------------------------------------------------- | | Web | Lexical LexicalComposer with the facade's default nodes and plugins | nodes, plugins, and replacement flags apply | Supported by the web Lexical engine | | Android | WordPress Aztec AztecText; a headless Lexical editor converts serialized state to and from HTML | Ignored | Unsupported; no live Lexical editor, plugin, transform, custom node, or browser view runs | | iOS | AztecEditor-iOS through the native Swift facade; HTML-backed JSON conversion | Ignored | Unsupported; no caller JavaScript engine plugins run | | macOS AppKit | Bundled Lexical editor in WKWebView | Ignored by the AppKit host | Unsupported through this facade |

On Android, save HTML as the canonical document. Lexical serialized state is a best-effort conversion through the fixed built-in node set; it is not a live Lexical editor state that supports commands or plugins. There is no live LexicalEditor on Android: no dispatchCommand, no node transforms; native returns the AztecText and the headless editor used for conversions is an internal detail. The 2026-10-06 Aztec nested-tree run preserved nested wrappers, attributes, and child order through synthetic text edits and save/reopen, but flattened paragraphs and accumulated <br> elements on reopen. That run exercised the shared Aztec HTML leaf, not Lexical's JSON conversion or structural commands. See the per-row capability record.

Focus callbacks report focus entering or leaving the editable surface. onJSONReady(true) fires once the web editor handle exists, so getJSON() is available from onReady and after readiness. On native, onJSONReady still reports when the lazy conversion bridge settles.

<LexicalEditor
	onJSONReady={(ready) => console.log('JSON ready:', ready)}
	onFocus={() => console.log('Editor focused')}
	onBlur={() => console.log('Editor blurred')}
/>

The Lexical family pins to 0.52.0 and @octanejs/lexical to 0.2.12 (peers octane ^0.16.0). Keep the framework's managed overrides so the adapter and your app share one engine. Starters include them; existing apps receive them through the upgrade command:

bunx @octane-xplat/cli@latest upgrade
bun install

@lexical/link's ICU-dependent URL-matcher literal is compiled away at bundle time by the xplatNative unicode-regex transform. See docs/app/rich-text.md for the shared contract, format vocabulary, and known limits.

macOS AppKit

The macOS export mounts a bundled local editor document in WKWebView. RichText uses StarterKit; Tiptap and Lexical use their existing web facades. Wait for onReady; synchronous getters return the latest received snapshot and commands cross WebKit asynchronously. The native handle is the Swift host transport. See AppKit setup and engine limits.

Native content and format limits

On iOS and Android, json takes precedence over value, including later HTML prop updates. Removing json returns control to value. An imperative setHTML cancels a pending JSON replacement; host recreation preserves the latest requested or edited content. These lifecycle rules have object-driver regression coverage, separate from native keyboard-input qualification.

import { LexicalEditor, type LexicalJSON, type LexicalEditorHandle } from '@octane-xplat/lexical'

export function Notes({ json }: { json?: LexicalJSON }) {
	return <LexicalEditor json={json} value="<p>HTML when JSON is absent</p>" />
}

export function replaceHTML(editor: LexicalEditorHandle) {
	editor.setHTML('<p>Replacement, including before JSON readiness</p>')
}

Use the platform format subset when building a toolbar. On iOS/Android, unsupported apply requests throw RangeError before changing content. Links require linkTo; iOS lacks taskList and alignment. Native JSON is a fixed-schema conversion through HTML and cannot preserve arbitrary caller nodes or run engine plugins.

import type { LexicalEditorHandle } from '@octane-xplat/lexical'

export function insertLink(editor: LexicalEditorHandle) {
	editor.linkTo('https://example.com', 'Example')
}