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

@josiesam/lms-cms

v0.1.0

Published

A headless, multi-tenant content block system for building Learning Management Systems (LMS) in React. Provides a schema-driven block editor, renderer, and adapter layer that works with any backend.

Downloads

82

Readme

@lms-cms

A headless, multi-tenant content block system for building Learning Management Systems (LMS) in React. Provides a schema-driven block editor, renderer, and adapter layer that works with any backend.


Packages

| Package | Description | |---------|-------------| | @lms-cms/core | Shared types, plugin registry, migrations, utilities | | @lms-cms/adapters | REST, GraphQL, and mock data adapters | | @lms-cms/renderer | React renderer, theme provider, data hooks | | @lms-cms/editor | Visual block editor UI | | @lms-cms/blocks | All built-in content blocks |


Quick Start

npm install @lms-cms/renderer @lms-cms/adapters @lms-cms/blocks
import { LMSProvider, ContentRenderer, useContent } from '@lms-cms/renderer'
import { createRestAdapter } from '@lms-cms/adapters'
import { registerAllBlocks } from '@lms-cms/blocks'

registerAllBlocks()

const adapter = createRestAdapter({ baseUrl: 'https://api.yourapp.com' })

function App() {
  return (
    <LMSProvider adapter={adapter} tenantId="acme-corp">
      <CourseViewer contentId="lesson-123" />
    </LMSProvider>
  )
}

function CourseViewer({ contentId }: { contentId: string }) {
  const { data: doc, isLoading } = useContent(contentId)
  if (isLoading || !doc) return <div>Loading...</div>
  return <ContentRenderer doc={doc} />
}

Architecture

The system is organized into three independent planes:

┌─────────────────────────────────────────────────┐
│               EDITING PLANE                     │
│  LMSEditor · BlockToolbar · BlockCanvas         │
└─────────────────┬───────────────────────────────┘
                  │ ContentDoc JSON
         ┌────────▼────────┐
         │ PLUGIN REGISTRY │  ← Zustand store (shared singleton)
         │  Map<type, Def> │
         └────────┬────────┘
                  │
┌─────────────────▼───────────────────────────────┐
│               RENDERING PLANE                   │
│  ContentRenderer · ThemeProvider · LMSProvider  │
└─────────────────┬───────────────────────────────┘
                  │ useContent() hook
┌─────────────────▼───────────────────────────────┐
│               DATA PLANE                        │
│  IContentAdapter · REST · GraphQL · Mock        │
└─────────────────────────────────────────────────┘

No plane imports directly from another — all communication is through the plugin registry and typed interfaces.


@lms-cms/core

Foundation package. Zero React dependencies. Every other package depends on it.

Installation

npm install @lms-cms/core

ContentDoc — Universal Format

Every piece of content is a ContentDoc:

interface ContentDoc {
  id: string               // UUID
  version: number          // schema version for migrations
  meta: Record<string, unknown>
  blocks: ContentBlock[]
  createdAt?: string
  updatedAt?: string
}

interface ContentBlock {
  id: string               // UUID, stable across edits
  type: string             // e.g. "core/text", "lms/quiz"
  version: number          // block schema version
  data: Record<string, unknown>
  children?: ContentBlock[] // nested blocks (columns, groups)
}

Plugin Registry

import { registerBlock, getBlock, listBlocks } from '@lms-cms/core'

// Register a block definition (called once at app init)
registerBlock(myBlockDef)

// Retrieve a definition by type
const def = getBlock('core/text')

// List all blocks, optionally by category
const textBlocks = listBlocks('text')
const allBlocks = listBlocks()

IContentAdapter Interface

All adapters implement this contract:

interface IContentAdapter {
  fetchContent(id: string, options?: FetchOptions): Promise<ContentDoc>
  saveContent(doc: ContentDoc, options?: SaveOptions): Promise<ContentDoc>
  listContents(query: ContentQuery): Promise<ContentList>
  deleteContent(id: string): Promise<void>
  uploadAsset(file: File, options?: AssetOptions): Promise<AssetResult>
}

Utilities

import { createBlock, createDoc, mergeTheme, themeToCSS } from '@lms-cms/core'

const block = createBlock('core/text', { text: 'Hello', align: 'left' })
const doc   = createDoc([block], { courseId: 'abc' })
const theme = mergeTheme({ '--color-primary': '#0EA5E9' })

