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

@arrisa/editor

v1.0.29

Published

Arrisa — editor

Readme

@arrisa/editor

Arrisa view layer — DOM rendering, input, decorations, keymaps, and UI chrome (menus, panels, tooltips, dialogs).

Install

pnpm add @arrisa/editor

Depends on @arrisa/doc, @arrisa/state, @arrisa/command, and @arrisa/phrases. For undo/redo, add @arrisa/history. For a full schema + menus, use @arrisa/schema.

Quick start

import {Arrisa, menuBar, KeyBinding, placeholder} from "@arrisa/editor"
import {history, undo as histUndo, redo as histRedo} from "@arrisa/history"
import {Command, undo as cmdUndo, redo as cmdRedo, Menu} from "@arrisa/command"
import {basicSchema} from "@arrisa/schema"

let editor = Arrisa.create({
  parent: document.querySelector("#editor")!,
  doc: "<p>Hello</p>",
  config: [
    basicSchema(),
    history(),
    Command.handler(cmdUndo, (ed) => histUndo({state: ed.state})),
    Command.handler(cmdRedo, (ed) => histRedo({state: ed.state})),
    menuBar(),
    placeholder("Start typing…"),
    Arrisa.scrolling(320),
    Arrisa.label("Document editor"),
  ],
})

editor.focus()
editor.dispatch({
  changes: {from: 1, to: 1, insert: /* … */},
  selection: /* … */,
})

Features / concepts

View owns DOM; state stays pure

Arrisa mounts an outer wrapper, scroll container, and contenteditable surface. All logical updates go through editor.dispatch(Transaction | Spec). State updates immediately; DOM flush is scheduled on the next animation frame.

Default keymap

KeyBinding installs bindings. By default, the editor includes a full default keymap (Enter, delete unit/word, arrows, page, Home/End, select-all, Escape to collapse a range, Mod-z/y undo/redo stubs, macOS Ctrl-b/f/p/n, …). Disable with KeyBinding.useDefaultKeymap.of(false).

For single-field / compose UIs, use composeKeymap() instead: it turns off the default map and installs a compact subset (enter/break, delete, arrows, Home/End, select-all, Escape, undo/redo) without page motion or macOS emacs-style Ctrl bindings.

composeKeymap({submit: "Enter" | "Shift-Enter"}) omits that chord so the host can bind send. The other chord is always insertLineBreak. When submit is omitted, Enter still runs enter (split textblock) and Shift-Enter runs insertLineBreak. Arrisa does not send messages. Bind the omitted chord with EditorState.prec.high(KeyBinding.of({key, run})). A missing host binding may fall through to beforeinput (insertParagraph → enter).

On macOS, Option-Arrow moves by word and Cmd-Arrow moves to the visual line start/end (Shift extends the selection). Mod-Arrow is Ctrl-Arrow by-word on Windows/Linux. Those Cmd-Arrow line chords are in both the default keymap and composeKeymap.

A non-empty selection also collapses on pointerdown outside editor.dom, except on tooltips (arrisa-tooltip), the floating menu, the link prompt, and the menubar (arrisa-menubar / .arrisa-menubar, including registered MenuHost.dom roots).

Chrome

| Extension | Role | |-----------|------| | menuBar | Sticky top toolbar from Menu.Item sources (MenuBarConfig) | | floatingMenu | Selection toolbar (tooltip-positioned) (FloatingMenuConfig) | | embeddedMenu | Toolbar mounted into a host parent (not a Panel) (EmbeddedMenuConfig) | | Panel | Top/bottom panel host (used by menu bar, dialogs) | | Dialog | Modal form panels | | Tooltip | Positioned tooltips + hover sources | | placeholder | Empty-doc placeholder widget | | dropCursor | Caret while dragging over the editor |

Decorations

