Skip to content

feat: media-image rich-editor extension - #12

Merged
bambamboole merged 15 commits into
mainfrom
feat/rich-editor-media-image
Aug 1, 2026
Merged

feat: media-image rich-editor extension#12
bambamboole merged 15 commits into
mainfrom
feat/rich-editor-media-image

Conversation

@bambamboole

@bambamboole bambamboole commented Aug 1, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds a media-image rich-editor extension built on lattice's server-side extension API: pick images from the media library inside RichEditor, store only {id, alt, conversion} per node, and resolve URLs server-side on every render/prefill (temporary/signed disk URLs just work).

  • PHP: MediaImage (#[AsEditorExtension('media-image')], registered app-wide) + MediaImageNode (tiptap-php). prepareDocument() batch-resolves ids into ephemeral url/width/height/mediaAlt attrs that are scrubbed before storage; validateDocument() rejects missing, gate-denied, and non-image media with per-id messages; the sanitizer allows exactly what renderHTML() emits.
  • Client: Tiptap node with React NodeView (image preview, missing-media placeholder, alt override + conversion size controls) and a toolbar button that opens the existing LibraryView dialog (carried in the extension's wire props). The dialog is lazy-loaded so the media grid stays out of the eager bundle.
  • Model linking: MediaImage::idsIn($document) feeds the existing syncMedia() — documented in the README together with the mediaConversions() pairing.
  • Deps: requires lattice ≥0.36 — 0.36.0 ships the rich-editor fix that runs validation/casting for array-shaped document submissions, which this extension's attach gate relies on.

Usage:

RichEditor::make('body')->withExtensions(MediaImage::make()->conversions('hero'));

$post->update(['body' => $validated['body']]);
$post->syncMedia(MediaImage::idsIn($validated['body']), 'content');

Testing

  • composer check green on lattice 0.36.0: Pint, Larastan, 143 Feature tests (409 assertions) — extension seams (wire shape, ephemeral scrub, batch URL resolution incl. conversions + nested nodes, sanitizer attrs, validation modes, idsIn).
  • npm run test (39) + npm run typecheck + npm run build green.
  • Browser suite 9/9 green, including a full round trip: insert via toolbar → save → stored doc has ids only (no url), attachment row in the content collection → reload re-renders the image.

🤖 Generated with Claude Code

bambamboole and others added 15 commits August 1, 2026 14:36
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Adds MediaImageNode (name mediaImage) and MediaImageView, the React
NodeView rendering the ephemeral url/mediaAlt attrs with an alt-text
input and a conversion select. Task 7 will add the toolbar and
registerMediaImage() to the same file.

Installs @testing-library/user-event for the NodeView interaction tests.
Adds the editor toolbar button that opens the LibraryView in pick mode
and inserts mediaImage nodes on confirm, wires registerMediaImage()
into plugin.ts so the extension is registered eagerly before the
editor mounts, and adds the EditorExtensionProps["media-image"] type
augmentation plus matching en/de translations.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Wires the RichEditor's body field with the MediaImage extension into
the workbench ProductMediaForm/Page, persisting the document and
syncing referenced media into the "content" collection, and adds a
browser test asserting the stored node has only {id, alt, conversion}
and the value round-trips on reload.
…d errors

Validates isImage() alongside the attach gate, constrains the picker's
upload accept to image/*, and replaces the generic reused validation
message with an id-carrying editor.not-attachable key so failures (missing,
gate-denied, non-image) are diagnosable. Adds Feature coverage for the new
non-image rejection, a nested mediaImage node, and sanitizer-preserved
width/height/loading attrs.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The insert-image toolbar control statically imported Dialog and
LibraryView, dragging the whole media-library grid stack into the eager
extension bundle registered at plugin boot. Extracts the dialog body into
media-image-dialog.tsx and loads it with React.lazy + Suspense, only when
the dialog is open. Test ids and insert attrs are unchanged; a standalone
media-image-dialog chunk now shows up separately in the build output.

Also asserts the "Original" conversion option renders alongside the
configured conversions.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@bambamboole
bambamboole merged commit b121a98 into main Aug 1, 2026
4 checks passed
@bambamboole
bambamboole deleted the feat/rich-editor-media-image branch August 1, 2026 15:54
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant