Skip to content

Store CSS classes, make editor toolbar extensible#143

Open
0aveRyan wants to merge 16 commits intoemdash-cms:mainfrom
0aveRyan:feat/content-styles-with-portable-text
Open

Store CSS classes, make editor toolbar extensible#143
0aveRyan wants to merge 16 commits intoemdash-cms:mainfrom
0aveRyan:feat/content-styles-with-portable-text

Conversation

@0aveRyan
Copy link
Copy Markdown

@0aveRyan 0aveRyan commented Apr 2, 2026

What does this PR do?

#144

Adds a plugin-driven editor styles system that lets plugins register toolbar buttons and dropdowns for toggling CSS classes on inline text and block elements. CSS
classes round-trip cleanly through ProseMirror ↔ Portable Text and are applied at render time on the published site.

Editor (admin)

  • CssClassMark — generic TipTap mark that stores arbitrary CSS classes on inline text spans. Persisted as cssClass markDefs in Portable Text. excludes: "" so
    multiple cssClass marks can stack on the same span.
  • BlockStyleExtension — adds a global cssClasses attribute to paragraph, heading, and every other block node. Persisted as a top-level cssClasses property on
    PT blocks.
  • EditorStyleToolbar — declarative toolbar renderer for buttons and dropdowns. Uses Floating UI portals so dropdowns escape overflow clipping inside the editor
    chrome.

Portable Text converters

The PT ↔ PM conversion is implemented in three independent places that all need to stay in lockstep: packages/core/src/content/converters/ (server),
packages/admin/.../PortableTextEditor.tsx (admin React editor), and packages/core/src/components/InlinePortableTextEditor.tsx (visual-edit inline editor). All three
are updated and pinned in place by the new parity test.

  • cssClasses is applied at the convertNode / convertBlock wrapper level, so every block type (paragraph, heading, blockquote, list item, image, code, break,
    horizontalRule) inherits it without per-case extraction.
  • Block-level cssClasses from a wrapping container (e.g. a styled blockquote) is merged with inner block classes via shared normalizeClassTokens /
    mergeCssClasses helpers, so a styled blockquote wrapping a paragraph that already has its own classes produces a single deduped token list.
  • cssClass markDefs are deduped by a namespaced key (cssClass:${classes}) to avoid colliding with link href markDefs that happen to share the same string.
  • Whitespace-only classes / cssClasses values are trimmed at every read/write boundary and rejected when empty, so PT storage and rendered HTML never contain
    class="" or class=" ".

Render path (published site)

  • New marks/CssClass.astro renders <span class="..."> for cssClass markDefs, walking outer-to-inner so multiple stacked marks nest as separate spans.
  • Block.astro, Image.astro, ListItem.astro, Code.astro, and Break.astro are extended (replacing astro-portabletext defaults where needed)
    to apply block-level cssClasses to the rendered element. All five guard against whitespace-only values.

Plugin API

admin.editorStyles flows from definePlugin() → runtime manifest → API → admin UI → editor toolbar, mirroring the existing portableTextBlocks pipeline. When no
plugin provides editorStyles, the toolbar is unchanged — zero behavioral difference.

definePlugin({
  id: "my-styles",
  admin: {
    editorStyles: [
      { type: "button", label: "Highlight", icon: "highlighter", scope: "inline", classes: "bg-yellow-200" },
      { type: "dropdown", label: "Typography", icon: "textAa", items: [
        { label: "Small Caps", scope: "inline", classes: "font-smallcaps" },
        { type: "separator" },
        { label: "Drop Cap", scope: "block", classes: "dropcap" },
        { label: "Ornamental Rule", scope: "block", classes: "divider-ornamental", nodes: ["horizontalRule"] },
      ]},
    ],
  },
});

Out of scope

  • No changes to link href or image alt whitespace handling (parallel issue, separate follow-up).
  • No zod-schema tightening for menus' cssClasses field.
  • No new built-in styles — plugins are the only source of toolbar entries.

Type of change

  • Bug fix
  • Feature (requires approved Discussion)
  • Refactor (no behavior change)
  • Documentation
  • Performance improvement
  • Tests
  • Chore (dependencies, CI, tooling)

Checklist

AI-generated code disclosure

  • This PR includes AI-generated code

Testing

Added three test files covering the cssClass family end-to-end:

  • packages/core/tests/unit/converters/css-classes.test.ts — block-level merging, mark round-trip, dedup, nested container/inner class merging, link/cssClass
    key-collision regression, and whitespace-only normalization (mark + block, both directions).
  • packages/core/tests/unit/converters/css-classes-parity.test.ts — pins the core and inline converters to the same normalized output shapes for a shared
    fixture set, so the two implementations cannot drift apart silently.
  • packages/admin/tests/editor/css-classes-conversion.test.ts — mirrors the parity coverage for the admin React converter (which can't be imported into core tests
    because of React).

Targeted runs:

pnpm --filter emdash test css-classes
pnpm --filter @emdash-cms/admin test css-classes-conversion

Files changed

23 files in packages/core and packages/admin directly related to this feature (~3.3k insertions, ~100 deletions excluding tests).

Area File Change
Plugin types packages/core/src/plugins/types.ts EditorStyleItem, EditorStyleButton, EditorStyleDropdown, EditorStyleEntry + editorStyles on
PluginAdminConfig
Manifest packages/core/src/astro/types.ts editorStyles on ManifestPlugin
Manifest packages/core/src/emdash-runtime.ts Serialize editorStyles in getManifest()
PT types packages/core/src/content/converters/types.ts cssClasses?: string on text and image blocks
Converter packages/core/src/content/converters/prosemirror-to-portable-text.ts cssClass mark → markDef, applyCssClasses wrapper, token-merge helpers,
namespaced dedup keys, whitespace trim
Converter packages/core/src/content/converters/portable-text-to-prosemirror.ts Reverse conversions with applyCssClasses wrapper, whitespace-safe
getCssClasses
Inline editor packages/core/src/components/InlinePortableTextEditor.tsx Full PM ↔ PT cssClass + cssClasses parity with the core converter
Render packages/core/src/components/marks/CssClass.astro New — renders <span class="..."> for cssClass markDefs
Render packages/core/src/components/Block.astro New — overrides default Block to apply block-level cssClasses
Render packages/core/src/components/ListItem.astro New — overrides default ListItem to apply cssClasses
Render packages/core/src/components/{Image,Code,Break}.astro Apply block-level cssClasses with whitespace guard
Editor packages/admin/src/components/editor/CssClassMark.ts New — generic TipTap mark for inline CSS classes
Editor packages/admin/src/components/editor/BlockStyleExtension.ts New — cssClasses global attribute on block nodes
Editor packages/admin/src/components/editor/EditorStyleToolbar.tsx New — declarative toolbar renderer (Floating UI portal)
Editor packages/admin/src/components/PortableTextEditor.tsx Wire extensions, editorStyles prop, PM ↔ PT cssClass conversions
Editor packages/admin/src/components/ContentEditor.tsx Thread editorStyles prop through
Admin API packages/admin/src/lib/api/client.ts editorStyles on AdminManifest plugin type
Admin router packages/admin/src/router.tsx getEditorStyles() extraction + wire through content pages
Tests packages/core/tests/unit/converters/css-classes.test.ts New (~700 lines)
Tests packages/core/tests/unit/converters/css-classes-parity.test.ts New core/inline parity harness
Tests packages/admin/tests/editor/css-classes-conversion.test.ts New admin converter coverage

@changeset-bot
Copy link
Copy Markdown

changeset-bot bot commented Apr 2, 2026

⚠️ No Changeset found

Latest commit: 2b56370

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 2, 2026

All contributors have signed the CLA ✍️ ✅
Posted by the CLA Assistant Lite bot.

@0aveRyan
Copy link
Copy Markdown
Author

0aveRyan commented Apr 2, 2026

I have read the CLA Document and I hereby sign the CLA

github-actions bot added a commit that referenced this pull request Apr 2, 2026
@0aveRyan 0aveRyan marked this pull request as ready for review April 3, 2026 12:59
@0aveRyan 0aveRyan force-pushed the feat/content-styles-with-portable-text branch from b50b46f to 00e2981 Compare April 4, 2026 01:15
@github-actions
Copy link
Copy Markdown
Contributor

github-actions bot commented Apr 5, 2026

Overlapping PRs

This PR modifies files that are also changed by other open PRs:

This may cause merge conflicts or duplicated work. A maintainer will coordinate.

Copilot AI review requested due to automatic review settings April 5, 2026 07:50
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Adds a plugin-driven “editor styles” system that lets plugins register toolbar buttons/dropdowns for toggling CSS classes on inline text (marks) and block nodes (attrs), and persists those styles through the Portable Text ↔︎ ProseMirror conversion pipeline.

Changes:

  • Introduces editorStyles types on plugin admin config, and threads them through runtime manifest → admin API → router → editor props.
  • Adds TipTap extensions for inline cssClass marks and block cssClasses attributes, plus a generic EditorStyleToolbar renderer.
  • Updates PT converters (core + admin-side copies) to round-trip cssClass markDefs and block-level cssClasses, plus HR variant.

