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

@instructure/platform-block-content-editor

v3.4.0

Published

Host-agnostic block content editor for Canvas activities — the blocks, the read-path viewer, and the write-path editor, all on native InstUI.

Readme

@instructure/platform-block-content-editor

Host-agnostic block content editor for the Canvas activity builder: the blocks, their schemas, the read path that renders a saved document, and the write path that authors one — all on native InstUI, with every Canvas coupling behind an injectable seam. Owned by foundation.

This package ships building blocks for rebuilding the editor, not a finished app. There is one convenient composition (BlockContentEditor), but everything it does is reproducible from the exposed primitives, so a host can arrange the pieces however it needs (a tray or the preview in a modal, a custom toolbar, its own layout).

Deep design notes and invariants live in CLAUDE.md; the block-authoring conventions in the bce-block-authoring skill; architecture decisions in the repo's docs/adr/. See Documentation below.

Install

pnpm add @instructure/platform-block-content-editor

InstUI, React, and zod are peer dependencies — the host provides its single shared copy of each (see the peerDependencies in package.json).

The package is ESM-only ("type": "module", one . export plus ./locales/en.json). Styles are emotion runtime-injected — there is no ./styles import. There is also no PlatformUiProvider: BCE is host-agnostic through its own set of seam contexts (see Seams), not a single umbrella provider. A minimal host can render Viewer / BlockContentEditor with just React, InstUI theming, and a composed registry.

The registry (both paths need it)

There is deliberately no default registry: which blocks a document may contain is the host's decision. Compose one from the shipped block factories — the keys are the wire format stored in documents (Header → Banner, QuoteBlock → Quote, …):

import { defineRegistry, createBannerBlock, createTextBlock } from '@instructure/platform-block-content-editor'

const registry = defineRegistry({
  Header: createBannerBlock({ label: t('Banner'), translations: {/* … */} }),
  Text: createTextBlock({ label: t('Rich content editor'), translations: {/* … */} }),
  // …the blocks this host offers, plus any host-owned blocks
})

Compose with defineRegistry(…); never annotate : BlockRegistry (it widens the type and silently collapses every derived type — see CLAUDE.md).

Read path — render a saved document

import { Viewer } from '@instructure/platform-block-content-editor'

<Viewer templateLayout={layout} templateData={data} componentRegistry={registry} />

Viewer emits no wrapper and takes no theme; host-specific typography belongs on the host's container. Unresolved blocks are reported via onUnresolvedBlock, never dropped or mutated.

Write path — author a document

The host owns the state engine (so it owns save, undo/redo, dirty-tracking, and DB↔package conversion). Call useBlockEditorState and hand the result to the editor:

import { useBlockEditorState, BlockContentEditor } from '@instructure/platform-block-content-editor'

function Builder() {
  const engine = useBlockEditorState({ initialData, initialLayout, registry, onDirty })

  return (
    <BlockContentEditor
      engine={engine}
      registry={registry}
      tools={[widgetTray, aiAssistantTool]} // host-tool slot (optional)
    />
  )
}

BlockContentEditor composes the tool-panel sidebar over the drag-and-drop canvas: it builds the registry-driven insertion trays, hoists a single DnD context so blocks can be dragged from a tray onto the canvas, coordinates canvas↔panel selection focus, and routes a selected block to its settings form. It is authoring-only — see Preview below.

Or compose the primitives yourself

Every arrangement above is reproducible. Wrap your own layout in one GridDndProvider and drop a bare DesignerCanvas and the trays inside it (so tray drag reaches the canvas), or use the standalone DesignerArea (which self-wraps its own provider) when you don't need a shared context:

import { GridDndProvider, DesignerCanvas, ComponentsTray, DesignerNavProvider } from '@instructure/platform-block-content-editor'

<GridDndProvider {...dndProps}>
  <DesignerNavProvider><ComponentsTray registry={registry} onAddComponent={engine.addComponent} /></DesignerNavProvider>
  <DesignerCanvas canvasRef={canvasRef} {...canvasProps} />
</GridDndProvider>