Schema Validation

import { ContentDocSchema, ContentBlockSchema } from '@lms-cms/core'

const result = ContentDocSchema.safeParse(rawJson)
if (!result.success) console.error(result.error)

@lms-cms/adapters

Installation

npm install @lms-cms/adapters

REST Adapter

import { createRestAdapter } from '@lms-cms/adapters'

const adapter = createRestAdapter({
  baseUrl: 'https://api.yourapp.com',
  auth: { type: 'bearer', token: 'your-token' },
  timeout: 10000,
})

Auth options: bearer, apikey, basic.

GraphQL Adapter

import { createGraphQLAdapter } from '@lms-cms/adapters'

const adapter = createGraphQLAdapter({
  endpoint: 'https://api.yourapp.com/graphql',
  auth: { type: 'bearer', token: 'your-token' },
})

Mock Adapter (development & testing)

import { createMockAdapter, createSeedMockAdapter } from '@lms-cms/adapters'

// Empty in-memory store
const adapter = createMockAdapter({ delay: 300 })

// Pre-seeded with sample LMS content (heading, text, image, quiz)
const adapter = createSeedMockAdapter()

Custom Adapter

Implement IContentAdapter from @lms-cms/core:

import type { IContentAdapter, ContentDoc } from '@lms-cms/core'

export function createSupabaseAdapter(client: SupabaseClient): IContentAdapter {
  return {
    async fetchContent(id) {
      const { data } = await client.from('content').select('*').eq('id', id).single()
      return data as ContentDoc
    },
    async saveContent(doc) {
      const { data } = await client.from('content').upsert(doc).select().single()
      return data as ContentDoc
    },
    async listContents(query) { /* ... */ },
    async deleteContent(id) { /* ... */ },
    async uploadAsset(file) { /* ... */ },
  }
}

@lms-cms/renderer

Installation

npm install @lms-cms/renderer @lms-cms/core

LMSProvider

Root context — wrap your entire app (or a section of it):

import { LMSProvider } from '@lms-cms/renderer'

<LMSProvider
  adapter={adapter}          // required: IContentAdapter
  tenantId="acme-corp"       // required: used for theme scoping and cache isolation
  theme={acmeTheme}          // optional: Partial<LMSTheme> brand overrides
  plugins={[customBlock]}    // optional: additional block definitions
>
  {children}
</LMSProvider>

Multiple LMSProvider instances can coexist on the same page with different themes — useful for multi-tenant admin UIs.

ContentRenderer

Renders a ContentDoc to React components:

import { ContentRenderer } from '@lms-cms/renderer'

<ContentRenderer
  doc={doc}
  className="my-content"
  onBlockClick={(block) => console.log(block.type)}
/>

Unknown block types render a visible error in development and nothing in production.

Data Hooks

All hooks must be used inside <LMSProvider>.

import { useContent, useContentList, useSaveContent, useDeleteContent } from '@lms-cms/renderer'

// Fetch a single document (runs block migrations automatically)
const { data: doc, isLoading, error } = useContent('lesson-123')

// Fetch a list with optional filters
const { data: list } = useContentList({ limit: 20, search: 'react' })

// Save (create or update)
const { mutate: save } = useSaveContent()
save(doc)

// Delete
const { mutate: remove } = useDeleteContent()
remove('lesson-123')

Cache is scoped by tenantId — switching tenants never cross-contaminates data.

Theming

import { useTheme } from '@lms-cms/renderer'

function MyComponent() {
  const theme = useTheme()
  return <div style={{ color: theme['--color-primary'] }}>Hello</div>
}

All CSS custom properties are injected on a scoped container element — not on :root. This means tenant themes are isolated even when multiple providers coexist.

Available tokens

