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

@lodado/sdui-document

v1.0.0

Published

Headless SDUI block document domain package.

Readme

@lodado/sdui-document

Headless block document domain for SDUI — schema, patches, permissions, and SDUI adapter without editor or database coupling.

npm version License: MIT TypeScript GitHub

Block Document Headless Patches

Quick start · Philosophy · Architecture · Patches · Adapters · Development


@lodado/sdui-document owns what a document means: block tree semantics, patch-based edits, permission policy, autosave state, and conversion to SDUI layout shape. It deliberately excludes MobX, ProseMirror, Yjs, React, and concrete storage.

SduiDocumentContent → patches → applyDocumentPatch → toSduiLayoutDocument → SduiLayoutRenderer

Pairs with:

| Package | Role | | ----------------------------- | -------------------------------------------------- | | @lodado/sdui-document | Document meaning, tree rules, patches | | @lodado/sdui-template | SDUI normalization, subscriptions, React rendering | | @lodado/sdui-document-react | React block editor UI |

End-to-end example

| ① block tree | → | ② SDUI adapter | → | ③ render | | ---------------- | --- | ------------------ | --- | ------------ |

import { SduiLayoutRenderer } from '@lodado/sdui-template'
import { sduiComponents } from '@lodado/sdui-template-component'
import { createDocumentBlock, toSduiLayoutDocument, type SduiDocumentContent } from '@lodado/sdui-document'

const content: SduiDocumentContent = {
  schemaVersion: '1.0',
  root: createDocumentBlock({
    id: 'root',
    type: 'document.root',
    children: [
      createDocumentBlock({
        id: 'heading',
        type: 'document.heading',
        state: { text: 'Project notes', level: 1 },
      }),
      createDocumentBlock({
        id: 'paragraph',
        type: 'document.paragraph',
        state: { text: 'Rendered through SDUI.' },
      }),
    ],
  }),
}

const layoutDocument = toSduiLayoutDocument(content, {
  documentId: 'doc-1',
  title: 'Project notes',
})

export function DocumentPreview() {
  return <SduiLayoutRenderer document={layoutDocument} components={sduiComponents} />
}

Table of Contents


Why this exists

Notion-like products need a stable document model that survives renderer, backend, and editor changes. Coupling document semantics to ProseMirror transactions or React state makes testing and server-side validation painful.

This package provides a clean-room contract: pure functions, immutable patches, and adapter interfaces for persistence, storage, search, and future collaboration.

Philosophy

1. Documents are domain data, not editor internals

SduiDocument
└─ SduiDocumentContent
   └─ SduiDocumentBlock
      ├─ document.heading
      ├─ document.paragraph
      ├─ document.checklist
      └─ document.link

No ProseMirror transactions, MobX observables, Yjs updates, or DOM selection in the public API.

2. SDUI renders; this package defines meaning

toSduiLayoutDocument lowers semantic blocks into an SDUI-compatible shape. @lodado/sdui-template renders it.

3. Patch-based editing

block.insert | block.update | block.delete | block.move

Enables autosave, optimistic UI, version conflicts, and future collaboration without a rich-text engine.

4. Block-level first

Deferred until proven necessary: character-level CRDT, ProseMirror plugins, complex inline marks, table cell selection, concrete storage/search backends.

5. Pure functions first

Patch application, tree moves, permissions, autosave, traversal, and text extraction are pure TypeScript — safe in client, server, and worker contexts.


Architecture

src/
  schema/          Public document, block, patch, event, workspace types
  content/         Traversal, plain text, link extraction
  blocks/          Patch engine and block-level errors
  tree/            Document parent/collection tree operations
  permissions/     Pure role/action policy
  autosave/        Autosave reducer / state machine
  repositories/    Persistence contract
  storage/         Attachment storage contract
  search/          Search indexer contract
  collaboration/   Future collaboration adapter contract
  sdui/            Document content → SDUI layout adapter

| Layer | Responsibility | Should not do | | ------------- | ---------------------------- | ------------------------ | | schema | Stable public contracts | Runtime persistence | | blocks | Apply patches | Talk to APIs or React | | tree | Move/archive document nodes | Edit block content | | permissions | Actor/action decisions | Trust client-only checks | | autosave | Track save state | Timers or network calls | | sdui | Convert blocks to SDUI nodes | Own document semantics |


Installation

Monorepo workspace:

{
  "dependencies": {
    "@lodado/sdui-document": "workspace:*"
  }
}
pnpm install

Quick start

See end-to-end example above.


Creating document content

import { createDocumentBlock, type SduiDocumentContent } from '@lodado/sdui-document'