Point decorations (widgets, attrs, shapes at a position) and range decorations (wrappers/attrs over spans), plus tag-level shape/wrapper/widget/attribute overrides via Decoration.Tag.*. Schedule phrase paint uses class .arrisa-schedule-phrase and color var(--arrisa-schedule-phrase, #005bd1).

Input rules

InputRule auto-replaces after input.type (markdown-style shortcuts, wrapping, textblock type changes).

Main public API

Arrisa (view)

Create & lifecycle

| API | Description | |-----|-------------| | Arrisa.create(spec) | Create view (parent?, state? or doc/selection/config, scrollTo?) | | editor.dom / scrollDOM / contentDOM | Outer, scroll, and editable elements | | editor.state | Current EditorState | | editor.dispatch(tr \| spec) | Apply transaction(s); flushes DOM next frame | | editor.focus() | Focus content | | editor.hasFocus / composing | Focus and IME state |

Geometry

| API | Description | |-----|-------------| | moveToLineBoundary / moveVertically | Used by motion/delete commands | | domAtPos / posAtDOM / nodeDOM / nodeFromDOM | DOM ↔ document mapping | | posAtCoords / coordsAtPos / coordsForElement | Screen coordinates | | selectionRect | Viewport box for the selection (plain {top,left,width,height}), flushed, same turn as dispatch | | scheduleDOMRead / scheduleDOMWrite | Split layout read/write phases | | plugin(plugin) | Plugin value lookup |

Static facets & helpers (selected)

| API | Description | |-----|-------------| | Arrisa.htmlSanitize | XSS boundary for clipboard HTML — app must supply sanitizer for untrusted paste | | Arrisa.trustedHTMLPolicy | Trusted Types policy (Arrisa never creates an identity policy) | | Arrisa.clipboard* | Input/output filters and text serializers/parsers | | Arrisa.pasteHandler / dropHandler | Override paste/drop | | Arrisa.editable | contenteditable on/off | | Arrisa.updateListener | Post-update callbacks | | Arrisa.theme / styles / scrolling | Styling helpers | | Arrisa.label | aria-label on the editable | | Arrisa.editorAttributes / contentAttributes | DOM attrs | | Arrisa.scrollIntoView / scrollHandler | Scroll control | | Arrisa.coveredMargins | Chrome margins (shared with @arrisa/command) | | Arrisa.colorScheme / cursorBlinkRate | Appearance | | Arrisa.announce | Screen-reader announcements | | Arrisa.exceptionSink / logException | Extension error reporting | | Arrisa.Plugin | View plugins (define / fromClass) | | Arrisa.Update | Update object passed to plugins/listeners | | Arrisa.domEventHandler / domEventObserver | Content DOM events |

// Theme scoped to the editor
Arrisa.theme({
  "&": {fontFamily: "system-ui"},
  "arrisa-content": {padding: "8px"},
})

// Fixed height + scroll
Arrisa.scrolling(280)

KeyBinding

import {KeyBinding} from "@arrisa/editor"
import {Command, toggleStrong} from "@arrisa/command"

KeyBinding.of({
  key: "Mod-b",
  run: toggleStrong,
})

// Platform-specific / shift-extend
KeyBinding.of({
  key: "ArrowLeft",
  run: Command.bind(moveByUnit, {dir: "left"}),
  shift: Command.bind(moveByUnit, {dir: "left", extend: true}),
})

// Disable defaults
KeyBinding.useDefaultKeymap.of(false)

// Include defaults as extensions if rebuilding the map yourself
KeyBinding.defaultKeymap.map((b) => b.extension)

// Compact map for compose / single-field UIs (disables defaults)
import {composeKeymap, type ComposeKeymapConfig} from "@arrisa/editor"
import {EditorState} from "@arrisa/state"
composeKeymap() // Enter = enter, Shift-Enter = insertLineBreak
let submitEnter: ComposeKeymapConfig = {submit: "Enter"}
composeKeymap(submitEnter) // Enter unbound; Shift-Enter = insertLineBreak
EditorState.prec.high(KeyBinding.of({key: "Enter", run: hostSend}))

Mod is Meta on macOS and Ctrl elsewhere. Specs support key, mac, win, linux, char, run, shift, scope, allowDefault.

Decorations

| Export | Description | |--------|-------------| | Decoration | Point / Range factories + Decoration.Tag shape/wrapper/widget/attribute | | Widget | Embed non-editable DOM (define, create, Text, EditableText) | | PointSet | Ordered zero-width decoration set | | RangeSet | Ordered non-overlapping range set |

import {Decoration, Widget, PointSet} from "@arrisa/editor"
import {EditorState} from "@arrisa/state"

let markType = Widget.define<{label: string}>({
  render: (v) => {
    let el = document.createElement("span")
    el.textContent = v.label
    el.className = "pg-mark"
    return el
  },
})

// Point decoration source facet
Decoration.Point.source.of((state) =>
  PointSet.create([[state.selection.head, Decoration.Point.widget(markType.of({label: "×"}))]]),
)

Tag-level overrides (by node type):

Decoration.Tag.shape(myNodeType, Elt.create("div", {class: "custom"}, [/* content hole */]))
Decoration.Tag.wrapper(myNodeType, wrapperElt)
Decoration.Tag.widget(myNodeType, "after", myWidget)
Decoration.Tag.attribute(myNodeType, "class", "foo")

Panels & menus

import {
  Panel,
  menuBar,
  floatingMenu,
  embeddedMenu,
  defaultFloatingWhen,
  type MenuBarConfig,
  type FloatingMenuConfig,
  type EmbeddedMenuConfig,
} from "@arrisa/editor"
import {Menu} from "@arrisa/command"

// Sticky bar (default template: Menu.Group.top) — MenuBarConfig
menuBar({
  // template: Menu.Group.top.template(),
  // theme: false, // supply your own CSS
})

// Floating selection toolbar — FloatingMenuConfig
floatingMenu({
  template: Menu.Group.inline.template(),
  above: true,
  when: defaultFloatingWhen, // non-empty selection
  hideOnBlur: true,
})

// Toolbar in a host parent (not a Panel; no bottom border in the default theme)
let embed: EmbeddedMenuConfig = {
  parent: toolbarEl,
  // template: Menu.Group.top.template(),
  // theme: false, // supply your own CSS
}
embeddedMenu(embed)

// Custom panel
Panel.show.of((editor) => ({
  dom: document.createElement("div"),
  top: true,
  update(update) {/* … */},
}))

Panel.configure({topContainer: externalEl})

Dialog

import {Dialog} from "@arrisa/editor"

let {close, result} = Dialog.show(editor, {
  label: "URL",
  submitLabel: "Insert",
  // content: (ed, close) => formElement,
})
let form = await result // HTMLFormElement | null
// close is an effect; auto-dispatched after result settles if still open

Tooltip

import {Tooltip} from "@arrisa/editor"

// Facet-driven tooltips
Tooltip.show.of((state) => ({
  pos: state.selection.head,
  above: true,
  create: (editor) => ({dom: tipElement}),
}))

Tooltip.configure({position: "absolute"})

// Hover tooltips
let {extension, active} = Tooltip.hover((editor, pos, side) => {
  return {pos, create: () => ({dom: hoverEl})}
}, {hoverTime: 300, hideOnChange: true})

Input rules

import {InputRule} from "@arrisa/editor"

// Replace match with string or custom apply
InputRule.define({
  expr: /--$/,
  apply: "—",
})

// Wrap block (e.g. `> ` → blockquote)
InputRule.wrapping(/^> $/, blockquoteTag)

// Change textblock type (e.g. `# ` → heading)
InputRule.textblockType(/^# $/, headingTag)

Rules run after input.type; first successful match wins. Default skips code-role ranges unless inCode: true.

Placeholder & drop cursor

import {placeholder, dropCursor} from "@arrisa/editor"

placeholder("Message…")
// or placeholder(() => document.createElement("span"))

dropCursor()

Layering / related packages

| Package | Role | |---------|------| | @arrisa/doc | Document model, HTML I/O | | @arrisa/state | State, transactions, extensions | | @arrisa/command | Commands + Menu model (this package renders them) | | @arrisa/phrases | Labels / a11y phrase sets | | @arrisa/history | Undo/redo (not a direct dependency — wire yourself) | | @arrisa/schema | Ready-made schema + menu items + keymaps | | @arrisa/types | Schema elements used by schema package |

Layer: view layer (3). Depends on command/phrases; must not be imported by doc/state/command/history.

Security

Schema validation is not an XSS boundary. Untrusted HTML (paste, string docs, drag-and-drop) requires an app-supplied sanitizer:

import {Arrisa} from "@arrisa/editor"
// import DOMPurify from "dompurify"

let sanitize = (html: string) => DOMPurify.sanitize(html)

Arrisa.create({
  parent,
  doc: sanitize(userHtml),
  config: [
    Arrisa.htmlSanitize.of(sanitize),
    // Arrisa.trustedHTMLPolicy.of(myPolicy), // if CSP enforces Trusted Types
  ],
})

See the monorepo SECURITY.md for paste filters, URL policies, and collab remote-effect defaults.

License

MIT