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

@sqlrooms/documents

v0.29.0

Published

Artifact-scoped Markdown documents, structured block documents, and knowledge-index utilities for SQLRooms.

Readme

@sqlrooms/documents

Artifact-scoped Markdown documents, structured block documents, and knowledge-index utilities for SQLRooms.

See the Blocks and Block Documents developer guide for the conceptual model, ownership rules, and a focused host setup.

Usage

import {
  BlockDocumentArtifact,
  BlockDocumentsSliceConfig,
  BlockDocumentChartRendererProvider,
  BlockDocumentStatefulBlockRendererProvider,
  MarkdownDocumentsSliceConfig,
  buildKnowledgeIndex,
  createBlockDocumentCommands,
  createBlockDocumentFeatureSlices,
  createMarkdownDocumentCommands,
  createMarkdownDocumentsSlice,
  createMarkdownDocumentBlockDefinition,
} from '@sqlrooms/documents';
import {createDocumentsCrdtMirror} from '@sqlrooms/documents/crdt';
import {
  createArtifactTypeFromStatefulBlock,
  defineArtifactTypes,
} from '@sqlrooms/artifacts';

const markdownBlockDefinition = createMarkdownDocumentBlockDefinition();

const artifactTypes = defineArtifactTypes({
  'markdown-document': createArtifactTypeFromStatefulBlock(
    markdownBlockDefinition,
  ),
  'block-document': {
    label: 'Block Document',
    defaultTitle: 'Block Document',
    component: BlockDocumentArtifact,
    onCreate: ({artifactId, store}) => {
      store.getState().blockDocuments.ensureBlockDocument(artifactId);
    },
    onEnsure: ({artifactId, store}) => {
      store.getState().blockDocuments.ensureBlockDocument(artifactId);
    },
    onDelete: ({artifactId, store}) => {
      store.getState().blockDocuments.removeBlockDocument(artifactId);
    },
  },
});

const roomStore = createRoomStore(
  persistSliceConfigs(
    {
      name: 'my-room',
      sliceConfigSchemas: {
        markdownDocuments: MarkdownDocumentsSliceConfig,
        blockDocuments: BlockDocumentsSliceConfig,
      },
    },
    (set, get, store) => ({
      ...createMarkdownDocumentsSlice()(set, get, store),
      ...createBlockDocumentFeatureSlices({
        onDeleteOwnedStatefulBlock: ({
          blockType,
          blockInstanceId,
          getState,
        }) => {
          if (blockType === 'dashboard') {
            getState().mosaicDashboard.removeDashboard(blockInstanceId);
          }
        },
      })(set, get, store),
    }),
  ),
);

MarkdownDocument uses the Tiptap-backed MarkdownDocumentEditor. It keeps Markdown as the controlled value, renders a rich document editing surface, and keeps the existing CodeMirror source panel for direct Markdown edits.

MarkdownDocumentEditor is also exported as a reusable controlled editor:

<MarkdownDocumentEditor
  value={markdown}
  assets={assets}
  onChange={setMarkdown}
/>

The rich editor is the primary surface. The optional Markdown source panel can be opened alongside it and edits the same canonical Markdown string:

<MarkdownDocumentEditor
  value={markdown}
  onChange={setMarkdown}
  sourcePanelOpen={showSource}
  onSourcePanelOpenChange={setShowSource}
/>

Markdown artifacts can reference artifact-owned assets with asset:// URLs:

