Skip to content
ilinxa/pro-ui

Rich Text Editor

alphav0.3.0

Plate-powered WYSIWYG editor and read-only viewer for long-form articles — code blocks, captioned images, floating toolbar, HTML export.

Category: Data DisplayUpdated: 2026-08-11Created: 2026-05-02Author: ilinxa

Context

First Plate (platejs) component in pro-ui. Two exports from one folder: `<RichTextEditor>` is a 'use client' editor (~165KB gzip after v0.2 additions) with a fixed top toolbar (marks / headings / lists / blockquote / link / image / table / code-block / font-family / font-size / color) PLUS a selection-anchored floating toolbar (marks + link). `<RichTextViewer>` is server-renderable via `platejs/static` (~32KB gzip). Both consume the same Plate `Value` JSON shape. Storage format is JSON, not HTML — Plate's docs warn against HTML round-trips. v0.2 adds: lowlight code-block syntax highlighting (15 languages registered, tokens themed via chart-1..5 palette in globals.css); per-image resize handle + inline caption editor (width stored as %); floating toolbar (selection-anchored, anchored via @floating-ui/react); HTML serialization escape hatch via `serializeRichTextToHtml(value)` for RSS / email / OG-tag export boundaries.

Installation

Initialize shadcn (once per project)Seeds lib/utils.ts and components.json. Skip if you've already used any shadcn component.
pnpm dlx shadcn@latest init
Register the @ilinxa namespace (once per project)Add to your components.json. Merge with existing config.
"registries": {
  "@ilinxa": "https://ui.ilinxa.com/r/{name}.json"
}
Install the component
pnpm dlx shadcn@latest add @ilinxa/rich-text-editor

Add -fixtures for dummy data:

pnpm dlx shadcn@latest add @ilinxa/rich-text-editor-fixtures

Preview

How sustainable cities are rethinking density

Across Europe and the Americas, urban planners are rewriting the rules — turning suburbs into 15-minute neighborhoods and waterfronts into thriving public spaces.

What changed in 2025

Three major policy shifts reshaped how cities approach growth: zoning reform, transit-oriented development, and carbon-constrained budgets.

We're no longer optimizing for cars — we're optimizing for the time people spend with their neighbors.

Three ideas worth tracking

Mixed-use density at every transit stop
Streets reclaimed as plazas (the Barcelona model)
Carbon-budgeted permitting (the Helsinki model)

For more, see  the full report .




Run an experiment:  code-friendly  variations of urban planning APIs.

Press Ctrl+S to save

Press Cmd/Ctrl+S to fire onSave. Select text to surface the floating toolbar.

Demo source

demo.tsxtsx

Usage

When to use

A WYSIWYG rich-text editor + read-only viewer for long-form article bodies — news, blog posts, doc pages, knowledge-base entries. Built on Plate (platejs), pro-ui's WYSIWYG substrate.

Two exports from one folder: RichTextEditor is a "use client" editor (~150KB gzip); RichTextVieweris server-renderable (~30KB gzip). Storage format is JSON — Plate's docs warn against HTML round-trips.

Editor

"use client"

import {
  RichTextEditor,
  type RichTextValue,
} from "@/components/rich-text-editor"

export function ArticleEditPage({ initial }: { initial: RichTextValue }) {
  const [value, setValue] = useState(initial)

  return (
    <RichTextEditor
      value={value}
      onChange={setValue}
      onSave={async (v) => {
        await fetch("/api/articles/save", { method: "POST", body: JSON.stringify(v) })
      }}
      onImageUpload={async (file) => {
        const fd = new FormData()
        fd.append("file", file)
        const res = await fetch("/api/upload", { method: "POST", body: fd })
        return res.json()    // { src, alt?, width?, height? }
      }}
    />
  )
}

Viewer (RSC)

The viewer is a server component — no "use client" boundary needed. It uses platejs/static to render the JSON without instantiating an editor.

import { RichTextViewer } from "@/components/rich-text-editor"