| Token | Default | Usage | |-------|---------|-------| | --color-primary | #6C2BD9 | Buttons, active states | | --color-primary-hover | #5B22B8 | Hover states | | --color-secondary | #E9D5FF | Selected states, light backgrounds | | --color-surface | #FFFFFF | Card backgrounds | | --color-surface-raised | #F9FAFB | Nested card backgrounds | | --color-background | #F3F4F6 | Page background | | --color-text-primary | #111827 | Body text | | --color-text-secondary | #6B7280 | Labels, captions | | --color-text-muted | #9CA3AF | Placeholders | | --color-border | #E5E7EB | All borders | | --color-danger | #EF4444 | Error states | | --color-success | #22C55E | Success states | | --color-warning | #F59E0B | Warning states | | --font-body | "Inter", system-ui, sans-serif | Body text | | --font-heading | "Inter", system-ui, sans-serif | Headings | | --font-mono | "JetBrains Mono", monospace | Code | | --font-size-base | 16px | Base font size | | --border-radius-sm | 4px | Inputs, small elements | | --border-radius-md | 8px | Cards, buttons | | --border-radius-lg | 12px | Modals, large cards | | --shadow-sm | 0 1px 2px rgba(0,0,0,0.05) | Subtle elevation | | --shadow-md | 0 4px 6px -1px rgba(0,0,0,0.1) | Cards, dropdowns |


@lms-cms/editor

Installation

npm install @lms-cms/editor @lms-cms/core

LMSEditor

Visual block editor. Renders a two-panel UI: block picker on the left, editable canvas on the right.

import { LMSEditor } from '@lms-cms/editor'
import { createDoc } from '@lms-cms/core'
import { useState } from 'react'

function CourseEditor() {
  const [doc, setDoc] = useState(() => createDoc())

  return (
    <LMSEditor
      doc={doc}
      onChange={setDoc}
      readOnly={false}
    />
  )
}

| Prop | Type | Default | Description | |------|------|---------|-------------| | doc | ContentDoc | required | The document to edit | | onChange | (doc: ContentDoc) => void | required | Called on every change | | readOnly | boolean | false | Hides the block picker, disables controls |

The editor reads all registered blocks from the plugin registry. Register blocks before mounting the editor.


@lms-cms/blocks

Installation

npm install @lms-cms/blocks @lms-cms/core

Block Catalogue

Text blocks (category: text)

| Block type | Label | Description | |-----------|-------|-------------| | core/text | Paragraph | Rich paragraph with text alignment | | core/heading | Heading | H1–H6 with level picker |

Media blocks (category: media)

| Block type | Label | Formats supported | |-----------|-------|-------------------| | core/image | Image | Any image URL (JPEG, PNG, GIF, WebP, SVG) | | core/video | Video | YouTube, Vimeo (iframe), direct .mp4 / .webm | | core/audio | Audio | .mp3, .ogg, .wav, .m4a and any audio URL | | core/pdf | PDF | Any PDF URL — rendered via browser iframe viewer | | core/file | File Download | PDF, DOCX, XLSX, PPTX, ZIP, CSV, MP3, MP4, and any other file type | | core/embed | Embed | iFrame embed — Loom, Notion, Figma, Google Slides, etc. |

LMS blocks (category: lms)

| Block type | Label | Description | |-----------|-------|-------------| | lms/quiz | Quiz | Multiple-choice questions, scoring, pass/fail, retry | | lms/progress | Progress Bar | Visual progress indicator with configurable color | | lms/checklist | Checklist | Interactive completion checklist with required items |

Registering blocks

import { registerAllBlocks } from '@lms-cms/blocks'

// Call once before rendering — registers all 11 built-in blocks
registerAllBlocks()

Or cherry-pick individual blocks:

import { registerBlock } from '@lms-cms/core'
import { quizBlockDef, videoBlockDef, pdfBlockDef } from '@lms-cms/blocks'

registerBlock(quizBlockDef)
registerBlock(videoBlockDef)
registerBlock(pdfBlockDef)

Or pass them through LMSProvider:

import { videoBlockDef, pdfBlockDef } from '@lms-cms/blocks'

<LMSProvider plugins={[videoBlockDef, pdfBlockDef]} ...>

Block schemas (data shapes)

core/text

{ text: string; align: 'left' | 'center' | 'right' }

core/heading

{ text: string; level: 1 | 2 | 3 | 4 | 5 | 6 }

core/image

{ url: string; alt: string; caption?: string; width: 'full' | 'wide' | 'medium' | 'small' }

core/video

{
  url: string          // YouTube, Vimeo, or direct video URL
  title?: string
  caption?: string
  autoplay: boolean
  loop: boolean
  controls: boolean
}

core/audio

{
  url: string          // any audio URL
  title?: string
  caption?: string
  controls: boolean
  loop: boolean
}

core/pdf

