Skip to content

feat(editor): Editor molecule#718

Merged
netchampfaris merged 15 commits into
mainfrom
v1/texteditor
Jun 1, 2026
Merged

feat(editor): Editor molecule#718
netchampfaris merged 15 commits into
mainfrom
v1/texteditor

Conversation

@netchampfaris
Copy link
Copy Markdown
Contributor

@netchampfaris netchampfaris commented May 21, 2026

Summary

Adds the v1 editor family under the frappe-ui/editor subpath (source in src/molecules/editor). The API is built around one renderless <Editor> component plus composable building blocks and configurable extension kits — there are no monolithic "ready-made" editors; consumers compose the pieces they need.

Public surface (frappe-ui/editor)

  • EngineuseEditor composable (built on @tiptap/core) and the <Editor> component. <Editor> is renderless: it owns the editor lifecycle, v-model (HTML or JSON via format), upload, and placeholder threading, and exposes { editor, isEmpty } to its slot.
  • Building blocksEditorContent, EditorFixedMenu, EditorBubbleMenu, EditorFloatingMenu. Usable inside <Editor> (they read the editor from context) or standalone on a useEditor ref via an explicit :editor prop.
  • KitsCommentKit, RichTextKit, InlineKit: configurable extension bundles. Each member can be reconfigured or removed (false), so one preset adapts across use cases.
  • Extensions — flat named exports with frappe-ui defaults applied: Link, Code/CodeBlock, Image/ImageGroup/ImageViewer/Video/Iframe, Mention, Tag, Emoji, SlashCommands, Table, TaskList, Toc, Color/Highlight, Typography, TextAlign, Placeholder, plus StarterKit. Tree-shakes naturally.
  • Menu items + presets — toolbar item definitions, groups, separators, and ready presets (minimalToolbar, commentToolbar, articleToolbar). Items self-prune when their extension isn't loaded.
  • Upload plumbing — a shared media-upload engine; uploadFunction is threaded through editor storage and read at use-time (v3-safe, since extension.options is frozen).

Other changes

  • Editor guide added under Molecules docs, with stories (RichText, Comment, Inline, Primitives).
  • Source path aliases added (@components, @molecules, @utils, @composables, frappe-ui/editor) across Vite, Vitepress, and tsconfig.

Fixes from a TipTap-conventions review

  • format="json" no longer resets the selection on every keystroke — the v-model echo guard previously only covered HTML; JSON's fresh-object identity defeated it, so each edit re-ran setContent() and rebuilt the doc. Now the exact emitted value is tracked and skipped.
  • Toolbar active/disabled state is reactive again — the @tiptap/core-based editor isn't a reactive ref like @tiptap/vue-3's, so isActive/isDisabled reads in EditorFixedMenu went stale on selection changes. MenuItems now bumps a version on each editor transaction to drive re-evaluation (scoped per instance, works with or without <Editor>).

Validation

  • yarn vitest --run src/molecules/editor — 97 passing
  • yarn build
  • yarn docs:build

Docs preview: https://ui.frappe.io/pr-preview/pr-718/

Coverage: 56.13% (+6.69% vs main)

netchampfaris and others added 14 commits June 2, 2026 04:55
Replaces the headless-primitives + ready-mades direction with a single <TextEditor> on the useEditor engine, configured by StarterKit-style kits and explicit fixedMenu/bubbleMenu/floatingMenu props. The library ships no assembled editors; each app builds its own component on <TextEditor>.

- ADR-0004 rewritten and renamed (...-primitives-and-readymades -> ...-composition-model): subpath is a dependency wall not a tree-shaking tool; v0 monolith retained for human-gated removal; gameplan port is the acceptance gate
- spec/editor.md rewritten: engine, component + L0-L4 ladder, kits (configure/disable + structure), two-axis menu model, placeholder-via-storage, TextStyle dep, isAvailable hide-pruning
- CONTEXT.md editor vocabulary updated (kits, app editor component)
- v1-release/issues/editor-family: 9 dependency-ordered issues + README (status lines, keep/new/delete inventory, guardrails)
- research/12 marked superseded

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Implements the editor family per spec/editor.md + ADR-0004: one
<TextEditor> on the useEditor engine, configured by kits and explicit
menu props — no ready-made assembled editors.

