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

@classytic/cms-ui

v0.5.0

Published

Composition layer for @classytic/cms: Plate-based block editor, form editor, MDX editor. Layers over the host's shadcn (Base UI) primitives via the @/components/ui/* alias (Fluid pattern). Shared BlockSpec contract (defineBlock), domain blocks, autosave,

Readme

@classytic/cms-ui

Composition layer for @classytic/cms. Plate-based block editor + formkit form editor + MDX editor, dispatched by ContentType.displayMode. Layers over the host's shadcn primitives via the @/components/ui/* alias.

Composable block packs

Use defineBlock with separate load and loadEditor modules, group manifests with defineBlockPack, and create one createBlockRegistry. The registry supplies the editor blocks, reader tag map, serializer specs, catalog and validation. Unselected lazy atomic blocks stay as cards; selecting them loads authoring UI.

See the registry guide and the third-party slide-deck example. checkBlock is exported from /testing; compileContent from /server pairs with ContentView from /viewer for precompiled content.

The big idea — three layers, single brand source

Host's components/ui/*       ← shadcn primitives (Button, Popover, ...)
       ↑ host owns, host modifies, host brands
─────────────────────────────────────────────────
@classytic/cms-ui            ← composes Plate plugins into <CmsEditor>
                               + vendored Plate UI reskinned to use the host alias
                               + our custom blocks (AssetImage, ContentLink)
─────────────────────────────────────────────────
Host app                     ← mounts <CmsEditor> with host's blocks/AI/asset picker

When the host modifies a <Button> in components/ui/, every Plate toolbar button, every Fluid card action, every custom block control re-adopts the change instantly. One source of brand truth. No theme drift.

Install

npm install @classytic/cms @classytic/cms-ui platejs
# Plate plugin packages (the full peer set this package composes):
npm install @platejs/basic-nodes @platejs/code-block @platejs/combobox \
            @platejs/dnd \
            @platejs/footnote @platejs/indent @platejs/link @platejs/list \
            @platejs/markdown @platejs/selection \
            @platejs/slash-command
# drag-to-reorder, slash-menu combobox, code-block highlighting:
npm install @ariakit/react react-dnd react-dnd-html5-backend lowlight
# optional: MDX rendering in the viewer
npm install next-mdx-remote-client

Form-mode content (displayMode: 'form') is rendered by the host's own form system through <CmsFormEditor render={…}> (e.g. @classytic/formkit); this package does not depend on one.

The exact peer set is the source of truth in this package's peerDependencies. If a bundler reports Can't resolve '@platejs/…', install that package — it's a declared peer.

Host requirements — shadcn primitives (Base UI variant)