![Revenue by week](asset://chart-revenue-week)

Pass the artifact asset map to MarkdownDocumentEditor to render those links as browser-loadable image data while preserving the canonical asset:// link in Markdown source. MarkdownDocument handles this automatically for artifacts stored in the markdownDocuments slice.

The markdownDocuments slice exposes upsertAsset, removeAsset, and getAsset for managing image assets alongside Markdown content. SVG assets may use utf8 or base64 encoding; PNG assets must use base64 encoding.

Block Documents

createBlockDocumentsSlice() exposes structured state for artifact types backed by composable blocks: text, lists, images, standalone Mosaic/vgplot charts, and direct stateful blocks such as dashboards, pivots, or Markdown documents.

The shared block vocabulary lives in @sqlrooms/blocks. @sqlrooms/documents builds on those contracts with the concrete Tiptap-backed BlockDocument editor, persistence slice, commands, and AI authoring helpers.

Block documents persist Tiptap/ProseMirror JSON as their canonical content and provide block DTO helpers for command and AI authoring surfaces:

import {
  BlockDocumentsSliceConfig,
  createAddBlockDocumentTextBlockTool,
  createBlockDocumentFeatureSlices,
  createListBlockDocumentBlocksTool,
  createMoveBlockDocumentBlockTool,
} from '@sqlrooms/documents';

const roomStore = createRoomStore(
  persistSliceConfigs(
    {
      name: 'my-room',
      sliceConfigSchemas: {
        blockDocuments: BlockDocumentsSliceConfig,
      },
    },
    (set, get, store) => ({
      ...createBlockDocumentFeatureSlices()(set, get, store),
    }),
  ),
);

Generic AI helpers use the same block DTOs as commands and the editor. Hosts provide a small BlockDocumentAiAdapter that ensures a document, lists its blocks, and appends new blocks; feature packages or apps can then compose these tools with their own stateful-block tools. Reorder tools require the narrower BlockDocumentMoveBlockAiAdapter capability:

const tools = {
  add_block_document_text_block: createAddBlockDocumentTextBlockTool({
    blockDocumentAdapter,
    blockDocumentId,
  }),
  list_block_document_blocks: createListBlockDocumentBlocksTool({
    blockDocumentAdapter,
    blockDocumentId,
  }),
  move_block_document_block: createMoveBlockDocumentBlockTool({
    blockDocumentAdapter,
    blockDocumentId,
  }),
};

The block-listing tool returns the target blockDocumentId and a documentExists flag alongside blocks, so agents can distinguish an empty document from a missing or incompatible artifact.

BlockDocumentAiAdapter.addBlock may return a block ID synchronously or from a promise. Hosts that already expose block-document mutations as room commands can therefore use createBlockDocumentCommandAiAdapter to invoke the canonical block-document.append-blocks and block-document.move-block commands while keeping generic AI tools package-neutral:

const blockDocumentAdapter = createBlockDocumentCommandAiAdapter({
  store,
});

The adapter accepts only block-document artifacts.

Block-Scoped Ask AI

startBlockScopedChat(...) opens or reuses an artifact-scoped AI session for a specific block in a block document. It is exported from @sqlrooms/documents so hosts can wire Ask AI buttons, block header actions, or context menus without duplicating session-selection rules.

The helper intentionally depends on a small action adapter instead of importing a room store. Hosts provide artifact validation, session ownership, prompt updates, and assistant visibility through StartBlockScopedChatActions:

import {
  startBlockScopedChat,
  type StartBlockScopedChatActions,
} from '@sqlrooms/documents';

const actions: StartBlockScopedChatActions = {
  getArtifact: (artifactId) =>
    store.getState().artifacts.getArtifact(artifactId),
  getCurrentArtifactId: () => store.getState().artifacts.currentArtifactId,
  setCurrentArtifact: (artifactId) =>
    store.getState().artifacts.setCurrentArtifact(artifactId),
  getAiSessions: () => store.getState().ai.config.sessions,
  getSessionArtifactLinks: () =>
    store.getState().artifactAi.config.sessionArtifactLinks,
  createArtifactScopedSession: () =>
    store.getState().artifactAi.createArtifactScopedSession(),
  switchSession: (sessionId) => store.getState().ai.switchSession(sessionId),
  getSessionDraftContextItemIds: (sessionId) =>
    store.getState().ai.getSessionDraftContextItemIds(sessionId),
  setSessionDraftContextItemIds: (sessionId, ids) =>
    store.getState().ai.setSessionDraftContextItemIds(sessionId, ids),
  setPrompt: (sessionId, prompt) =>
    store.getState().ai.setPrompt(sessionId, prompt),
  startAnalysisWhenReady: (sessionId) =>
    store.getState().ai.startAnalysisWhenReady(sessionId),
};

await startBlockScopedChat({
  target: {
    blockDocumentId,
    blockId,
    blockType: 'map',
    blockInstanceId: mapId,
  },
  prompt: 'Repair this map',
  revealAssistant: () => setAssistantOpen(true),
  actions,
  isValidBlockDocumentArtifact: (artifact) =>
    artifact.type === 'block-document',
});

isValidBlockDocumentArtifact is required because hosts may use product-specific artifact type names or compatibility aliases. The helper validates the target artifact before mutating UI state, switches to the target artifact when needed, and derives the block context item with blockContextItemId(...) unless contextItemId is provided.

Only running sessions for the same artifact and context item block a new Ask AI turn. Finished sessions that already contain the block context are reused, their prompt is replaced, and their draft context is left untouched if it already includes the block item. When a matching running session exists, the helper switches to it, shows a toast, and returns without revealing the assistant, changing the prompt, or starting another analysis.

The slice can create block documents, replace the Tiptap JSON body, and append/insert/update/remove/reorder top-level blocks. Supported block DTOs include headings, paragraphs, lists, todos, images, chart images, standalone chart blocks, and direct stateful blocks.

BlockDocumentArtifact and BlockDocumentEditor provide the first rich editor surface for this structured state. BlockDocumentArtifact injects an editable, non-movable title node into the Tiptap document and reports title changes through onTitleChange, so hosts can keep artifact metadata and tab labels in sync. The editor owns Tiptap nodes for SQLRooms custom blocks, but chart and stateful block rendering are host-provided so @sqlrooms/documents does not import Mosaic, pivot, or other feature packages:

<BlockDocumentChartRendererProvider renderer={MosaicBlockDocumentChartRenderer}>
  <BlockDocumentStatefulBlockRendererProvider
    renderers={{
      dashboard: DashboardBlockRenderer,
      pivot: PivotBlockRenderer,
    }}
    blockTypes={[
      {
        blockType: 'dashboard',
        label: 'Dashboard',
        description: 'Interactive dashboard',
        createNode: (blockId) => ({
          type: 'blockDocumentStatefulBlock',
          attrs: {
            id: blockId,
            blockType: 'dashboard',
            blockInstanceId: createDashboardBlockState(blockId),
            ownership: 'owned',
            caption: '',
          },
        }),
      },
    ]}
  >
    <BlockDocumentArtifact
      artifactId={blockDocumentArtifactId}
      title="Analysis"
      onTitleChange={(title) =>
        renameBlockDocument(blockDocumentArtifactId, title)
      }
    />
  </BlockDocumentStatefulBlockRendererProvider>
</BlockDocumentChartRendererProvider>

Hosts can customize a chart block's outer frame with getBlockFrameClassName. The callback receives the document ID, block ID, block type, and current selection state. Its classes are merged after the built-in frame classes, which is useful for host-owned states such as an in-progress AI edit:

<BlockDocumentChartRendererProvider
  renderer={MosaicBlockDocumentChartRenderer}
  getBlockFrameClassName={({blockId, selected}) =>
    editingBlockIds.has(blockId)
      ? 'border-amber-500 ring-1 ring-amber-500'
      : selected
        ? 'border-primary'
        : undefined
  }
>
  <BlockDocumentArtifact artifactId={blockDocumentArtifactId} />
</BlockDocumentChartRendererProvider>

Descendants implementing custom chart node views can read the same optional callback with useBlockDocumentChartGetBlockFrameClassName(). The getter is a plain function rather than a hook; hosts should subscribe to reactive state in their provider component and pass a fresh callback when frame styling changes.

Stateful blocks carry document-local label/binding attributes, surfaced to renderers via BlockDocumentStatefulBlockRendererProps:

  • caption — the block's user-facing label in the document flow (onCaptionChange).
  • tableName — the table a table-bound block reads from (e.g. data-table), resolved via db.findTable like the chart block's tableName (onTableNameChange). Block types that keep their data binding inside their own backing state leave this unset.

If no renderer is registered, chart and stateful blocks render a clear unsupported state while preserving their Tiptap JSON attributes. blockTypes controls both the host-specific entries shown in the plus menu and which stateful block types are editable. A persisted stateful block with a registered renderer but no matching blockTypes entry renders read-only and cannot be turned into, dragged, or deleted with the editor's shared block controls. This lets hosts preserve disabled blocks as placeholders without making the rest of the document read-only. Chart renderers also receive a selected flag so their controls can reflect whether the block is the active Tiptap node selection. When a block is converted through the handle menu, custom createNode callbacks receive an optional {initialText} value with the source block text; hosts can use it to seed stateful blocks such as embedded Markdown documents. Stateful block types can opt into persisted vertical resizing with resizableHeight, defaultHeight, minHeight, and maxHeight; the editor stores the resulting height on the block node and renders a bottom resize handle just below the block for writable documents. Interactive blocks can also opt into requireScrollModifier; ordinary wheel gestures then keep scrolling the document and show a short hint, while Cmd+scroll on macOS or Ctrl+scroll elsewhere scrolls nested overflow regions inside the block. Use scrollHintLabel to customize the hint target text.

The backing instance owns its own display name. Hosts should seed that name when creating the backing state (for example from the registered block type's label or command defaultTitle) and resolve it from the instance when a UI needs the current name. Shared block-document state does not persist a stateful-block title mirror.

Block and panel definitions can provide reusable settings components. The host settings shell is owned by @sqlrooms/documents, while feature packages own the actual settings UI. Settings components receive the selected blockId, optional parent dashboardId, optional blockInstanceId, and an optional onClose callback when the host shell can be collapsed. Custom controls that should reveal the settings shell can call blockSettings.requestOpenSettingsPanel(). Controls that represent the currently shown settings can read blockSettings.runtime.isSettingsPanelOpen and call blockSettings.requestCloseSettingsPanel() to toggle the shell closed. Hosts that want the standard resizable side panel shell can wrap their surface with BlockSettingsPanelLayout; pass editor and documentId when rendering outside a BlockDocumentEditor context, or omit them for dashboard panels that use SelectablePanelWrapper.

createBlockDocumentFeatureSlices() composes createBlockDocumentsSlice() with the shared createBlockSettingsSlice() for apps that want a block document surface with reusable settings. If an app also uses another feature helper that includes block settings, install the shared settings slice only once by using one feature helper plus the other feature's lower-level slice.

Stateful Blocks

Use a statefulBlock block when the document should host a stateful SQLRooms surface directly, without wrapping it in an artifact shell:

blockDocuments.appendBlocks(blockDocumentArtifactId, [
  {
    id: 'pivot-block',
    type: 'statefulBlock',
    blockType: 'pivot',
    blockInstanceId: 'pivot-instance-1',
    ownership: 'owned',
    caption: 'Pivot table',
  },
]);

Hosts provide renderers through BlockDocumentStatefulBlockRendererProvider:

<BlockDocumentStatefulBlockRendererProvider
  renderers={{
    pivot: PivotBlockRenderer,
    dashboard: DashboardBlockRenderer,
  }}
  blockTypes={[
    {
      blockType: 'pivot',
      label: 'Pivot Table',
      description: 'Embedded pivot table',
    },
  ]}
>
  <BlockDocumentArtifact
    artifactId={blockDocumentArtifactId}
    title="Embedded Report"
    onTitleChange={(title) =>
      renameBlockDocument(blockDocumentArtifactId, title)
    }
  />
</BlockDocumentStatefulBlockRendererProvider>

Top-level artifacts should wrap stateful blocks or block containers at the workspace/tab layer. Block documents host the stateful block directly instead of embedding an artifact shell.

Owned stateful blocks are lifecycle-managed by the host app. Pass onCreateOwnedStatefulBlock to initialize feature state when a new owned block reference appears, and onDeleteOwnedStatefulBlock to clean it up when an owned block is removed from a document or when its owning block document is deleted. Blocks with ownership: 'shared' or ownership: 'external' are not cleaned up by the documents slice. Captions stay local to the block document. Backing instance names are changed through the owning feature's UI or commands, not by editing a block attribute. Stateful block renderers receive onCaptionChange when a writable document lets the embedded surface edit the document-local caption.

The editor normalizes pasted or duplicated owned stateful blocks by assigning fresh top-level block IDs and fresh blockInstanceId values when a duplicate owned instance would otherwise point at the same backing state.

Standalone Chart Blocks

Standalone chart blocks are meant for focused, in-document charts. They store the target tableName, a Mosaic ChartConfig, an optional caption, and an optional selectionGroupId:

blockDocuments.appendBlocks(blockDocumentArtifactId, [
  {
    id: 'revenue-histogram',
    type: 'chart',
    tableName: 'sales',
    config: {
      chartType: 'histogram',
      settings: {field: 'revenue'},
    },
    selectionGroupId: 'overview',
    caption: 'Revenue distribution',
  },
]);

Hosts can render these blocks with the same Mosaic/vgplot chart implementation and settings UI used inside dashboard panels, without embedding a full dashboard. Charts with the same selectionGroupId in one block document share a crossfilter selection. Charts without a group get independent document/block-scoped selections.

Hosted Dashboards

Use a statefulBlock block when the document needs a multi-panel interactive dashboard. The block instance id should map to dashboard state in the host app's Mosaic slice, while the top-level artifact shell remains optional for workspace navigation.

Standalone chart blocks are best for one chart with local context. Dashboard stateful blocks are best for coordinated multi-panel views, richer dashboard layout, or when dashboard AI tools are the natural authoring path.

Commands

createMarkdownDocumentCommands() registers AI- and palette-friendly commands for Markdown artifacts:

  • markdown-document.list
  • markdown-document.get
  • markdown-document.create
  • markdown-document.set-markdown
  • markdown-document.append-markdown

createBlockDocumentCommands() registers commands for structured block document artifacts. By default the command IDs are:

  • block-document.list
  • block-document.get
  • block-document.create
  • block-document.append-blocks
  • block-document.insert-blocks
  • block-document.update-block
  • block-document.remove-block
  • block-document.move-block
  • block-document.create-chart-block
  • block-document.create-stateful-block

The artifact type is always block-document, command IDs always use block-document.*, and command descriptions use “block document”. Artifact type, label, and command namespace overrides are not supported. Hosts can customize UI labels in their artifact registry, and pass commandGroup and defaultTitle without changing the AI vocabulary.

Hosts can pass statefulBlockTypes to expose supported feature-backed block types to block-document.create-stateful-block.

Hosts with a narrower block surface can also pass allowedBlockTypes. The generic create, append, insert, and update commands then reject other block kinds; allowed statefulBlock payloads are additionally restricted to the configured statefulBlockTypes.

Block mutation command results include the full refreshed document data plus focused mutation payloads such as blockId, blockIds, blockType, blockTypes, and affectedBlocks. Chart and stateful block creation also return follow-up IDs such as tableName, blockInstanceId, statefulBlockType, the seed instanceTitle, and chosen caption values.

Structured block payloads may include an optional intent string. Use it for the durable natural-language purpose of an agent- or command-created block, such as the question a chart should answer or the job an embedded dashboard should serve. It is persisted with the block, unlike transient mutation metadata.

CRDT

@sqlrooms/documents/crdt exposes Loro Mirror bindings for document state:

createCrdtSlice({
  mirrors: {
    documentState: createDocumentsCrdtMirror(),
  },
});

createDocumentsCrdtMirror() syncs Markdown bodies, block document Tiptap JSON content, document-owned assets, standalone chart block configs, block document and Markdown artifact metadata, and their artifact tab order. The current artifact selection is kept local.

The mirror syncs block-document and markdown-document artifact metadata without legacy aliases. Pre-release sync snapshots and saved AI context are not migrated; reset incompatible development state when upgrading.

Hosted dashboard state should continue to use the host app's Mosaic persistence, or a future Mosaic-specific CRDT mirror.

Knowledge Index

buildKnowledgeIndex is a pure derived index. It does not persist data.

const index = buildKnowledgeIndex({
  markdownDocuments: roomStore.getState().markdownDocuments.config,
  artifacts: roomStore.getState().artifacts.config,
});

It extracts [[Document Title]] wikilinks, body hashtags such as #metrics, and optional frontmatter tags. Links are resolved against Markdown artifact titles. Missing or ambiguous titles are reported as unresolved links.

Markdown document naming and persistence

Markdown artifacts and embeddable blocks use markdown-document; their commands use markdown-document.*. Use createMarkdownDocumentsSlice, MarkdownDocumentsSliceConfig, MarkdownDocumentsSliceState, and useStoreWithMarkdownDocuments with the markdownDocuments store key. createMarkdownDocumentCommands provides the command family. These replace the former generic DocumentsSlice* APIs and markdown.* commands.

The room state and CRDT field both use markdownDocuments. The CLI migrates local workspace snapshots from the persisted documents slice and markdown artifact/block types, preserving document content and assets. If both slice keys exist, canonical records take precedence while disjoint legacy records are retained. Experimental CRDT snapshots and saved AI context are not migrated; reset incompatible development sync and saved-session state when upgrading. DocumentAsset stays shared by both document families.