Issue 03 — menus: rename :buttons -> :items across the menu components;
add isAvailable(editor) to predefined items (schema mark/node or
extension check) so MenuItems hides unavailable items and drops empty
groups (one preset adapts across kits).

Issue 05 — kits (new kits.ts): CommentKit / RichTextKit / InlineKit as
configurable StarterKit-style bundles; flat `Partial<Opts> | false`
members; heading threaded into StarterKit; Color co-registers TextStyle;
InlineKit single-line via a one-block Document. Data members
(mention/tag) inert until given items.

Issue 06 — component: rebuild TextEditor.vue on useEditor (required
extensions, unnamed v-model + format, reactive placeholder via
editor.storage.placeholder + editable, autofocus/uploadFunction/
maxHeight, fixedMenu/bubbleMenu/floatingMenu props + slots). Delete the
RichTextEditor/CommentEditor/InlineEditor ready-mades.

Issue 02 alignment: add TextStyle export (fix v3 named import); make Tag
a complete node + gated suggestion; move mention/tag to canonical
{ items }; storage-thread Placeholder; wire the richer LinkExtension
(inline edit popup, Mod-k, paste handling) as the exported Link.

Issue 08 (exports): index.ts now ships the spec surface; ready-mades
removed; stories re-composed on TextEditor + kits + presets.

Tests: 34 editor tests (kits via getSchema/resolveExtensions, TextEditor
with a real editor in jsdom, menu self-pruning). v0 monolith untouched.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Realign the v1 editor to the renderless <TextEditor> design and fix the two
library bugs the gameplan parity port surfaced.

Renderless redesign (spec/editor.md + ADR-0004 amendment):
- <TextEditor> renders no UI of its own — it owns the editor lifecycle,
  v-model, upload and placeholder threading, and exposes { editor, isEmpty }
  through its #default slot. Drop the fixedMenu/bubbleMenu/floatingMenu props
  and the #actions/#fixedMenu/#bubbleMenu/#floatingMenu slots; the consumer
  composes layout from the building blocks inside the slot.
- Add focus/blur/transaction emits and a defineExpose escape hatch.
- Move placeholder threading into setPlaceholder() next to the extension so
  the component never reaches into editor storage.

