Store CSS classes, make editor toolbar extensible#143
Store CSS classes, make editor toolbar extensible#1430aveRyan wants to merge 16 commits intoemdash-cms:mainfrom
Conversation
|
|
All contributors have signed the CLA ✍️ ✅ |
|
I have read the CLA Document and I hereby sign the CLA |
b50b46f to
00e2981
Compare
Overlapping PRsThis PR modifies files that are also changed by other open PRs:
This may cause merge conflicts or duplicated work. A maintainer will coordinate. |
There was a problem hiding this comment.
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
editorStylestypes on plugin admin config, and threads them through runtime manifest → admin API → router → editor props. - Adds TipTap extensions for inline
cssClassmarks and blockcssClassesattributes, plus a genericEditorStyleToolbarrenderer. - Updates PT converters (core + admin-side copies) to round-trip
cssClassmarkDefs and block-levelcssClasses, plus HRvariant.
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.
packages/core/src/content/converters/prosemirror-to-portable-text.ts
Outdated
Show resolved
Hide resolved
@emdash-cms/admin
@emdash-cms/auth
@emdash-cms/blocks
@emdash-cms/cloudflare
emdash
create-emdash
@emdash-cms/gutenberg-to-portable-text
@emdash-cms/x402
@emdash-cms/plugin-ai-moderation
@emdash-cms/plugin-atproto
@emdash-cms/plugin-audit-log
@emdash-cms/plugin-color
@emdash-cms/plugin-embeds
@emdash-cms/plugin-forms
@emdash-cms/plugin-webhook-notifier
commit: |
There was a problem hiding this comment.
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.
packages/core/src/content/converters/portable-text-to-prosemirror.ts
Outdated
Show resolved
Hide resolved
1ffeb20 to
5a187ed
Compare
There was a problem hiding this comment.
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.
packages/core/src/content/converters/prosemirror-to-portable-text.ts
Outdated
Show resolved
Hide resolved
packages/core/src/content/converters/portable-text-to-prosemirror.ts
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
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.
packages/core/src/content/converters/portable-text-to-prosemirror.ts
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
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.
packages/core/src/content/converters/portable-text-to-prosemirror.ts
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
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.
ee86c82 to
38932be
Compare
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>
38932be to
2b56370
Compare
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 ascssClassmarkDefs in Portable Text.excludes: ""somultiple cssClass marks can stack on the same span.
BlockStyleExtension— adds a globalcssClassesattribute to paragraph, heading, and every other block node. Persisted as a top-levelcssClassesproperty onPT blocks.
EditorStyleToolbar— declarative toolbar renderer for buttons and dropdowns. Uses Floating UI portals so dropdowns escape overflow clipping inside the editorchrome.
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), andpackages/core/src/components/InlinePortableTextEditor.tsx(visual-edit inline editor). All threeare updated and pinned in place by the new parity test.
cssClassesis applied at theconvertNode/convertBlockwrapper level, so every block type (paragraph, heading, blockquote, list item, image, code, break,horizontalRule) inherits it without per-case extraction.
cssClassesfrom a wrapping container (e.g. a styled blockquote) is merged with inner block classes via sharednormalizeClassTokens/mergeCssClasseshelpers, so a styled blockquote wrapping a paragraph that already has its own classes produces a single deduped token list.cssClassmarkDefs are deduped by a namespaced key (cssClass:${classes}) to avoid colliding with linkhrefmarkDefs that happen to share the same string.classes/cssClassesvalues are trimmed at every read/write boundary and rejected when empty, so PT storage and rendered HTML never containclass=""orclass=" ".Render path (published site)
marks/CssClass.astrorenders<span class="...">forcssClassmarkDefs, walking outer-to-inner so multiple stacked marks nest as separate spans.Block.astro,Image.astro,ListItem.astro,Code.astro, andBreak.astroare extended (replacing astro-portabletext defaults where needed)to apply block-level
cssClassesto the rendered element. All five guard against whitespace-only values.Plugin API
admin.editorStylesflows fromdefinePlugin()→ runtime manifest → API → admin UI → editor toolbar, mirroring the existingportableTextBlockspipeline. When noplugin provides
editorStyles, the toolbar is unchanged — zero behavioral difference.Out of scope
hrefor imagealtwhitespace handling (parallel issue, separate follow-up).cssClassesfield.Type of change
Checklist
pnpm typecheckpassespnpm --silent lint:json | jq '.diagnostics | length'returns 0pnpm testpasses (or targeted tests for my change)pnpm formathas been runAI-generated code disclosure
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/cssClasskey-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 sharedfixture 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 testsbecause of React).
Targeted runs:
Files changed
23 files in
packages/coreandpackages/admindirectly related to this feature (~3.3k insertions, ~100 deletions excluding tests).packages/core/src/plugins/types.tsEditorStyleItem,EditorStyleButton,EditorStyleDropdown,EditorStyleEntry+editorStylesonPluginAdminConfigpackages/core/src/astro/types.tseditorStylesonManifestPluginpackages/core/src/emdash-runtime.tseditorStylesingetManifest()packages/core/src/content/converters/types.tscssClasses?: stringon text and image blockspackages/core/src/content/converters/prosemirror-to-portable-text.tscssClassmark → markDef,applyCssClasseswrapper, token-merge helpers,packages/core/src/content/converters/portable-text-to-prosemirror.tsapplyCssClasseswrapper, whitespace-safegetCssClassespackages/core/src/components/InlinePortableTextEditor.tsxpackages/core/src/components/marks/CssClass.astro<span class="...">for cssClass markDefspackages/core/src/components/Block.astropackages/core/src/components/ListItem.astropackages/core/src/components/{Image,Code,Break}.astropackages/admin/src/components/editor/CssClassMark.tspackages/admin/src/components/editor/BlockStyleExtension.tscssClassesglobal attribute on block nodespackages/admin/src/components/editor/EditorStyleToolbar.tsxpackages/admin/src/components/PortableTextEditor.tsxeditorStylesprop, PM ↔ PT cssClass conversionspackages/admin/src/components/ContentEditor.tsxeditorStylesprop throughpackages/admin/src/lib/api/client.tseditorStylesonAdminManifestplugin typepackages/admin/src/router.tsxgetEditorStyles()extraction + wire through content pagespackages/core/tests/unit/converters/css-classes.test.tspackages/core/tests/unit/converters/css-classes-parity.test.tspackages/admin/tests/editor/css-classes-conversion.test.ts