export default async function NewsArticle({ params }: { params: { id: string } }) {
  const article = await fetchArticle(params.id)
  return (
    <article>
      <h1>{article.title}</h1>
      <RichTextViewer value={article.body} />
    </article>
  )
}

Composed with the news-domain article column

<article className="lg:col-span-8">
  <h1>{article.title}</h1>

  <ArticleMeta divider items={metaItems} />

  <RichTextViewer value={article.body} />

  <ShareBar targets={[...]} title={article.title} headingAs="h4" divider />
</article>

Image upload contract

The component never talks to a backend directly. Pass onImageUpload — a function that takes a File and resolves to { src, alt?, width?, height? }. Without this prop, the editor falls back to a URL prompt.

Toolbar

Marks: bold, italic, underline, strikethrough, inline code, highlight, subscript, superscript. Blocks: H1–H3, blockquote, code block, horizontal rule. Lists: bullet, ordered. Insert: link, image, table. Styles: font family, font size, text color.

Notes

  • Storage format is the Plate Valuetype (an array of element nodes). Don't round-trip through HTML — keep JSON authoritative; serialize to HTML only at export boundaries (RSS, email, etc.).
  • The viewer renders bare HTML with Plate's data-slate-* attributes stripped — no editor instance, no client hooks. Safe in Server Components.
  • Press Cmd/Ctrl+S to fire onSave with the current document. Override autoFocus to focus the editor on mount.
  • The editor is echo-guarded on the value prop: external updates flow in via setValue on the editor without triggering an extra onChange emit.
  • Toolbar buttons are onMouseDown-driven so the editor selection isn't lost when clicking a button.
  • Plate ships a brand-recognizable suite of plugins under @platejs/*. This component pins to v53. When bumping Plate, re-verify SSR, image upload, and the JSON shape.

Features

  • Editor + Viewer split — same JSON shape, different bundle profiles
  • Fixed top toolbar (marks, headings, lists, blockquote, link, image, table, code, font-family, font-size, color)
  • Floating toolbar (selection-anchored marks + link insertion) — appears on text selection, anchored via @floating-ui/react virtual element
  • Lowlight syntax highlighting on code blocks (15 languages: bash / css / diff / go / html / java / javascript / json / markdown / python / rust / shell / sql / typescript / xml / yaml; tokens themed via pro-ui chart palette)
  • Image resize handle + inline caption editor (width stored as percentage, caption editable inline)
  • HTML serialization escape hatch via serializeRichTextToHtml(value) — async, server-only, for export boundaries (RSS / email / OG tags)
  • Image insertion via onImageUpload(file) Promise OR URL prompt fallback
  • Tables (insert + keyboard-driven row/col ops via Plate's table plugin)
  • Indent / outdent on paragraphs / headings / blockquotes / code blocks
  • Controlled OR uncontrolled value (echo-guarded sync from external value prop)
  • Cmd/Ctrl+S → onSave(value); platform-aware key descriptor in footer
  • JSON-as-storage (not HTML round-trips per Plate's storage guidance)
  • Read-only mode hides both toolbars; editor still selectable
  • Pure server-renderable viewer via createStaticEditor + PlateStatic
  • Tailwind v4 + signal-lime token integration; prose styling via @tailwindcss/typography

Tags

rich-text-editorrich-textwysiwygeditorviewerplateplatejslowlightsyntax-highlightingimagehtml-exportdata

Dependencies

shadcn primitives: button
npm peer deps: platejs@^53.0.3, @platejs/basic-nodes@^53.0.0, @platejs/basic-styles@^53.0.0, @platejs/caption@^53.0.0, @platejs/code-block@^53.0.0, @platejs/indent@^53.0.0, @platejs/link@^53.0.3, @platejs/list@^53.0.2, @platejs/media@^53.0.1, @platejs/table@^53.0.0, @floating-ui/react@^0.27.19, lowlight@^3.3.0, highlight.js@^11.11.1, lucide-react@^1.11.0