const content: SduiDocumentContent = {
  schemaVersion: '1.0',
  root: createDocumentBlock({
    id: 'root',
    type: 'document.root',
    children: [
      createDocumentBlock({
        id: 'todo-1',
        type: 'document.checklist',
        state: { text: 'Ship headless core', checked: true },
      }),
      createDocumentBlock({
        id: 'callout-1',
        type: 'document.callout',
        state: { text: 'Keep rich text out until proven.' },
      }),
    ],
  }),
}

Supported block types

All types are defined in src/block-types/ and registered in BLOCK_TYPE_MODULES:

| Type | Description | | ----------------------------------------- | ------------------------------------------------ | | document.root | Document root | | document.paragraph | Body text (state.text) | | document.heading | Heading levels 1–3 (state.level, state.text) | | document.bulletedList | Bulleted list item | | document.numberedList | Numbered list item | | document.checklist | Checkbox item (state.checked, state.text) | | document.quote | Block quote | | document.callout | Callout / notice block | | document.code | Code block | | document.divider | Horizontal rule | | document.toggle | Collapsible section (children nested inside) | | document.image | Image | | document.video | Video | | document.embed | Embed / iframe | | document.file | File attachment | | document.link | Link to another document | | document.bookmark | URL bookmark preview | | document.columnList / document.column | Multi-column layout | | document.collection | Database / table / gallery | | document.page | Sub-page link | | document.toc | Table of contents | | document.tags | Tag chips | | document.button | Action button | | document.sdui | Embedded SDUI layout inside a block |

// Legacy union excerpt — see blocks/schema/block.ts for the authoritative list
type SduiDocumentBlockType = 'document.root' | 'document.paragraph' | 'document.heading'
// … see table above for full set

Applying block patches

Immutable — returns new content, never mutates input.

import { applyDocumentPatch, createDocumentBlock } from '@lodado/sdui-document'

// Insert
const next = applyDocumentPatch(content, {
  type: 'block.insert',
  parentId: 'root',
  before: 'existing-block-id',
  block: createDocumentBlock({
    id: 'new-paragraph',
    type: 'document.paragraph',
    state: { text: 'A newly inserted block.' },
  }),
})

// Update
applyDocumentPatch(next, {
  type: 'block.update',
  blockId: 'new-paragraph',
  state: { text: 'Updated text' },
})

// Move
applyDocumentPatch(next, {
  type: 'block.move',
  blockId: 'new-paragraph',
  parentId: 'root',
  after: null,
})

// Delete
applyDocumentPatch(next, {
  type: 'block.delete',
  blockId: 'new-paragraph',
})

// Split text block at inline offset
applyDocumentPatch(next, {
  type: 'block.split',
  blockId: 'new-paragraph',
  offset: 5,
  newBlockId: 'split-block',
})

// Merge into previous block
applyDocumentPatch(next, {
  type: 'block.merge',
  blockId: 'split-block',
  intoBlockId: 'new-paragraph',
})

// Turn into another block type
applyDocumentPatch(next, {
  type: 'block.setType',
  blockId: 'new-paragraph',
  blockType: 'document.heading',
  attributes: { level: 2 },
})

Block ordering (schema 1.1)

Sibling order uses fractional position strings. Anchors replace numeric indices:

| Anchor | Meaning | | ----------------------- | --------------------------------------- | | after: null | Prepend as first child | | before: null | Append as last child | | after: 'block-id' | Insert after sibling | | before: 'block-id' | Insert before sibling | | fallbackAfter: ['id'] | Offline replay when after was deleted |

Schema 1.0 documents migrate to 1.1 lazily on first patch.

Errors: BlockNotFoundError, ParentBlockNotFoundError, InvalidBlockMoveError, RootBlockCannotBeDeletedError


Document tree operations

Manage relationships between documents (not blocks inside a document):

import { moveDocument, archiveDocumentSubtree, restoreDocumentSubtree } from '@lodado/sdui-document'

moveDocument({
  documents,
  documentId: 'doc-a',
  targetParentDocumentId: 'doc-b',
  targetIndex: 0,
})

archiveDocumentSubtree({ documents, documentId: 'doc-a' })

Errors: DocumentNotFoundError, InvalidDocumentDestinationError


Permission policy

Pure decisions from actor role + action:

import { canPerformDocumentAction, getDocumentAccessMode } from '@lodado/sdui-document'

canPerformDocumentAction({ actor, action: 'read' }) // boolean
getDocumentAccessMode({ actor }) // 'none' | 'readOnly' | 'editable'

