diff --git a/plugins/bifrost-player/CLAUDE.md b/plugins/bifrost-player/CLAUDE.md new file mode 100644 index 0000000..425b213 --- /dev/null +++ b/plugins/bifrost-player/CLAUDE.md @@ -0,0 +1,63 @@ +# Bifrost Player — Development Notes + +Production site: https://developershowcase3.wpcomstaging.com/ + +Resources to take into account: +- https://developer.wordpress.org/block-editor/reference-guides/interactivity-api/core-concepts/client-side-navigation/ +- https://make.wordpress.org/core/2026/02/23/changes-to-the-interactivity-api-in-wordpress-7-0/ +- https://make.wordpress.org/core/2025/11/12/changes-to-the-interactivity-api-in-wordpress-6-9/ +- https://make.wordpress.org/core/2025/11/12/interactivity-apis-client-navigation-improvements-in-wordpress-6-9/ +- https://make.wordpress.org/core/2025/03/24/interactivity-api-best-practices-in-6-8/ + + +## Build toolchain + +- When `--experimental-modules` is active, `@wordpress/scripts/config/webpack.config` returns an **array** of configs (scripts + modules). Do NOT spread it into a single object (`{ ...defaultConfig }`) — export the array directly: `module.exports = defaultConfig`. +- To get `style-index.css` output for a block, the block **must** have an `editorScript` entry in `block.json` (even if minimal). Without it, wp-scripts' scripts webpack doesn't create an entry and CSS imported in `index.js` is never extracted. A minimal `index.js` that just does `import './style.css'` is enough. + +## Interactivity API + +- **Generator actions that call `e.preventDefault()`** must be wrapped with `withSyncEvent()`. Without it, the browser processes the default action before the generator resumes from its first `yield`. +- **`getContext()`** returns the context of the element where the directive fires, not the element where the store is defined. This is what makes cross-block communication work (e.g., play-button sets context on its element, audio-player reads it in `playTrack`). +- **`data-wp-interactive` namespace conflicts**: Setting `data-wp-interactive="bifrost-player"` on an element inside a `data-wp-interactive="core/playlist"` region overrides the namespace for that subtree. This breaks core directives. Use `namespace::` prefixes instead (e.g., `data-wp-on--click="bifrost-player::actions.foo"`). +- **Cross-namespace directives**: Use the `namespace::` prefix to call actions from a different store without changing the element's namespace (e.g., `data-wp-init="bifrost-player::callbacks.initPlaylistBridge"` on a `core/playlist` element). + +## Interactivity Router (client-side navigation) + +- **Import map timing**: Blocks rendered during `wp_footer` (like our audio-player) have their `viewScriptModule` processed AFTER the import map is printed. This means `yield import('@wordpress/interactivity-router')` fails with `Failed to resolve module specifier`. Fix: enqueue the block's view script module early via `wp_enqueue_script_module('bifrost-player-audio-player-view-script-module')` in `wp_enqueue_scripts`. This forces WordPress to process the asset file (including dynamic dependencies) before the import map is printed. +- **Script module handle naming**: WordPress auto-generates handles from `block.json`. For non-core blocks: `str_replace('/', '-', blockName) . '-view-script-module'`. Example: `bifrost-player/audio-player` → `bifrost-player-audio-player-view-script-module`. +- **Do NOT use `wp_enqueue_script_module('@wordpress/interactivity-router')` directly** — enqueue the block's own view module instead, which declares the router as a dynamic dependency in its asset file. This matches how blocks normally work (the reference example at `block-development-examples/interactivity-router-2f43f8` doesn't enqueue the router directly either — it works because the block renders in page content, before the import map). +- **Navigation directives go on each `` tag, NOT on a parent container**. Event delegation from a parent breaks because `e.target` is whatever nested element was clicked (image, span), not the link. Use `getElement().ref.href` instead of `e.target.href`, which always returns the element the directive is on. +- **Block style variation suffix mismatch**: WordPress generates unique numbered class suffixes per page render (e.g., `is-style-site-footer--50` on page A, `--8` on page B). During client-side navigation, the router swaps stylesheets (new CSS targets `--8`) but elements outside router regions keep their old HTML (`--50`). Fix: add router regions to `core/template-part` blocks (header/footer) so their HTML is also updated, keeping suffixes in sync with the new CSS. The persistent audio player (injected via `wp_footer`, outside all regions) is unaffected because it uses custom classes, not block style variations. + +## Core/playlist integration + +- `core/playlist` uses a **locked** store (`{ lock: true }`). External stores cannot call its actions directly. +- Setting `data-wp-interactive="bifrost-player"` on playlist track `
  • ` elements overrides the namespace for that subtree, so core's `actions.changeTrack` silently no-ops — which is the desired behavior since the in-page waveform is hidden. + +## Persistent WaveformPlayer (`@arraypress/waveform-player`) + +The persistent audio player embeds its own `WaveformPlayer` instance for seekable playback with waveform visualization. The `
    `, `
    `, and `