Also public: DesignerSideBar (the tool-panel chrome), ComponentEditorForm (the settings form), useDesignerNav (the sidebar's view-navigation hook), useDesignerSelectionFocus, and the block-level AI (BlockAIMenu, useGenerateAltText).

Preview — the host presents it

PreviewArea is a framed student preview over the read-path Viewer, sized from a required, controlled viewport prop ('desktop' | 'mobile'). It renders no switch of its own — the host builds the Desktop/Mobile toggle and drives viewport from its own state. The package ships no modal: how the preview is presented (modal, drawer, fullscreen, inline) is the host's choice.

import { PreviewArea, PREVIEW_VIEWPORTS, type PreviewViewport } from '@instructure/platform-block-content-editor'

const [viewport, setViewport] = useState<PreviewViewport>(PREVIEW_VIEWPORTS.DESKTOP)

<Modal open={previewing} onDismiss={…}>
  <div role="group" aria-label="Preview viewport">
    <Button onClick={() => setViewport(PREVIEW_VIEWPORTS.DESKTOP)}>Desktop</Button>
    <Button onClick={() => setViewport(PREVIEW_VIEWPORTS.MOBILE)}>Mobile</Button>
  </div>
  <PreviewArea
    templateLayout={engine.templateLayout}
    templateData={engine.templateData}
    componentRegistry={registry}
    viewport={viewport}
  />
</Modal>

Seams — everything Canvas-specific is injected

Blocks and the editor read only from context; the host provides the implementations:

| Context | What the host injects | | --- | --- | | BrandColorContext | the brand color (hex/rgb()), or undefined | | BlockTranslationsProvider | resolved strings per block + designer chrome (bundled English is the fallback) | | RichTextRendererContext | a renderer for RCE-authored HTML (read path); unset → sanitized fallback | | RichTextEditorContext | the RCE editor component (write path); unset → plain TextArea | | AIActionContext | the AI executor; presence enables the block-level AI, unset → hidden | | BlockEditorApiProvider | host data hooks — image/video/widget URLs and the settings-form file upload/delete |

Each is host-agnostic: the host closes over Canvas course/plugin/file config inside its own hook/component; the package only ever passes opaque ids and reads back resolved values.

Wiring it up

The canonical nesting a host provides (the order proven by the consumer/index.tsx reintegration fixture) — supply only the seams you use; each has a safe default:

<BrandColorContext.Provider value={brandColor}>
  <DesignerModeContext.Provider value={false}>
    <BlockEditorApiProvider value={hostDataHooks}>        {/* image/video/widget + upload hooks */}
      <RichTextRendererContext.Provider value={renderHtml}>
        <RichTextEditorContext.Provider value={HostRceEditor}>
          <AIActionContext.Provider value={executeAiToolUse}> {/* presence enables block-level AI */}
            <BlockTranslationsProvider value={resolvedStrings}>
              {/* <Viewer />, <BlockContentEditor />, <PreviewArea /> … */}
            </BlockTranslationsProvider>
          </AIActionContext.Provider>
        </RichTextEditorContext.Provider>
      </RichTextRendererContext.Provider>
    </BlockEditorApiProvider>
  </DesignerModeContext.Provider>
</BrandColorContext.Provider>

Reintegration gate

The public surface is the contract a host compiles against. pnpm check:consumer builds dist/ and type-checks the consumer/ fixture against it, catching any exported inferred type that can't be named across the exports map (TS2742) — which the package's own type-check never sees. Add newly-exported symbols to consumer/index.tsx.

Development

pnpm --filter @instructure/platform-block-content-editor test        # vitest run
pnpm --filter @instructure/platform-block-content-editor type-check  # tsc -p tsconfig.check.json
pnpm --filter @instructure/platform-block-content-editor build       # vite build (+ .d.ts emit)
pnpm --filter @instructure/platform-block-content-editor check:consumer  # the reintegration gate
pnpm storybook                                                       # 21 stories: every block + the editor

Cross-package types resolve through built dist/, so run pnpm build once after a clone/pull before type-check/test (see the gotchas in CLAUDE.md). Storybook needs Node ≥ 22.12.

Documentation

  • This README — how to consume the package.
  • CLAUDE.md — what is true in src/ right now: the read/write paths, the seams, the non-negotiable invariants, and the build gotchas.
  • bce-block-authoring skill (.claude/skills/) — the conventions for creating or modifying a block (styling, accessibility, the schema toolkit, per-block translations), loaded on demand.
  • docs/adr/ (repo root) — architecture decision records; append-only.
  • In-file comments — reasoning local to one file, especially the block barrels and src/schema/.