| Actor role | Read | Update | | ------------------ | ---- | ------ | | workspace admin | yes | yes | | collection manager | yes | yes | | collection editor | yes | yes | | collection viewer | yes | no | | document editor | yes | yes | | guest (no role) | no | no |

Client checks are UX gating only. Server adapters must re-check before writes.


Autosave state machine

Pure reducer — no timers, debounce, or fetch:

import { createInitialAutosaveState, reduceAutosaveState } from '@lodado/sdui-document'

let state = createInitialAutosaveState()
state = reduceAutosaveState(state, { type: 'local.change', patchCount: 1 })
state = reduceAutosaveState(state, { type: 'save.request' })
state = reduceAutosaveState(state, {
  type: 'save.success',
  acknowledgedVersion: state.localVersion,
})

Statuses: idle | dirty | saving | saved | failed | offline

  • Stale save success is ignored
  • Save failure preserves pending patches
  • Offline edits return to dirty when back online

Content helpers

import { extractPlainText, extractDocumentLinks, walkDocumentBlocks } from '@lodado/sdui-document'

extractPlainText(content) // plain text for search/preview
extractDocumentLinks(content) // backlink extraction
walkDocumentBlocks(content, fn) // depth-first traversal

Adapter contracts

Interfaces only — no concrete implementations:

| Interface | Purpose | | ---------------------------------- | -------------------------------------------- | | SduiDocumentRepository | getDocument, savePatches, moveDocument | | SduiDocumentAttachmentStorage | Upload/download URLs | | SduiDocumentSearchIndexer | Index, remove, search | | SduiDocumentCollaborationAdapter | Future block-level collaboration |

Implement for REST, Next.js routes, IndexedDB, Postgres, S3, or search providers.


Recommended application flow

1. Load SduiDocument from repository/API
2. Check access with canPerformDocumentAction
3. Render via toSduiLayoutDocument + SduiLayoutRenderer
4. Convert UI edits → SduiDocumentPatch
5. Apply locally with applyDocumentPatch (optimistic UI)
6. Persist patches through SduiDocumentRepository
7. Track save status with reduceAutosaveState
8. Extract text/links for search as async side effect

Storybook: apps/docs/src/stories/SduiDocument.stories.tsx


For AI assistants

MCP (@lodado/sdui-mcp) covers SDUI layout JSON, not this package. Read this README and docs/AI-ASSISTANT-GUIDE.md for block document work.

When to use this package

  • Server-side or client-side patch validation before save
  • Headless document transforms (no React, no ProseMirror)
  • Permission checks with canPerformDocumentAction
  • Autosave state machine with reduceAutosaveState
  • Converting block content → SDUI layout with toSduiLayoutDocument

Do not use this package for

  • React editor UI → @lodado/sdui-document-react
  • Rendering SDUI JSON → @lodado/sdui-template
  • Arbitrary rich-text engine APIs → patches + block schema only

Authoring checklist

  1. Root block is always type: 'document.root'
  2. Unique id on every block
  3. Text blocks: state.text (string) for initial/simple content
  4. Use anchor placement (after / before), not array indices
  5. Apply patches immutably — never mutate content in place
  6. Re-validate permissions on the server before persisting

Key exports

// Content
createDocumentBlock, type SduiDocumentContent, type SduiDocumentBlock

// Patches
applyDocumentPatch, type SduiDocumentPatch

// Tree (documents, not blocks)
moveDocument, archiveDocumentSubtree, restoreDocumentSubtree

// Permissions & autosave
canPerformDocumentAction, getDocumentAccessMode
createInitialAutosaveState, reduceAutosaveState

// Content helpers
extractPlainText, extractDocumentLinks, walkDocumentBlocks

// SDUI bridge
toSduiLayoutDocument

// Contracts (implement in your app)
type SduiDocumentRepository
type SduiDocumentAttachmentStorage
type SduiDocumentSearchIndexer

MCP connection (for SDUI layout inside document.sdui blocks)

If your block document embeds SDUI widgets (document.sdui blocks), connect @lodado/sdui-mcp for layout JSON authoring — see root README MCP section.


Development

pnpm --filter @lodado/sdui-document test
pnpm --filter @lodado/sdui-document lint
pnpm --filter @lodado/sdui-document build

Design constraints

Do not add yet

  • React editor components
  • Concrete DB / storage / search clients
  • Yjs/Hocuspocus, ProseMirror, MobX

One-line rule

sdui-document owns meaning.
sdui-template owns rendering.
patches express edits.
adapters connect infrastructure.

License

MIT