Skip to content

feat: M4 · media bridge for Gutenberg MediaUpload (#314) - #330

Merged
ViewFromTheBox merged 2 commits into
release/1.0from
feature/314-media-bridge
Apr 19, 2026
Merged

ViewFromTheBox merged 2 commits into
release/1.0from
feature/314-media-bridge

Conversation

@ViewFromTheBox

@ViewFromTheBox ViewFromTheBox commented Apr 19, 2026 •

Copy link
Copy Markdown
Member

Description

Wires artisanpack-ui/media-library's React MediaModal and uploadMedia into Gutenberg's MediaUpload slot-fill so every core media block — core/image, core/gallery, core/video, core/audio, core/file, core/cover, core/media-text — is production-usable inside the visual editor. Replaces the M2 stub that only surfaced an alert.

Closes: #314

Type of Change

  • New feature (adds new functionality)

Related Issue

Issue: #314 (milestone M4 under umbrella #309)

Motivation and Context

Without this, Gutenberg's core media blocks either crash trying to reach the WP REST API or render an empty picker. After this, the picker opens the MediaModal from artisanpack-ui/media-library, drag-and-drop uploads to the canvas go through the same uploadMedia pipeline, and selections flow back to the block as Gutenberg-shape attachments with id, url, alt, caption, mime, width, height, media_type, sizes.

The bridge stays framework-decoupled — the visual editor doesn't hard-depend on media-library. Hosts register the picker at boot:

import { MediaModal, uploadMedia } from '@artisanpack-ui/media-library';
import { registerMediaBridge } from '@artisanpack-ui/visual-editor';

registerMediaBridge({ MediaBridge: MediaModal, uploadMedia });

Changes Made

  • resources/js/visual-editor/media-bridge/ — new module (adapter, slot-fill, settings callback, registry, barrel)
    • types.ts — BridgeMedia, MediaBridgeComponent, GutenbergMedia, MediaUploader, etc.
    • adapter.ts — mediaToGutenberg() (including sizes extraction + media_type inference) and allowedTypesToBridgeTypes() (handles both categorical names and mime types)
    • media-upload.tsx — the slot-fill replacement registered under editor.MediaUpload; supports both render and cloned-child trigger patterns; handles multiple/gallery fan-out
    • media-upload-setting.ts — settings.mediaUpload callback for drag-and-drop and block-library upload helpers; unwraps { data } response envelopes
    • state.ts — registerMediaBridge() public API, ensureMediaBridgeFilter(), idempotent filter install, test reset helper
    • index.ts — barrel
  • editor/editor-app.tsx — swaps M2 stub (registerMediaUploadStub / mediaUploadStub) for ensureMediaBridgeFilter() / mediaUploadSetting
  • editor/main.tsx — re-exports registerMediaBridge (and companion types) so hosts can wire the bridge from the package's entry
  • sandbox/sandbox-editor.tsx — mirrors the swap so the sandbox canvas uses the new filter
  • resources/js/visual-editor/vendor/media-upload-stub.tsx — deleted; superseded
  • config/visual-editor.php — updated comment on media.adapter: the M4 wiring is client-side via registerMediaBridge()

How Has This Been Tested?

Testing Environment:

  • Operating System: macOS 14 (Darwin 25.3.0)
  • Browser: Safari (manual), Chrome (sandbox)
  • PHP Version: 8.4.3
  • Project Version: release/1.0

Tests Performed:

  1. ./vendor/bin/pest — all 66 Pest tests pass (no PHP changes; verified the package's existing suite stays green).
  2. npm test — all 34 vitest files pass (267 tests, 1 skipped). 31 new tests cover the media bridge.
  3. Manual browser verification via the dev app (~/Herd/artisanpack-ui):
    • Added core/image → clicked Media Library → MediaModal opened, showed library + upload tabs, selection round-tripped into the block with alt/caption/dimensions.
    • Repeated for core/gallery (multi-select), core/video, core/audio, core/file, core/cover, core/media-text.
    • Drag-and-drop onto the canvas went through uploadMedia and inserted the resulting media.

Accessibility Tests Run

  • Keyboard navigation tested (MediaModal's built-in focus management)
  • Screen reader tested
  • Color contrast verified (DaisyUI theme)
  • ARIA labels checked (MediaModal's role="dialog", escape-to-close, focus trap)

Details: Keyboard flow was exercised in the browser — Tab/Shift-Tab cycles through modal controls, Enter confirms a focused media item, Esc closes the modal. The bridge itself doesn't introduce new focusable UI — it delegates entirely to MediaModal, whose a11y is covered in the media-library package.

Tests Added

  • Unit tests added/updated — adapter.test.ts (13 tests) covering shape conversion, is_image/is_video/is_audio flag precedence, mime-prefix fallback, image sizes in metadata, and allowedTypes → bridge categories.
  • Integration tests added/updated:
    • media-upload.test.tsx (5 tests) — slot-fill component with and without a registered bridge, render/child trigger patterns, single vs. multi-select, gallery: true fan-out.
    • media-upload-setting.test.ts (6 tests) — uploader invocation, { data } unwrapping, maxUploadFileSize enforcement, error propagation, empty-list short-circuit.
    • core-blocks.test.tsx (7 tests) — per-block contract replay for the seven core media blocks, verifying each emits the expected multiSelect / allowedTypes and that onSelect receives a single record or array per block.
  • All tests passing (267 vitest, 66 Pest).

Test details: Direct Gutenberg block rendering via @wordpress/block-library + registerCoreBlocks couldn't be exercised inside Vitest because @wordpress/block-editor's block-compare imports diff/lib/diff/character without a file extension — Node's ESM resolver rejects it while Vite's browser build accepts it. Rather than ship a fragile resolve.alias patch, the integration test replays each block's <MediaUpload> invocation at the filter level with the exact props core blocks send. End-to-end verification in a real editor is handled by the sandbox and the dev app.

Documentation

  • Inline code documentation added (file-level block comments explaining each module's role; intent notes on non-obvious choices like the unwrap helper and filter-scoped component reference in state.ts)
  • README updated
  • Wiki updated
  • API documentation updated

Documentation details: Public API notes live in the module docblocks. The umbrella docs (docs/gutenberg-adoption.md) already reference M4 by issue; no structural docs change is required before the full V1 release docs pass in M15 (#325).

Pre-Submission Checklist

  • Followed contributing guidelines
  • Checked for other open PRs for same update
  • Code passes all tests
  • Code has been linted
  • Accessibility tests completed
  • Code follows project style guide
  • Self-review completed
  • Comments added for complex code
  • No new warnings generated

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Replaced the legacy media stub with a client-side media bridge so the visual editor opens a real media picker and uploads properly.
  • Tests

    • Added comprehensive tests covering media adapters, upload flows, settings behavior, and core block integration.
  • Documentation

    • Updated visual editor configuration comments to clarify the media adapter/bridge usage and binding guidance.
  • Chores

    • Removed the old media-upload stub and wired the new bridge initialization.

Replaces the M2 stub with a production slot-fill that routes core media
blocks (image, gallery, video, audio, file, cover, media-text) through
`artisanpack-ui/media-library`'s `MediaModal` and `uploadMedia`.

Hosts wire the bridge by calling `registerMediaBridge({ MediaBridge,
uploadMedia })` before `bootVisualEditor()`. The slot-fill translates
Gutenberg's MediaUpload contract — `allowedTypes`, `multiple`, `value`,
`onSelect`, `render` — onto MediaModal's props, and the adapter converts
media-library `Media` records to the `{ id, url, alt, caption, mime,
width, height, media_type, sizes }` shape core blocks read. Drag-and-drop
onto the canvas flows through the same `uploadMedia` pipeline via
`settings.mediaUpload`, so there is no separate upload endpoint.

When no bridge is registered the picker surfaces a loud operator-facing
message instead of silently dropping clicks (the previous stub behaviour,
preserved for apps that haven't yet wired media-library).

Tests: 31 new vitest cases across adapter shape conversion, slot-fill
behaviour, settings.mediaUpload upload flow, and per-block contract
coverage for the seven core media blocks. End-to-end verification against
a real `MediaModal` is done via the sandbox and the dev app.

Closes #314

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Apr 19, 2026 •

Copy link
Copy Markdown

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 1e051c5a-8d9f-434d-ae8d-91e021b9dfb7

📥 Commits

Reviewing files that changed from the base of the PR and between f8aba4f and f26a537.

📒 Files selected for processing (4)
  • resources/js/visual-editor/media-bridge/__tests__/media-upload-setting.test.ts
  • resources/js/visual-editor/media-bridge/__tests__/media-upload.test.tsx
  • resources/js/visual-editor/media-bridge/media-upload-setting.ts
  • resources/js/visual-editor/media-bridge/media-upload.tsx
✅ Files skipped from review due to trivial changes (1)
  • resources/js/visual-editor/media-bridge/tests/media-upload.test.tsx
🚧 Files skipped from review as they are similar to previous changes (3)
  • resources/js/visual-editor/media-bridge/tests/media-upload-setting.test.ts
  • resources/js/visual-editor/media-bridge/media-upload.tsx
  • resources/js/visual-editor/media-bridge/media-upload-setting.ts

📝 Walkthrough

Walkthrough

Client-side media bridge added: new media-bridge modules (types, state, adapter, UI, upload-setting), re-exports from editor entry, editor boot and sandbox now wire the bridge setting/filter, legacy media-upload stub removed, and comprehensive unit/integration tests added. (No runtime config defaults changed.)

Changes

Cohort / File(s) Summary
Configuration
config/visual-editor.php
Updated comment for media.adapter to describe it as reserved for server-side binding and document client-side media bridge wiring; no runtime value changes.
Public API / Editor Entry
resources/js/visual-editor/editor/main.tsx, resources/js/visual-editor/editor/editor-app.tsx
Re-exported registerMediaBridge and media types from media-bridge; editor boot now calls ensureMediaBridgeFilter() and uses mediaUploadSetting instead of the legacy stub.
Media Bridge Core
resources/js/visual-editor/media-bridge/types.ts, resources/js/visual-editor/media-bridge/state.ts, resources/js/visual-editor/media-bridge/index.ts
Added type contracts, in-memory registry/state for bridge + uploader, public index exporting adapter, components, state helpers, and types.
Adapter & Upload Logic
resources/js/visual-editor/media-bridge/adapter.ts, resources/js/visual-editor/media-bridge/media-upload-setting.ts
Added pure adapter functions to map BridgeMedia→GutenbergMedia and to normalize allowedTypes; implemented mediaUploadSetting to resolve uploader at call time, validate sizes, run uploads, unwrap results, and call Gutenberg callbacks.
Slot-fill Replacement UI
resources/js/visual-editor/media-bridge/media-upload.tsx
Added MediaUploadBridge component that installs as Gutenberg MediaUpload replacement, resolves registered bridge component, maps props (multiSelect/allowedTypes/title/context), converts selection to Gutenberg shape, and registers itself with slot-fill filter helper.
Tests
resources/js/visual-editor/media-bridge/__tests__/*
Added unit tests for adapter mapping and allowed-types conversion, tests for mediaUploadSetting behavior (errors, uploads, size limits), and integration tests verifying core blocks' prop translation and MediaUploadBridge behavior.
Sandbox & Legacy Removal
resources/js/visual-editor/sandbox/sandbox-editor.tsx, resources/js/visual-editor/vendor/media-upload-stub.tsx
Sandbox switched to ensureMediaBridgeFilter()/mediaUploadSetting; removed the legacy media-upload-stub.tsx implementation and its exports.

Sequence Diagram(s)

sequenceDiagram
    participant Host as Host App
    participant Reg as registerMediaBridge()
    participant State as Bridge State
    participant Filter as WP filter (editor.MediaUpload)
    participant Block as Gutenberg Block
    participant MUB as MediaUploadBridge
    participant Bridge as Registered Media Bridge
    Host->>Reg: registerMediaBridge({ MediaBridge, uploadMedia })
    Reg->>State: store bridge & uploader
    Reg->>Filter: ensure editor.MediaUpload filter installed
    Block->>MUB: render / user clicks media button
    MUB->>State: getMediaBridge()
    State-->>MUB: Bridge component | null
    alt Bridge present
        MUB->>Bridge: render(open=true, multiSelect, allowedTypes, title)
        Bridge->>MUB: onSelect(BridgeMedia[])
        MUB->>MUB: mediaToGutenberg(...)
        MUB->>Block: onSelect(GutenbergMedia | GutenbergMedia[])
    else No bridge
        MUB->>MUB: alert("registerMediaBridge")
    end
Loading
sequenceDiagram
    participant Block as Gutenberg Block
    participant MUS as mediaUploadSetting()
    participant State as Bridge State
    participant Uploader as Host Uploader
    participant Adapter as mediaToGutenberg()
    participant Callback as onFileChange()
    Block->>MUS: call mediaUploadSetting(options with filesList)
    MUS->>State: getMediaUploader()
    alt no uploader
        MUS->>Block: options.onError(message)
    else uploader present
        MUS->>MUS: validate maxUploadFileSize (may error)
        loop each file
            MUS->>Uploader: uploader(file, additionalData)
            Uploader-->>MUS: BridgeMedia or { data: BridgeMedia }
            MUS->>Adapter: mediaToGutenberg(BridgeMedia)
        end
        MUS->>Callback: onFileChange(GutenbergMedia[])
    end
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Poem

🐰 I stitched a bridge from hop to byte,
Adapters hum through day and night,
Blocks call forth the picker’s light,
Tests ensure each photo’s right,
A rabbit cheers — the picker’s bright! 🥕✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 34.62% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'feat: M4 · media bridge for Gutenberg MediaUpload (#314)' accurately describes the main change: implementing a media bridge for Gutenberg's MediaUpload slot-fill to integrate artisanpack-ui/media-library with core media blocks.
Linked Issues check ✅ Passed The PR implementation satisfies all coding requirements from issue #314: media bridge component wired into MediaUpload slot-fill [#314], props translated between Gutenberg and MediaModal contracts [#314], adapter converts media-library Media to Gutenberg shape [#314], uploads flow through media-library pipeline [#314], and comprehensive tests added covering adapter and core-block integration [#314].
Out of Scope Changes check ✅ Passed All changes are within scope of issue #314 implementation: new media-bridge module and its components, adapter functions, type definitions, registration/state management, test suites, updates to editor boot sequence, and removal of legacy media-upload-stub.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/314-media-bridge

Comment @coderabbitai help to get the list of available commands and usage tips.

@ViewFromTheBox ViewFromTheBox moved this from Backlog to In Review in ArtisanPack UI Overview Apr 19, 2026
@ViewFromTheBox ViewFromTheBox linked an issue Apr 19, 2026 that may be closed by this pull request
7 tasks
@ViewFromTheBox
ViewFromTheBox marked this pull request as ready for review April 19, 2026 21:26

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🧹 Nitpick comments (3)
resources/js/visual-editor/media-bridge/__tests__/media-upload-setting.test.ts (1)

139-160: Add a test for synchronous uploader throws.

Current coverage checks rejected promises, but not throw new Error(...) before a promise is returned. Adding that case will protect the error-path contract from regressions.

✅ Suggested test case
+    it('surfaces synchronous uploader throws via onError', async () => {
+        const uploader: MediaUploader = () => {
+            throw new Error('sync boom');
+        };
+
+        registerMediaBridge({
+            MediaBridge: () => null,
+            uploadMedia: uploader,
+        });
+
+        const onError = vi.fn();
+        mediaUploadSetting({
+            filesList: [textFile('a.txt', 'a')],
+            onError,
+        });
+
+        await vi.waitFor(() => {
+            expect(onError).toHaveBeenCalledTimes(1);
+        });
+        expect(onError.mock.calls[0]?.[0]).toBe('sync boom');
+    });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@resources/js/visual-editor/media-bridge/__tests__/media-upload-setting.test.ts`
around lines 139 - 160, Add a test that verifies synchronous throw from an
uploader is surfaced via the onError callback: create a MediaUploader
implementation that throws (e.g., const uploader: MediaUploader = () => { throw
new Error('boom') }), register it with registerMediaBridge via the uploadMedia
property, call mediaUploadSetting with filesList and an onError spy (vi.fn()),
wait for the callback with vi.waitFor and assert onError was called once and its
first argument is the thrown error message; use the same structure/expectations
as the existing rejection test to ensure both Promise rejections and synchronous
throws are covered.
resources/js/visual-editor/media-bridge/state.ts (1)

73-74: Comment is stale relative to implementation.

Line 73-74 references an “import inside the callback,” but there is no import there. Please update the comment to reflect the current lazy variable-resolution approach.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@resources/js/visual-editor/media-bridge/state.ts` around lines 73 - 74, The
inline comment above the block that used to do a callback import is outdated;
update the comment in state.ts near the lazy resolution logic (look for the lazy
variable-resolution implementation or function/getter that defers resolving
media-upload dependencies) to describe the current lazy variable-resolution
approach instead of mentioning an "import inside the callback" and explain
briefly that the module avoids a cycle by resolving the dependency lazily at
runtime rather than performing an import in the callback.
resources/js/visual-editor/media-bridge/adapter.ts (1)

72-73: Adjust docstring to match actual allowedTypesToBridgeTypes behavior.

The comment says all-inclusive input returns undefined, but the function currently returns all recognized categories. Please align comment or implementation.

Doc-only fix example
- * Returns `undefined` when the input would allow every category so the
- * picker does not over-constrain itself.
+ * Returns `undefined` only when no recognized category is present (or the
+ * input is empty/omitted).
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@resources/js/visual-editor/media-bridge/adapter.ts` around lines 72 - 73, The
docstring for allowedTypesToBridgeTypes is incorrect: it claims "Returns
`undefined` when the input would allow every category" but the implementation
returns all recognized categories instead; update the docstring to state that
when the input allows every category the function returns an array of all bridge
categories (i.e., all recognized categories) rather than undefined, and include
a short example or phrase clarifying that the result is an explicit list of
categories rather than undefined so callers know to expect an array from
allowedTypesToBridgeTypes.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@resources/js/visual-editor/media-bridge/media-upload-setting.ts`:
- Around line 72-76: The files.map call currently calls uploader(file,
options.additionalData) synchronously before Promise.resolve, so synchronous
throws escape the promise chain; change the mapping to defer uploader execution
by invoking Promise.resolve(file).then(() => uploader(file,
options.additionalData)).then(unwrap) (or equivalent) so any synchronous
exceptions from uploader are caught by the existing .catch and routed to
onError; ensure references to files.map, uploader, options.additionalData,
Promise.resolve, unwrap and the onError/.catch chain are preserved.

In `@resources/js/visual-editor/media-bridge/media-upload.tsx`:
- Around line 128-132: The click handler currently always calls open() after
invoking existingOnClick, which violates child intent when the child prevents
the default; update the cloned onClick (used in cloneElement) to invoke
existingOnClick(event) first, then check the event for cancellation (e.g., if
event is an Event and event.defaultPrevented is true or (event as
any)?.defaultPrevented) and only call open() when the click was not prevented;
reference the cloned onClick wrapper, existingOnClick, and open to locate and
change the logic.
- Around line 39-52: The trigger callback currently uses the render-time
captured Bridge variable (const Bridge = getMediaBridge()), which can be stale;
instead, inside the open handler of renderTrigger call resolve the bridge at
click time by calling getMediaBridge() and then proceed — i.e., replace the use
of the outer Bridge in the open function with a fresh const bridge =
getMediaBridge(), check bridge === null and call notifyUnconfigured() if needed,
otherwise call setOpen(true); update references to Bridge, getMediaBridge,
renderTrigger, notifyUnconfigured, and setOpen accordingly.

---

Nitpick comments:
In
`@resources/js/visual-editor/media-bridge/__tests__/media-upload-setting.test.ts`:
- Around line 139-160: Add a test that verifies synchronous throw from an
uploader is surfaced via the onError callback: create a MediaUploader
implementation that throws (e.g., const uploader: MediaUploader = () => { throw
new Error('boom') }), register it with registerMediaBridge via the uploadMedia
property, call mediaUploadSetting with filesList and an onError spy (vi.fn()),
wait for the callback with vi.waitFor and assert onError was called once and its
first argument is the thrown error message; use the same structure/expectations
as the existing rejection test to ensure both Promise rejections and synchronous
throws are covered.

In `@resources/js/visual-editor/media-bridge/adapter.ts`:
- Around line 72-73: The docstring for allowedTypesToBridgeTypes is incorrect:
it claims "Returns `undefined` when the input would allow every category" but
the implementation returns all recognized categories instead; update the
docstring to state that when the input allows every category the function
returns an array of all bridge categories (i.e., all recognized categories)
rather than undefined, and include a short example or phrase clarifying that the
result is an explicit list of categories rather than undefined so callers know
to expect an array from allowedTypesToBridgeTypes.

In `@resources/js/visual-editor/media-bridge/state.ts`:
- Around line 73-74: The inline comment above the block that used to do a
callback import is outdated; update the comment in state.ts near the lazy
resolution logic (look for the lazy variable-resolution implementation or
function/getter that defers resolving media-upload dependencies) to describe the
current lazy variable-resolution approach instead of mentioning an "import
inside the callback" and explain briefly that the module avoids a cycle by
resolving the dependency lazily at runtime rather than performing an import in
the callback.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 0ecd3ba9-d03f-42fd-9337-fe4120cc5131

📥 Commits

Reviewing files that changed from the base of the PR and between 8c397a5 and f8aba4f.

📒 Files selected for processing (15)
  • config/visual-editor.php
  • resources/js/visual-editor/editor/editor-app.tsx
  • resources/js/visual-editor/editor/main.tsx
  • resources/js/visual-editor/media-bridge/__tests__/adapter.test.ts
  • resources/js/visual-editor/media-bridge/__tests__/core-blocks.test.tsx
  • resources/js/visual-editor/media-bridge/__tests__/media-upload-setting.test.ts
  • resources/js/visual-editor/media-bridge/__tests__/media-upload.test.tsx
  • resources/js/visual-editor/media-bridge/adapter.ts
  • resources/js/visual-editor/media-bridge/index.ts
  • resources/js/visual-editor/media-bridge/media-upload-setting.ts
  • resources/js/visual-editor/media-bridge/media-upload.tsx
  • resources/js/visual-editor/media-bridge/state.ts
  • resources/js/visual-editor/media-bridge/types.ts
  • resources/js/visual-editor/sandbox/sandbox-editor.tsx
  • resources/js/visual-editor/vendor/media-upload-stub.tsx
💤 Files with no reviewable changes (1)
  • resources/js/visual-editor/vendor/media-upload-stub.tsx

Comment thread resources/js/visual-editor/media-bridge/media-upload-setting.ts
Comment thread resources/js/visual-editor/media-bridge/media-upload.tsx
Comment thread resources/js/visual-editor/media-bridge/media-upload.tsx
- `media-upload-setting.ts`: defer the `uploader` call into a resolved
  `.then()` so synchronous throws route through the existing `.catch`
  → `onError` path instead of escaping `Promise.all`.
- `media-upload.tsx` (cloned trigger): after invoking the child's
  `onClick`, short-circuit when the event was `preventDefault()`ed so
  callers that consume the click don't get a surprise picker.
- `media-upload.tsx` (click handler): re-resolve the registered bridge
  at click time via `getMediaBridge()` rather than closing over the
  render-time snapshot — covers hot-reload and out-of-order boots
  where registration happens after the component first rendered.

Added three tests covering the new behaviours.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@ViewFromTheBox
ViewFromTheBox merged commit 6efe2fc into release/1.0 Apr 19, 2026
1 check passed
@github-project-automation github-project-automation Bot moved this from In Review to Done in ArtisanPack UI Overview Apr 19, 2026
@ViewFromTheBox
ViewFromTheBox deleted the feature/314-media-bridge branch April 19, 2026 21:44
@coderabbitai coderabbitai Bot mentioned this pull request Apr 21, 2026
19 of 21 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Development

Successfully merging this pull request may close these issues.

M4 · Media bridge

1 participant