Reviewed changes

Copilot reviewed 13 out of 13 changed files in this pull request and generated 7 comments.

Show a summary per file
File Description
packages/core/src/plugins/types.ts Adds editor style entry type system and exposes editorStyles on plugin admin config.
packages/core/src/emdash-runtime.ts Serializes editorStyles into the runtime manifest.
packages/core/src/content/converters/types.ts Adds cssClasses?: string to PT text blocks.
packages/core/src/content/converters/prosemirror-to-portable-text.ts Converts cssClass marks + adds cssClasses propagation wrapper; adds HR variant.
packages/core/src/content/converters/portable-text-to-prosemirror.ts Converts cssClass markDefs back; applies cssClasses onto node attrs; adds HR variant.
packages/core/src/astro/types.ts Adds editorStyles to the manifest plugin type for Astro consumers.
packages/admin/src/router.tsx Extracts editorStyles from all plugins and passes into content pages.
packages/admin/src/lib/api/client.ts Extends AdminManifest plugin type to include editorStyles.
packages/admin/src/components/editor/EditorStyleToolbar.tsx New generic toolbar renderer (buttons + dropdown) that maps config to TipTap commands.
packages/admin/src/components/editor/CssClassMark.ts New TipTap mark for arbitrary inline CSS classes.
packages/admin/src/components/editor/BlockStyleExtension.ts New TipTap extension adding cssClasses to block nodes and commands to toggle it.
packages/admin/src/components/PortableTextEditor.tsx Wires new extensions + threads editorStyles prop into the toolbar; updates admin-side converters.
packages/admin/src/components/ContentEditor.tsx Threads editorStyles down to PortableTextEditor.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new bot commented Apr 5, 2026

Open in StackBlitz

@emdash-cms/admin

npm i https://pkg.pr.new/@emdash-cms/admin@143

@emdash-cms/auth

npm i https://pkg.pr.new/@emdash-cms/auth@143

@emdash-cms/blocks

npm i https://pkg.pr.new/@emdash-cms/blocks@143

@emdash-cms/cloudflare

npm i https://pkg.pr.new/@emdash-cms/cloudflare@143

emdash

npm i https://pkg.pr.new/emdash@143

create-emdash

npm i https://pkg.pr.new/create-emdash@143

@emdash-cms/gutenberg-to-portable-text

npm i https://pkg.pr.new/@emdash-cms/gutenberg-to-portable-text@143

@emdash-cms/x402

npm i https://pkg.pr.new/@emdash-cms/x402@143

@emdash-cms/plugin-ai-moderation

npm i https://pkg.pr.new/@emdash-cms/plugin-ai-moderation@143

@emdash-cms/plugin-atproto

npm i https://pkg.pr.new/@emdash-cms/plugin-atproto@143

@emdash-cms/plugin-audit-log

npm i https://pkg.pr.new/@emdash-cms/plugin-audit-log@143

@emdash-cms/plugin-color

npm i https://pkg.pr.new/@emdash-cms/plugin-color@143

@emdash-cms/plugin-embeds

npm i https://pkg.pr.new/@emdash-cms/plugin-embeds@143

@emdash-cms/plugin-forms

npm i https://pkg.pr.new/@emdash-cms/plugin-forms@143

@emdash-cms/plugin-webhook-notifier

npm i https://pkg.pr.new/@emdash-cms/plugin-webhook-notifier@143

commit: 2b56370

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 17 out of 17 changed files in this pull request and generated 7 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@0aveRyan 0aveRyan force-pushed the feat/content-styles-with-portable-text branch from 1ffeb20 to 5a187ed Compare April 7, 2026 21:03
@0aveRyan 0aveRyan requested a review from Copilot April 8, 2026 14:11
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 28 out of 28 changed files in this pull request and generated 9 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 27 out of 27 changed files in this pull request and generated 3 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 27 out of 27 changed files in this pull request and generated 3 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 29 out of 29 changed files in this pull request and generated 3 comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

0aveRyan and others added 16 commits April 9, 2026 06:36
Underlying foundation for letting EmDash Plugins register Editor toolbar buttons and dropdowns and allow for CSS class names to be stored for blocks and markdefs.
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
…ror.ts

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
…ror.ts

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
@0aveRyan 0aveRyan force-pushed the feat/content-styles-with-portable-text branch from 38932be to 2b56370 Compare April 9, 2026 13:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants