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/tiptap

v0.17.1

Published

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

Readme

@octane-xplat/tiptap

The unified rich-text facade: one TiptapEditor component across web and native. Web renders a real tiptap Editor through @octanejs/tiptap's EditorContent; iOS and Android render the @octane-xplat/richtext leaf (WordPress Aztec on Android, AztecEditor-iOS on iOS) and layers tiptap document JSON on top through the DOM-free ProseMirror slices (@tiptap/pm, @tiptap/static-renderer) with a zeed-dom parse shim. No DOM-bound tiptap code (EditorView) ever enters a native bundle — divergence lives at the file-suffix boundary.

import { useState } from 'octane'
import { TiptapEditor, supported } from '@octane-xplat/tiptap'
import { Text } from '@octane-xplat/ui'

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

On web the web prop configures the underlying tiptap Editor: extensions appends caller Extension/Node/Mark entries after the built-in StarterKit, and starterKit takes StarterKit.configure() options or false to hand the schema to extensions entirely. A caller starterKit entry replaces the built-in instead of registering it twice. The options are web-only. iOS, Android, and AppKit do not run caller ProseMirror extensions, plugins, node views, or browser menus. A passed web option has no effect on those targets.

import { TiptapEditor } from '@octane-xplat/tiptap'
import { Mark } from '@tiptap/core'

const Spoiler = Mark.create({
	name: 'spoiler',
	parseHTML: () => [{ tag: 'span[data-spoiler]' }],
	renderHTML: () => ['span', { 'data-spoiler': '' }, 0],
})

export function Notes() {
	return <TiptapEditor web={{ extensions: [Spoiler] }} />
}

Engine and feature boundary

| Target | Editing engine | web options | Caller extensions, plugins, and browser views | | ------------ | ------------------------------------------------------------------------------------ | ----------------------------------- | ------------------------------------------------------------------------------ | | Web | Tiptap Editor + EditorContent | extensions and starterKit apply | Supported by the web Tiptap engine | | Android | WordPress Aztec AztecText; a DOM-free Tiptap bridge converts JSON to and from HTML | Ignored | Unsupported; no live ProseMirror view, plugin, node view, or browser menu runs | | iOS | AztecEditor-iOS through the native Swift facade; HTML-backed JSON conversion | Ignored | Unsupported; no caller JavaScript engine plugins run | | macOS AppKit | Bundled Tiptap editor in WKWebView | Ignored by the AppKit host | Unsupported through this facade |

On Android, keep HTML as the facade's canonical interchange. Tiptap JSON is a best-effort conversion over Aztec HTML, not a live ProseMirror document. The 2026-10-06 Aztec nested-tree run preserved nested wrappers, attributes, and child order through synthetic text edits and save/reopen. It also flattened paragraphs and accumulated <br> elements on reopen; it did not qualify Tiptap JSON, OS input, or structural tree commands. See the per-row capability record.

value/onChange exchange document HTML; editable controls whether the user can edit. getJSON/setJSON exchange tiptap document JSON: synchronous on web, bridged on native after onJSONReady(true) — the lazy bridge reports false (and getJSON() returns null) on runtimes that cannot host the schema modules. On native, HTML is the canonical interchange format; JSON output is a best-effort mapping of Aztec's flat span list onto ProseMirror's tree.

import { useRef, useState } from 'octane'
import { TiptapEditor, type TiptapEditorHandle } from '@octane-xplat/tiptap'
import { Button } from '@octane-xplat/ui'

export function DocumentCopy() {
	const editor = useRef<TiptapEditorHandle | null>(null)
	const [html, setHtml] = useState('<p>Trip notes</p>')
	const [jsonReady, setJsonReady] = useState(false)
	return (
		<>
			<TiptapEditor
				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>
		</>
	)
}

The tiptap family pins to 3.28.0 to match @octanejs/[email protected] — extension ^ ranges float ahead of core and break (getPreviousBlockSibling), so consumers should keep the workspace override. 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 { TiptapEditor, type TiptapJSON, type TiptapEditorHandle } from '@octane-xplat/tiptap'

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

export function replaceHTML(editor: TiptapEditorHandle) {
	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 { TiptapEditorHandle } from '@octane-xplat/tiptap'

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