Feature: Removing an inline image from the editor As a verified RenewCMS owner writing an article I want to remove an image I've already inserted into the article body, either through an explicit delete button or by pressing Backspace/Delete on the selected image, directly from the editor So that a mistaken or unwanted image doesn't have to stay in the article forever, and removing it behaves like every other destructive action in RenewCMS: it asks for confirmation first, however the user starts it Background: Given docs/design/ has no board or state for removing an inline image; this feature was added at the user's explicit request ahead of design coverage, reusing the editor's existing visual language rather than inventing new components: the trash icon and error color already used by the article "Delete" button, and the ConfirmDialog component already used by "Delete this article?" (docs/design/screens/editor.md's "Delete: confirm" state) And this slice changes ArticleEditor's exposed insertImage(src, alt) to insertImage(src, alt, imageId), stamping the inserted node with the image's numeric id as a data-image-id attribute, persisted in the saved article body HTML; EditorView passes the id it now receives from a successful upload (features/remove_article_image.feature's change to POST .../image's 201 body) through to insertImage And an image already present in an article's body from before this slice has no data-image-id attribute and so shows no delete affordance and is not intercepted by Backspace/Delete, until it is removed and re-added And selecting an image node that does carry a data-image-id attribute shows a small delete button anchored to the image's top-right corner, icon i-lucide-trash-2, color error, matching the article Delete button's icon and color; deselecting the image (clicking elsewhere) hides it And there are two equivalent triggers for the same removal flow: activating that delete button, and pressing Backspace or Delete while the image node is selected (a Tiptap NodeSelection on the image). Tiptap's native node deletion on Backspace/Delete is intercepted before it reaches the document (e.g. via a keyboard-shortcut/keydown handler registered on the Image node or at the editor level, checked first while an image node is selected) so the key press never removes the node directly; instead it triggers exactly the same flow as the button And either trigger emits ArticleEditor's new "request-remove-image" event with the node's numeric image id; ArticleEditor does not call the API or show any dialog itself, mirroring how "request-image" already only opens ImageUploader from EditorView And EditorView reacts to "request-remove-image" by opening the existing ConfirmDialog component with title "Remove this image?", description "This image will be removed from the article. This can't be undone.", confirmLabel "Remove image", cancelLabel "Cancel", pendingLabel "Removing…", tone "danger" — the same component and tone already used for "Delete this article?" And ConfirmDialog's action calls DELETE /v1/article/{articleId}/image/ {imageId} through useApi (not the raw XMLHttpRequest used for upload, since there is no progress to observe) And on success (204) the action removes the image node from ArticleEditor's content via a new exposed removeImage(imageId) method, which moves useArticleDraft's state to "dirty" the same way any other body edit does; ConfirmDialog then closes and a toast appears: color success, icon i-lucide-circle-check, title "Image removed", description "Save to keep this change." And on any failure (404 for either the article or the image, 500, or a network failure) the action throws, so ConfirmDialog shows its own inline error "Couldn't remove the image, so it's still there. Try again in a moment." and stays open; the image node is not removed and the draft's dirty state is unchanged And a 401 is handled entirely by useApi's global handling (features/api_client_global_error_handling.feature) and is out of scope for new scenarios here And once the confirm dialog is open, its cancel/confirm/failure behaviour (covered below) is identical regardless of which trigger opened it, so those scenarios are written once, against the button trigger, and are not duplicated per trigger Scenario: Selecting an inserted image reveals its delete button Given the user is on /article/12/edit with an image in the body carrying data-image-id "10" When the user clicks that image Then a delete button (trash icon, error color) appears at the image's top-right corner When the user clicks elsewhere in the editor Then the delete button is no longer shown Scenario Outline: Either trigger opens the confirm dialog and sends no request yet, without letting Tiptap delete the node itself Given the image with data-image-id "10" is selected on /article/12/edit When the user Then a dialog titled "Remove this image?" opens, with description "This image will be removed from the article. This can't be undone.", cancel "Cancel" and confirm "Remove image" And no request has been sent to DELETE /v1/article/12/image/10 And the image node is still present in the editor's content (Tiptap's native node deletion did not run) Examples: | triggers-removal | | activates the image's delete button | | presses Backspace with the image node selected | | presses Delete with the image node selected | Scenario: Cancelling the confirm dialog changes nothing Given the confirm dialog is open for image id 10 on /article/12/edit When the user activates "Cancel" Then the dialog closes And no request has been sent to DELETE /v1/article/12/image/10 And the image is still present in the editor's content And the draft's dirty state is unchanged Scenario: Confirming removes the image, marks the draft dirty, and shows a success toast Given the confirm dialog is open for image id 10 on /article/12/edit, the draft in the Saved state When the user activates "Remove image" Then DELETE /v1/article/12/image/10 is sent When that request resolves 204 Then the dialog closes And the image is removed from the editor's content And SaveStatus reads "Unsaved changes" and the Save button becomes enabled And a toast appears: color success, icon i-lucide-circle-check, title "Image removed", description "Save to keep this change." Scenario Outline: A failed removal keeps the image and shows the dialog's inline error Given the confirm dialog is open for image id 10 on /article/12/edit When the user activates "Remove image" And DELETE /v1/article/12/image/10 Then the dialog stays open, showing the inline error "Couldn't remove the image, so it's still there. Try again in a moment." And the image is still present in the editor's content And the draft's dirty state is unchanged Examples: | failure | | resolves with a 404 response | | resolves with a 500 response | | never reaches the server | Scenario: Confirming a removal started by Backspace/Delete behaves exactly like confirming one started by the button Given the confirm dialog is open for image id 10 on /article/12/edit, opened by pressing Backspace with the image node selected, the draft in the Saved state When the user activates "Remove image" Then DELETE /v1/article/12/image/10 is sent When that request resolves 204 Then the dialog closes And the image is removed from the editor's content And SaveStatus reads "Unsaved changes" and the Save button becomes enabled And a toast appears: color success, icon i-lucide-circle-check, title "Image removed", description "Save to keep this change."