{
  url: string
  title?: string
  height: 'small' | 'medium' | 'large' | 'full'
  showDownload: boolean
}

core/file

{
  url: string
  filename: string
  fileType: 'pdf' | 'docx' | 'xlsx' | 'pptx' | 'zip' | 'csv' | 'mp3' | 'mp4' | 'other'
  size?: string        // e.g. "2.4 MB"
  description?: string
}

core/embed

{
  url: string          // any embeddable URL
  title?: string
  height: number       // pixels, 100–2000
  caption?: string
}

lms/quiz

{
  questions: Array<{
    id: string
    text: string
    options: string[]   // 2–6 options
    correctIndex: number
    explanation?: string
  }>
  passingScore: number  // 0–100
  showExplanations: boolean
}

lms/progress

{
  label: string
  value: number         // 0–100
  showPercentage: boolean
  color: 'primary' | 'success' | 'warning' | 'danger'
}

lms/checklist

{
  title?: string
  items: Array<{ id: string; text: string; required: boolean }>
  showCompletion: boolean
}

Building Custom Blocks

Any block type can be added without modifying the library. Custom blocks are namespaced to avoid collisions.

import { z } from 'zod'
import type { BlockDefinition } from '@lms-cms/core'
import { registerBlock } from '@lms-cms/core'

const calloutSchema = z.object({
  text: z.string().min(1),
  type: z.enum(['info', 'warning', 'success', 'danger']),
  title: z.string().optional(),
})

type CalloutData = z.infer<typeof calloutSchema>

const calloutBlockDef: BlockDefinition<CalloutData> = {
  type: 'acme/callout',     // namespace with your org prefix
  version: 1,
  label: 'Callout',
  icon: '💬',
  category: 'layout',
  schema: calloutSchema,
  defaultData: { text: '', type: 'info' },
  EditorComponent: CalloutEditor,
  RenderComponent: CalloutRenderer,
}

registerBlock(calloutBlockDef)

Block Naming Convention

core/text          ← built-in library blocks
lms/quiz           ← built-in LMS blocks
acme/callout       ← your org's custom blocks
eduflow/poll       ← another tenant's blocks

Block Versioning and Migrations

When a block's data shape changes, bump version and add a migration:

const richTextDef: BlockDefinition = {
  type: 'acme/richtext',
  version: 2,
  schema: z.object({ text: z.object({ value: z.string(), markdown: z.boolean() }) }),
  defaultData: { text: { value: '', markdown: false } },
  migrations: [
    {
      fromVersion: 1,
      toVersion: 2,
      migrate: (data) => ({
        text: { value: String(data['text'] ?? ''), markdown: false },
      }),
    },
  ],
  // ...
}

Migrations run automatically via migrateDoc() when content is fetched — no database changes needed.


Multi-Tenant Usage

// Each tenant gets its own LMSProvider with isolated theme + cache
function App({ tenantId, theme, adapter }: TenantConfig) {
  return (
    <LMSProvider adapter={adapter} theme={theme} tenantId={tenantId}>
      <CourseViewer />
    </LMSProvider>
  )
}

Theme switching is instant — no page reload, no flash:

const [tenant, setTenant] = useState(TENANTS[0])

// Re-renders with new CSS custom properties applied to the scoped container
<LMSProvider theme={tenant.theme} tenantId={tenant.id} ...>

Design Principles

  1. Dependency Inversion — components depend on interfaces, not implementations
  2. Progressive Adoption — install just @lms-cms/renderer to start; add editor and blocks incrementally
  3. Schema-as-Code — Zod validates all block data; no database migrations for content schema changes
  4. Zero Hardcoded Brand Values — every visual value references a CSS custom property
  5. Block Isolation — each block is a self-contained unit; blocks cannot read each other's data

Development

# Install dependencies
pnpm install

# Build all packages
pnpm build

# Run tests
pnpm test

# Start demo app
pnpm --filter demo dev

# Watch mode for a specific package
pnpm --filter @lms-cms/renderer dev

Local testing with yalc

# Publish to local yalc store
pnpm build
yalc publish packages/core
yalc publish packages/adapters
yalc publish packages/renderer
yalc publish packages/editor
yalc publish packages/blocks

# In your consuming project
yalc add @lms-cms/renderer
yalc add @lms-cms/blocks

# Push updates after changes
pnpm build && yalc push packages/renderer

License

MIT