This package targets the Base UI variant of shadcn/ui — the same one @classytic/fluid expects (see Fluid's src/types/shadcn.d.ts, which we mirror verbatim). The host must have a @/components/ui/* path alias and these primitives installed:

| Primitive | Used by | |---|---| | @/components/ui/button | toolbar, code-block language picker | | @/components/ui/checkbox | to-do list items | | @/components/ui/command | code-block language picker (cmdk-based in both shadcn variants) | | @/components/ui/popover | code-block language picker | | @/components/ui/textarea | MDX-mode editor | | @/lib/utils (cn) | every vendored component |

Install them with the shadcn CLI you already use for Fluid. If you don't have the Base UI shadcn set up, run the shadcn-baseui skill (see below) — it scaffolds the alias + primitives.

Radix shadcn vs Base UI shadcn. Most primitives have an identical import surface across both variants, and the slash menu is variant-agnostic (it's built on Plate's own Ariakit InlineCombobox, not on your command). The few places we touch a host primitive (button/checkbox/command/popover/textarea) are typed against the Base UI API. If you're on classic Radix shadcn and hit a prop mismatch, the fix is local to your components/ui/* — the package imports through your alias, so your copy wins.

Missing a primitive? Imports resolve at your build time, so a missing @/components/ui/command surfaces as a clear bundler error (Cannot resolve '@/components/ui/command') pointing at the exact file — not a silent runtime failure. The table above is the full list to pre-install.

Pre-req recap: host has the @/components/ui/* alias resolving to their shadcn primitives directory (standard Next.js shadcn setup; also required by @classytic/fluid).

Next.js / Turbopack integration notes

Two host-side settings are required for this package to bundle correctly under Next.js (App Router / Turbopack):

1. Add @classytic/cms-ui to transpilePackages

This package ships ESM that imports the host's @/components/ui/* alias (the Fluid pattern). Turbopack does not apply the host's tsconfig path aliases inside node_modules by default, so the alias won't resolve and the build fails. Transpile the package through the host's resolution:

// next.config.ts
const nextConfig = {
  // Both fluid and cms-ui import `@/components/ui/*`.
  transpilePackages: ["@classytic/fluid", "@classytic/cms-ui"],
};

2. Consuming a local / unpublished build — use a packed tarball, not file:

A file: dependency that symlinks to a checkout outside the host's project root (e.g. file:../../cms-ui) breaks Turbopack's subpath-export resolution — root-level imports work but @classytic/cms-ui/contract, /mdx-io, etc. fail with Module not found, because Turbopack resolves the symlink's realpath (outside the workspace) and doesn't apply the exports map there.

Consume a real node_modules copy instead — pack a tarball and install it (this also mirrors how the published install behaves):

# in the cms-ui checkout
npm pack                       # -> classytic-cms-ui-<version>.tgz
# in the host app
npm install ../path/to/classytic-cms-ui-<version>.tgz

Once @classytic/cms-ui is published to npm, install it by version range as normal — these notes only apply to local/unpublished builds.

3. Tailwind v4 — import the styles partial

This package ships no precompiled CSS — its vendored Plate UI (toolbar, editor container, placeholders, block spacing, focus rings) is styled with Tailwind utility classes that your build must generate. Import the bundled styles.css partial in your CSS entry (after @import "tailwindcss"), exactly like @classytic/fluid:

@import "tailwindcss";
@import "@classytic/fluid/styles.css";
@import "@classytic/cms-ui/styles.css";   /* generates cms-ui's classes */

styles.css self-declares @source "./dist/**/*.mjs" relative to the package, so you never hand-write a fragile ../node_modules/... path. Without it the editor mounts but renders unstyled (raw, edge-to-edge content). The classes reference your theme tokens (border-input, muted-foreground, ring, brand colors), so the editor adopts your theme automatically once they're generated.

Use

import { CmsEditor } from '@classytic/cms-ui';
import { defineBlock } from '@classytic/cms-ui/contract';
import { useAutosave } from '@classytic/cms-ui/hooks';

// Host's domain block — one file, ~30 lines.
const QuizBlock = defineBlock({
  key: 'quiz',
  tag: 'Quiz',
  void: true,
  label: 'Quiz',
  icon: QuizIcon,
  category: 'interactive',
  schema: z.object({ questionId: z.string(), options: z.array(z.string()) }),
  Component: QuizComponent,
  toAttrs: (node) => ({ questionId: node.questionId }),
  fromAttrs: (attrs) => ({ questionId: String(attrs.questionId), options: [] }),
});

export function LessonEditor({ lessonId }) {
  const { item, contentType, save } = useCmsItem(lessonId);   // host's API hook
  const autosave = useAutosave({ onSave: save, debounceMs: 1500 });

  return (
    <CmsEditor
      item={item}
      contentType={contentType}
      blocks={[QuizBlock]}                 // host owns the enabled-blocks list
      onChange={autosave.onChange}
      onPickAsset={openAssetLibrary}       // optional — host's media picker
    />
  );
}

What lives in this package

| Module | What | |---|---| | <CmsEditor> (root) | Dispatcher — picks block/form/mdx editor by ContentType.displayMode | | @classytic/cms-ui/block | <CmsBlockEditor> — Plate-based block editor + kit-builder + custom blocks | | @classytic/cms-ui/form | <CmsFormEditor> — thin @classytic/formkit wrapper | | @classytic/cms-ui/mdx | <CmsMdxEditor> — Plate in markdown-only mode | | @classytic/cms-ui/contract | defineBlock() and the shared block contract | | @classytic/cms-ui/hooks | useAutosave() and friends | | @classytic/cms-ui/mdx-io | Projects UI blocks into CMS serializer BlockSpec values | | @classytic/cms-ui/viewer | Lightweight client MDX renderer; optional runtime, no Plate/editor imports |

Plate's UI components (toolbar, slash menu, drag handle, popovers) are vendored into src/plate-ui/ and reskinned to consume the host's @/components/ui/* alias. The host doesn't run any shadcn install command for them — they ship inside this package.

Import the consumer viewer from its subpath (and dynamically in Next when appropriate):

const Viewer = dynamic(
  () => import('@classytic/cms-ui/viewer').then((module) => module.CmsMdxViewer),
  { ssr: false },
);

The /viewer graph depends on next-mdx-remote-client but imports no Plate kits. Hosts supply a closed components registry for domain blocks. Fluid remains the owner of prose/layout presentation around the viewer.

What the host owns (this package never touches)

  • Auth, routing, page chrome, navigation, sidebars
  • Brand theme tokens (Fluid handles)
  • Asset library viewer / picker UI (we expose onPickAsset callback)
  • Asset storage adapter (R2/S3/Bunny presigned URLs)
  • List / table / filter UI for ContentItems
  • Tier gating — host computes blocks array; no tier concept in this package
  • Save button placement, autosave indicator UI
  • Publish / archive button placement (host wires cms.repositories.contentItem.publish() to whatever button)
  • Brand-specific blocks: host defines via defineBlock(), passes via the blocks prop

Block contract

import { defineBlock } from '@classytic/cms-ui/contract';

defineBlock({
  key:        string;                          // Plate node type + MDX JSX tag name
  label:      string;                          // slash menu label
  icon?:      ComponentType;                   // slash menu / toolbar icon
  description?: string;
  category?:  'text' | 'media' | 'embed' | 'layout' | 'interactive';
  tag?:        string;                          // MDX JSX tag; defaults from key
  void?:       boolean;                         // explicit atomic/container behavior
  schema:     ZodSchema;                       // validates node props at save + render
  Component:  ComponentType<NodeProps>;        // rendered inside Plate
  toAttrs?:   (node) => Record<string, unknown>;
  fromAttrs?: (attrs) => Record<string, unknown>;
})

Two files per domain block: the defineBlock() call (registered into the editor) and the Component (renders the block visually). The same Component works in editor mode and in the consumer-site renderer — host imports both.

Build

npm run build       # tsdown — ESM, unbundle, .d.mts + .mjs per source file
npm run typecheck   # tsc --noEmit (strict)
npm test            # unit + integration (happy-dom)

License

MIT

Trademark

MIT-licensed code. "Classytic"/"arc" names + logos are trademarks of Classytic LLC — see TRADEMARK.md.