String-icon menu convention:
- Predefined menu items ship a default lucide-* string icon; MenuItems masks
  it into an icon span (Button's house path). Add InlineCode, Undo, Redo.
- Migrate editor node-views (image/iframe/image-group, image viewer, link
  popup, slash list) from ~icons/lucide/* component imports to lucide-* spans.
- RichTextKit now includes StyleClipboard.

Parity fixes (from the gameplan port, see .parity-evidence):
- Bubble/floating menus rendered nothing: EditorBubbleMenu/EditorFloatingMenu
  imported the Extension object from @tiptap/extension-* instead of the Vue
  component from @tiptap/vue-3/menus. Fix the imports + the test mocks, which
  had masked the bug by mocking the same wrong package.
- Image/video upload was dead: tiptap v3 froze extension.options into an
  immutable getter, so uploadAware's cached uploadFunction stayed null. Drop
  uploadAware; resolve the function at use-time via resolveUploadOptions
  (editor.storage.upload) across image/video/content-paste/image-group.

Tests: 32 pass (kits, menu, useEditor, TextEditor).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…s, docs

Rename the v1 component TextEditor → Editor and export it from
frappe-ui/editor (the v0 `TextEditor` alias stays at top-level frappe-ui,
so the two no longer collide). Building blocks now resolve the editor from
context via editor-context.ts, so the :editor prop is optional inside <Editor>.

- iframe: replace InsertIframe.vue with a dialog-controller
  (IframeInsertDialog + iframeInsertDialogController)
- link: rework LinkEditorPopup, add link-shortcut-plugin, and wire the
  custom LinkExtension into the exported Link
- restore tippy.js (the in-place v0 monolith still imports it; it was
  dropped in error and broke any consumer of top-level frappe-ui)
- docs: rewrite molecules/editor.md to the v1 model and add the v0→v1
  migration guide (## Editor) to migration.md
- spec/issues: rename to <Editor>; mark editor-family 07/08/09 done

97/97 editor unit tests pass.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two correctness fixes surfaced by a TipTap-conventions review:

- useEditor: in `format: 'json'`, every keystroke wrote a fresh getJSON()
  object back through v-model, which re-fired the content watcher and ran
  setContent(), rebuilding the doc and resetting the selection. The echo
  guard only covered HTML string-equality. Track the exact emitted value
  and skip the watcher when the incoming ref is our own write.

- MenuItems: the editor is built on @tiptap/core (no reactive customRef
  wrapper like @tiptap/vue-3 installs), so isActive/isDisabled reads never
  re-rendered on selection changes — EditorFixedMenu showed stale pressed
  and disabled (Undo/Redo) state. Bump a version ref on each transaction
  and route the bindings through it. Scoped per MenuItems instance so it
  works with or without <Editor>.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@netchampfaris netchampfaris added the beta-release Auto-publish a beta to npm when the PR is merged label Jun 1, 2026
@netchampfaris
Copy link
Copy Markdown
Contributor Author

/barista review

@barista-for-frappe
Copy link
Copy Markdown

Re-reviewing per @netchampfaris — focused on the public API surface of the new editor family.

Concerns — the new surface is clean, but the deprecation bridge misdirects migrating consumers.

  • src/components/TextEditor/TextEditor.vue (onMounted, the new console.warn) — the message says "Use <RichTextEditor> for standard rich text", but ADR 0004 explicitly does not ship RichTextEditor/CommentEditor/InlineEditor (spec/adr/0004-...md + index.ts exports none of them; docs/.../molecules/editor.md:337 documents <Editor> + kits instead). Anyone following the warning hunts for a component that doesn't exist. Point it at the real surface: <Editor> + RichTextKit from frappe-ui/editor. The string looks carried over from the stale v1-release/research/12-editor-v1-preview.md:25866 plan that predates the no-ready-mades decision.
  • Same line — the warn is an unconditional console.warn in onMounted, so it fires once per instance per mount. The migration plan called for a "one-time warning per mount lifecycle"; a module-level warnOnce guard (and/or dev-only gate) would avoid spamming prod consoles where many <TextEditor>s render.

API tightness — looks good. This is the direction the library should move:

  • Editor.vue uses canonical events (change/focus/blur), unnamed v-model via defineModel, and a single format: 'html' | 'json' instead of v-model:html/:json — good P1/P2 call.
  • Building blocks (EditorContent/EditorFixedMenu/EditorBubbleMenu/EditorFloatingMenu) style via data-slot rather than class-injection props (P10), and resolve the editor from context with an optional editor? override — no forced :editor threading.
  • useEditor options stay primitive (content/format/editable/autofocus/uploadFunction/extensions); no config-blob, no semantic-color axis, no per-component renames. Kits configure via the standard tiptap .configure({ member: false }) pattern. Tests cover kit composition and the Cypress behaviors.

No public prop/slot/event drift found. The two items above are both in the deprecation shim, not the new API.

Point migrating consumers at the real surface (<Editor> + RichTextKit
from frappe-ui/editor) instead of the nonexistent <RichTextEditor>, and
guard the warning behind a module-scoped flag and import.meta.env.DEV so
it fires once per process and never in production.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@netchampfaris netchampfaris changed the title feat(editor): add editor molecule family feat(editor): Editor molecule Jun 1, 2026
@netchampfaris netchampfaris merged commit 0ff53a9 into main Jun 1, 2026
7 checks passed
pull Bot pushed a commit to Zezo-Ai/Zezo-Ai-frappe-ui that referenced this pull request Jun 2, 2026
The `secrets` context is not available in step `if:` conditions, so
`if: ${{ secrets.RELEASE_TOKEN == '' }}` made beta-release.yml invalid.
GitHub failed every run at parse time ("workflow file issue"), so PR frappe#718
(labeled `beta-release`) never bumped the version or published a beta.

Hoist RELEASE_TOKEN to a job-level env var and test `env.RELEASE_TOKEN`.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

beta-release Auto-publish a beta to npm when the PR is merged

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant