GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Sign in
  • ActionsCI/CD and automation
  • CodespacesCloud development environments
  • All Features
  • Documentation
  • Training
  • Certifications
  • Build with Copilot
  • Customize Copilot
  • Integrations
  • Trending repos
  • GitHub Sponsors
  • Developer articles
  • Enterprises

This view

  • Original page
  • New address

GeezerVison is showing github.com — GitHub - hunvreus/pagescms-editor: A simple, Notion-like WYSIWYG editor component for shadcn/ui. Built with TipTap, ProseMirror, and React.

BranchesTags
Open more actions menu

Latest commit

 

History

38 Commits
38 Commits

Folders and files

NameName
Last commit message
Last commit date
.github/workflows
.github/workflows
 
 
fixtures/strict-consumer
fixtures/strict-consumer
 
 
public
public
 
 
registry/default/editor
registry/default/editor
 
 
scripts
scripts
 
 
src
src
 
 
tests
tests
 
 
.gitignore
.gitignore
 
 
README.md
README.md
 
 
components.json
components.json
 
 
index.html
index.html
 
 
package-lock.json
package-lock.json
 
 
package.json
package.json
 
 
playwright.config.ts
playwright.config.ts
 
 
registry.json
registry.json
 
 
tsconfig.json
tsconfig.json
 
 
vite.config.ts
vite.config.ts
 
 

Repository files navigation

Editor

Editor is a format-aware TipTap component (HTML or Markdown) with:

  • Bubble menu formatting controls
  • Slash commands
  • Links, images (including ALT editing), and tables

Mounting the editor or changing disabled does not emit onChange; content changes do.

Component export:

  • Editor from src/components/ui/editor.tsx

Local Development

npm install
npm run dev

Build:

npm run build
npm run registry:build

npm run build now:

  • builds client assets
  • builds SSR entry
  • prerenders / into dist/index.html

Styling Model

  • Uses shadcn-style Tailwind utility classes and semantic tokens.
  • Uses local shadcn component files where needed (button, tabs).
  • Demo/base styles live in src/index.css.

Registry

Registry source:

  • registry.json
  • registry/default/editor/*

Build registry artifacts:

npm run registry:build

Generated files:

  • public/r/registry.json
  • public/r/editor.json

Install In Another Project

Install this component directly from GitHub:

npx shadcn@latest add https://editor.pagescms.org/r/editor.json

Basic Usage

import { useState } from "react";
import { Editor } from "@/components/ui/editor";

export function Example() {
  const [value, setValue] = useState("");

  return (
    <Editor
      value={value}
      onChange={setValue}
    />
  );
}

Notes:

  • Default format is "html".
  • format supports "markdown" and "html".
  • className applies classes to the root wrapper (cn-editor).
  • editorClassName applies classes to the WYSIWYG surface only.

HTML policy

markdownHtml uses CSS selectors to control HTML in Markdown and HTML modes:

<Editor
  format="markdown"
  value={value}
  onChange={setValue}
  markdownHtml={{
    keep: ["div.callout", "details", "summary", "span.badge", "figure"],
    strip: ["div.legacy-wrapper"],
    drop: [".discard"],
    attributes: ["data-kind"],
  }}
/>
  • keep retains matching supported wrappers. div, section, article, aside, details, and summary contain editable editor content; retained span elements are editable marks. Other permitted browser HTML, such as figure, is displayed as an atomic fragment; clicking selects it without following links, and double-clicking edits its HTML.
  • strip replaces a matching element and its descendants with plain text, losing nested formatting. Descendants matching drop are removed before extracting text.
  • drop removes a matching element and its contents.
  • attributes permits additional attributes on retained wrappers/fragments, such as data-kind. Classes, IDs, and standard link, image, table, list, and details attributes are already permitted. Class preservation keeps every class value; selectors choose elements, not attribute values.

Precedence is drop, then strip, then keep, then normal Tiptap parsing. Rules apply to nested elements as well as roots. Scripts, event handlers, inline styles, unsafe URLs, frames, MathML, and form controls remain forbidden, including when explicitly matched by keep. HTML source, clipboard input, rendered fragments, and edited fragment output are sanitized with DOMPurify. Source mode remains a source editor: switching modes alone does not rewrite the stored value; policy-filtered output is emitted when editing the visual editor.

Controlled values may arrive asynchronously: acknowledgments of earlier emitted edits do not replace newer typing. Explicit external replacements still reload the editor content.

Static SVG icons are supported by default in HTML and Markdown, without an extra option or keep selector. Drawing elements and SVG attributes such as viewBox, fill, and stroke survive. Scripts, events, embedded HTML, animation, links, external resources, and unsupported SVG elements are removed even with permissive attribute configuration. Icons are rendered fragments with double-click source editing. SVG support is for static artwork, not interactive SVG applications.

In Markdown, retained containers serialize their entire contents as HTML. Markdown inside an explicitly kept container is parsed into supported editor content; output normalizes that content to HTML. Inline Markdown inside retained spans is supported. Whitespace, attribute order, and wrapper boundaries can normalize during editing.

HTML comments and unknown capitalized component syntax such as <Badge text="New" /> are preserved in Markdown as visible, selectable source items. Double-click to edit their source. A fragment containing nested comments or components falls back to a source item for the whole fragment; its interior is not visually editable. These opaque items are source rather than editable HTML, so nested selector rules are not applied inside them. Root strip/drop rules still apply. Source containing unsafe browser HTML exports as a code literal. HTML-mode source items serialize as escaped code, not executable components. This is not general MDX support: complex expressions (such as object-valued props) are not reliably preserved and must stay in source mode. Shortcode/MDX expression parsing, embeds, arbitrary extensions, and custom CSS are outside this implementation.

Selector rules run when content enters the visual editor. Rendering and saving do not apply them again. Prefer class or attribute selectors for stable policies: positional selectors can match different children when filtered content is reopened.

The policy is supplied when mounting the editor. Remount when changing its rules.

Image Workflows

Editor supports both pre-uploaded URLs and local file uploads.

  • Slash image selector:
    • Return { kind: "url", src } from onRequestImage if the image is already uploaded.
    • Return { kind: "file", file } from onRequestImage to run optimistic upload flow.
  • Paste/drop:
    • Enable with enableImagePasteDrop.
    • Provide onUploadImage to upload files and return final { src }.
    • Optional fallback with imageFallback="data-url" for base64 insertion.
  • Swap behavior:
    • The editor preloads the final uploaded URL before replacing the temporary blob: preview.
    • If preload fails or times out, the editor keeps the temporary preview and marks the image with an upload error state.
  • Pending uploads:
    • Use onPendingUploadsChange to disable autosave/publish while uploads are in progress.

Implement Your Own Source Toggle

Editor is intentionally focused on rich-text editing. If you need a source view, keep a single shared value state and switch between Editor and your own text input.

import { useState } from "react";
import { Editor } from "@/components/ui/editor";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Textarea } from "@/components/ui/textarea";

type View = "editor" | "source";

export function EditorWithSourceToggle() {
  const [view, setView] = useState<View>("editor");
  const [value, setValue] = useState("");

  return (
    <Tabs value={view} onValueChange={(next) => setView(next as View)} className="w-full">
      <TabsList>
        <TabsTrigger value="editor">Editor</TabsTrigger>
        <TabsTrigger value="source">Source</TabsTrigger>
      </TabsList>
      <TabsContent value="editor">
        <Editor
          value={value}
          onChange={setValue}
          format="markdown"
        />
      </TabsContent>
      <TabsContent value="source">
        <Textarea
          value={value}
          onChange={(event) => setValue(event.target.value)}
          className="min-h-64 font-mono"
        />
      </TabsContent>
    </Tabs>
  );
}

Props

Prop Type Default Description
value string "" Current editor content.
onChange (value: string) => void - Called whenever content changes.
format "markdown" | "html" "html" Parsing and output format.
disabled boolean false Disables editing and toolbar actions.
enableImages boolean true Enables image-related behaviors (slash command and image actions).
enableImagePasteDrop boolean false Enables image file paste and drag/drop insertion.
onRequestImage (ctx) => { kind: "url", src, alt?, title? } | { kind: "file", file, alt?, title? } | null | Promise<...> - Called by Image slash command. Return a URL result for already-uploaded images or a file result for local upload flow.
onUploadImage (file, ctx) => { src, alt?, title? } | null | Promise<...> - Upload hook for local files (paste/drop/slash file).
imageFallback "data-url" | "prompt-url" | "none" "prompt-url" Fallback when no callback inserts an image.
maxImageBytes number 1000000 Max file size used by "data-url" fallback.
onPendingUploadsChange (count: number) => void - Receives pending optimistic upload count.
markdownHtml { keep?: string[]; strip?: string[]; drop?: string[]; attributes?: string[] } - Controls HTML retention, filtering, and attributes using CSS selectors.
className string - Extra classes for the root wrapper (cn-editor).
editorClassName string - Extra classes for the WYSIWYG surface.
...props HTMLAttributes<HTMLDivElement> - Forwarded to the root container.

Deploy (Cloudflare Pages)

Use Git integration with these settings:

  • Framework preset: None
  • Root directory: editor
  • Build command: npm run build
  • Build output directory: dist

This deploys the prerendered static HTML + hydrated React demo.

Keeping registry consumers aligned

Edit registry/default/editor/html-policy.ts, then distribute and verify the exact module used by the demo and Pages CMS:

npm run sync:html-policy -- ../pagescms
npm run check:html-policy -- ../pagescms
npm run registry:build

The CMS adapter and media behavior stay outside this module. The sync command does not overwrite the CMS editor entry or its local integration changes.

Browser tests

Install the test browsers with npx playwright install chromium firefox webkit, then run npm test. The suite runs against Chromium, Firefox, and WebKit. To run one browser, use npm test -- --project=chromium.

HTML policy tests cover both HTML and Markdown output, repeated edit/reopen cycles, nested content, selector precedence, attribute filtering, clipboard input, malformed markup, source editing and undo, and malicious HTML. The strict consumer check is npm run fixture:strict.

Snippets

Pass snippets={[{ label: "Alert", source: '<div class="alert"><h2>Heads up!</h2><section>Your message.</section></div>' }]} to the editor. Snippets appear in the slash menu and can be searched by label. Their source is parsed, rather than inserted as escaped text, using the field format by default; optional format: "html" | "markdown" overrides the input format. Output still uses the editor format. HTML policy and sanitization apply as usual: a snippet does not implicitly keep its wrappers. Insertion replaces the slash query and can be undone. Snippets are unavailable inside table cells.

The slash menu scrolls the selected command into view during keyboard navigation and also supports mouse-wheel scrolling.

Kept header and footer use the same editable container behavior as div and section. Direct inline content is normalized into paragraphs; output preserves content and supported attributes, rather than the exact original HTML structure. Editable links retain standard and configured extra attributes through text and URL edits.

Definition lists are visually editable with html={{ keep: ["dl"] }}. Their dt terms and dd descriptions are retained automatically; descriptions accept blocks and nested definition lists, while terms accept inline formatting. Direct description text normalizes into a paragraph. Existing typeset styles cover terms, descriptions, and spacing.

Standalone images support an optional link through the existing Link control. HTML exports <a><img></a>; Markdown exports [![alt](image)](destination), or HTML when preserved link-wrapper attributes require it. Image links can be changed or removed without changing the image, and clicks select the image rather than navigate. Destination URLs use the same URI validation as ordinary links.

About

A simple, Notion-like WYSIWYG editor component for shadcn/ui. Built with TipTap, ProseMirror, and React.

Topics

editornotionnotion-editorreactreactjsshadcnshadcn-uishadcnuitailwindtailwindcsswysiwygwysiwyg-editor

Resources

Readme
Activity

Stars

106 stars

Watchers

0 watching

Forks

7 forks
Report repository

Releases

Sponsor this project

Packages

Contributors

Languages

You are visitor 01402503 — GeezerVison does not store the pages you look up.
Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.