Skip to content

refactor(Editor): the command-handler debt is upstream's, not ours — not diverging for it #88

Description

@IgorShevchik

Closed as not planned. Re-measured against nuxt/ui@v4: every as any this issue counts is upstream's, and the prompt() it calls a UX defect is not reached on the documented path. Fixing either means diverging from a file we sync. Figures below.


Background

Surfaced during the June 2026 multi-angle project audit (engineering review). Repo-wide TS discipline is otherwise strong (0 @ts-ignore, 1 as unknown as); the Editor stack is the concentrated exception. Complements #56, which covers only the DateTimePicker/@internationalized/date casts.

Problem as filed

  • src/runtime/utils/editor.ts (502 lines): 19 as any + 22 : any, 0/16 exported functions documented. Every handler takes cmd: any; commands are invoked via (editor.can() as any)[fnName](); mapEditorItems returns any[] | any[][].
  • EditorToolbar.vue: 5 as any + 3 vague @ts-expect-error.
  • useEditorMenu.ts (685 lines): cleanupMenu vs destroy duplicate ~25 lines of teardown.
  • EditorSuggestionMenu.vue: 7 : any.
  • createLinkHandler / createImageHandler call native prompt() — "blocking, unstylable, untestable, and inconsistent with the rest of the kit's overlay system".

Re-measured, 2026-08-30

The typing debt is upstream's, to the line

Counted the same way in both trees (grep -o … | wc -l), against nuxt/ui@v4:

file as any upstream as any here : any upstream : any here
src/runtime/utils/editor.ts 21 21 22 22
src/runtime/composables/useEditorMenu.ts 3 3 5 5
src/runtime/components/EditorSuggestionMenu.vue 0 0 7 7

editor.ts diffs against upstream as 85 lines added, 0 removed — it is their file verbatim with our code appended, and our 85 lines contribute no as any at all. (The issue's "502 lines / 19" was accurate in June; the file is now 587 lines because of that appended code, which is why the counts here read 21.)

So there is no local sloppiness to clean up. Rewriting the handler layer into a discriminated EditorCommand union means rewriting upstream's file in 45 places, in exactly the code path we re-sync from them — the same cost we declined to pay in #87 for the snapshot corpus, and for the same reason.

The prompt() is not on the documented path

It is a fallback, not the default experience:

// utils/editor.ts — createLinkHandler
const href = cmd?.href || prompt('Enter the URL:')

The first example on the Editor docs page ships its own EditorLinkPopover.vue, which calls editor.chain().setLink({ href }) directly and never enters createLinkHandler. Images behave the same way. The native dialog appears only if a consumer drives the built-in toolbar item without supplying href and without wiring a popover — which is not what the documentation shows or recommends.

Both prompt() calls are also upstream's, at src/runtime/utils/editor.ts:112 and :150 in nuxt/ui@v4, character for character.

What is actually ours

Three @ts-expect-error in EditorToolbar.vue, where upstream has none, carrying the comment need test at nuxt.ui? but this work. Someone silenced the type checker and was not sure why. That is a handful of lines and the only part of this issue that is not upstream's; it is worth doing on its own terms, not as a refactor of the whole layer.

Decision

Not diverging from upstream for inherited debt in a file we sync. If upstream types this layer, we pick it up on the next sync for free.

The trigger to reopen: upstream typing it and us failing to pick it up, or a consumer actually hitting the prompt() fallback in a supported configuration.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or requestwontfixThis will not be worked on

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions